Created
January 9, 2026 18:56
-
-
Save griffeth-barker/c72ef9d62fae3f1016bee6d41ef6d7c5 to your computer and use it in GitHub Desktop.
Create EntraID Enterprise Apps with SSO and Security Groups
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <# | |
| .SYNOPSIS | |
| Creates necessary componenets for an EntraID Enterprise Application with SAML SSO. | |
| .DESCRIPTION | |
| Creates an Application Registration, Enterprise Application, and two Security Groups | |
| in Microsoft EntraID based on provided parameters and associates them as necessary. | |
| .PARAMETER Name | |
| The name of the enterprise application. This is used to formulate the groups names | |
| as well. | |
| .PARAMETER IdentifierUri | |
| The Microsoft EntraID Identifier. This is the first text field in the SAML SSO setup | |
| GUI. | |
| .PARAMETER ReplyUri | |
| The ACS/Reply URL. This would be the second text field in the SAML SSO setup GUI. | |
| .PARAMETER OwnerUPN | |
| The UPN of the user who should be assigned as the owner of the User group. | |
| .PARAMETER Note | |
| An optional string to be added to the description of the groups and the notes | |
| field of the enterprise application. Helpful for including a reference ID to | |
| an internal ticketing system, etc. | |
| .INPUTS | |
| System.String[] | |
| .OUTPUTS | |
| System.Management.Automation.PSObject[] | |
| .EXAMPLE | |
| $appParams = @{ | |
| Name = 'App Name' | |
| IdentifierUri = 'https://example.domain.tld/app' | |
| ReplyUri = 'https://example.domain.tld/saml/acs' | |
| OwnerUPN = 'admin@domain.tld' | |
| Note = 'Requested by person in reference #12345' | |
| } | |
| New-GJBEntraAppDeployment $appParams | |
| Create a single enterprise app with SAML SSO. | |
| .EXAMPLE | |
| Import-Csv -Path "C:\path\to\appsToCreate.csv" | New-GJBEntraAppDeployment | |
| Create many enterprise apps with SAML SSO based on CSV input. | |
| .NOTES | |
| Requires Cloud Application Administrator or Global Administrator role in EntraID. | |
| Requires connection to Graph API with the following scopes: | |
| - 'Group.ReadWrite.all' | |
| - 'Application.ReadWrite.All' | |
| - 'AppRoleAssignment.ReadWrite.All' | |
| #> | |
| function New-GJBEntraAppDeployment { | |
| [CmdletBinding()] | |
| [OutputType([System.Management.Automation.PSObject])] | |
| param( | |
| [Parameter(Mandatory, ValueFromPipelineByPropertyName)] | |
| [ValidateNotNullOrEmpty] | |
| [string]$Name, | |
| [Parameter(Mandatory, ValueFromPipelineByPropertyName)] | |
| [ValidateNotNullOrEmpty] | |
| [string]$IdentifierUri, | |
| [Parameter(Mandatory, ValueFromPipelineByPropertyName)] | |
| [ValidateNotNullOrEmpty] | |
| [string]$ReplyUri, | |
| [Parameter(Mandatory, ValueFromPipelineByPropertyName)] | |
| [ValidateNotNullOrEmpty] | |
| [string]$UserGroupOwnerUpn, | |
| [Parameter(ValueFromPipelineByPropertyName)] | |
| [string]$Note | |
| ) | |
| begin { | |
| function timestamp { | |
| Get-Date -Format 'yyyy-MM-dd hh:mm:ss' | |
| } | |
| try { | |
| Write-Verbose "[$(timestamp)] Check for Microsoft Graph Connection" | |
| $MgContext = Get-MgContext -ErrorAction Stop | |
| Write-Verbose "[$(timestamp)] Connected to Tenant: $($MgContext.TenantId) as $($MgContext.Account)" | |
| } | |
| catch { | |
| Write-Error "[$(timestamp)] $($_.ErrorDetails) - $($_.Exception.Message))" | |
| exit 1 | |
| } | |
| Write-Verbose "[$(timestamp)] Deployment cycle beginning" | |
| } | |
| process { | |
| try { | |
| Write-Verbose "[$(timestamp)] Beginning processing for $($Name)" | |
| Write-Verbose "[$(timestamp)] Creating security groups '$($Name) Users Security' and '$($Name) Admins Security'" | |
| $CleanNickname = $Name -replace '[^a-zA-Z0-9]', '' # mailNickname is mandatory and must be alphanumeric | |
| $TargetOwner = Get-MgUser -UserId $UserGroupOwnerUpn -ErrorAction Stop | |
| $Me = Get-MgUser -UserId $($MgContext.Account) -ErrorAction Stop | |
| $GroupParams = @{ | |
| MailEnabled = $false | |
| SecurityEnabled = $true | |
| Description = $Note | |
| } | |
| try { | |
| $UserGroup = New-MgGroup @GroupParams -DisplayName "$Name Users" -MailNickname "$($CleanNickname)Users" | |
| $AdminGroup = New-MgGroup @GroupParams -DisplayName "$Name Admins" -MailNickname "$($CleanNickname)Admins" | |
| $userGroupOwnerOdata = @{ | |
| "@odata.id" = "https://graph.microsoft.com/v1.0/users/$($TargetOwner.Id)" | |
| } | |
| New-MgGroupOwnerByRef -GroupId $UserGroup.Id -BodyParameter $userGroupOwnerOdata | |
| $adminGroupOwnerOdata = @{ | |
| "@odata.id" = "https://graph.microsoft.com/v1.0/users/$($Me.Id)" | |
| } | |
| New-MgGroupOwnerByRef -GroupId $AdminGroup.Id -BodyParameter $adminGroupOwnerOdata | |
| Write-Verbose "[$(timestamp)] Created security groups" | |
| } | |
| catch { | |
| Write-Error "[$(timestamp)] $($_.ErrorDetails) - $($_.Exception.Message))" | |
| } | |
| Write-Verbose "[$(timestamp)] Creating app registration $Name" | |
| $AppParams = @{ | |
| DisplayName = $Name | |
| IdentifierUris = @($IdentifierUri) | |
| Web = @{ RedirectUris = @($ReplyUri) } | |
| } | |
| try { | |
| $AppReg = New-MgApplication @AppParams -ErrorAction Stop | |
| Write-Verbose "[$(timestamp)] Created app registration $Name" | |
| } | |
| catch { | |
| Write-Error "[$(timestamp)] $($_.ErrorDetails) - $($_.Exception.Message))" | |
| } | |
| Write-Verbose "[$(timestamp)] Provisioning enterprise app '$Name' with SAML SSO" | |
| $SpParams = @{ | |
| AppId = $AppReg.AppId | |
| PreferredSingleSignOnMode = "saml" | |
| AppRoleAssignmentRequired = $true | |
| Notes = $Note | |
| Tags = @("WindowsAzureActiveDirectoryCustomSingleSignOnApplication", "WindowsAzureActiveDirectoryIntegratedApp") | |
| } | |
| try { | |
| $ServicePrincipal = New-MgServicePrincipal @SpParams -ErrorAction Stop | |
| Write-Verbose "[$(timestamp)] Provisioned enterprise app '$Name' with SAML SSO" | |
| } | |
| catch { | |
| Write-Error "[$(timestamp)] $($_.ErrorDetails) - $($_.Exception.Message))" | |
| } | |
| Write-Verbose "[$(timestamp)] Assigning security groups to enterprise app" | |
| $AssignmentParams = @{ | |
| ServicePrincipalId = $ServicePrincipal.Id | |
| ResourceId = $ServicePrincipal.Id # the app itself | |
| Id = "00000000-0000-0000-0000-000000000000" # default role Id | |
| } | |
| try { | |
| New-MgServicePrincipalAppRoleAssignedTo @AssignmentParams -PrincipalId $UserGroup.Id | Out-Null | |
| New-MgServicePrincipalAppRoleAssignedTo @AssignmentParams -PrincipalId $AdminGroup.Id | Out-Null | |
| Write-Verbose "[$(timestamp)] Assigned security groups to enterprise app" | |
| } | |
| catch { | |
| Write-Error "[$(timestamp)] $($_.ErrorDetails) - $($_.Exception.Message))" | |
| } | |
| Write-Verbose "[$(timestamp)] Done processing for $($Name)" | |
| # output: emit a PSCustomObject | |
| [PSCustomObject]@{ | |
| ApplicationId = $AppReg.AppId | |
| ApplicationName = $Name | |
| SamlIdentifier = $IdentifierUri | |
| SamlReplyUri = $ReplyUri | |
| SamlMetadataUrl = "https://login.microsoftonline.com/$($MgContext.TenantId)/federationmetadata/2007-06/federationmetadata.xml?appid=$($AppReg.AppId)" | |
| UserGroupObjectId = $UserGroup.Id | |
| UserGroupDisplayName = $UserGroup.DisplayName | |
| AdminGroupObjectId = $AdminGroup.Id | |
| AdminGroupDisplayName = $AdminGroup.DisplayName | |
| } | |
| } | |
| catch { | |
| Write-Error "[$(timestamp)] $($_.ErrorDetails) - $($_.Exception.Message))" | |
| } | |
| } | |
| end { | |
| Write-Verbose "[$(timestamp)] Deployment cycle complete." | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment