How to create a service principal name for Azure Stack Hub integrated with Active Directory Federation Services identity using PowerShell

Overview

This article explains how to create a service principal name (SPN) to manage Azure Stack Hub integrated with Active Directory Federation Services (AD FS) identity using PowerShell.

For more information about this process, visit Give an app access to Azure Stack Hub resources.

It will guide you through the creation of:

  • An AD FS application and the associated service principal object which represents the application’s identity within the Active Directory

  • Role assignment

Prerequisites

Prerequisites from a Windows-based external client are:

Overview of the creation process for Azure Stack Hub SPN

  1. Declare your variables accordingly.

  2. Log in to your Azure Stack Hub Default Provider Subscription with administrator user credentials (needs to have the Owner role).

  3. Create your AD FS application/service principal.

  4. Assign the appropriate Role to your service principal.

  1. Log in to your Azure Stack Hub Default Provider Subscription using the SPN account.

  2. Verify SPN authentication and the role assignment.

Create Azure Stack Hub SPN

Create a PFX Certificate

#region Declare variables

$CertificateName = "ADFSAutomationCert"

$CertStore = "cert:\LocalMachine\My" # This can also be "cert:\CurrentUser\My" but in general service accounts cannot access CurrentUser cert store

$CertSubject = "CN=$CertificateName"

$PfxFilePath = "C:\Temp"

if (-not (Test-Path -Path $PfxFilePath)) {

    New-Item -ItemType Directory -Path $PfxFilePath -Force | Out-Null

}

$PfxFilePathFull = Join-Path -Path $PfxFilePath -ChildPath "$($CertificateName).pfx"

$PfxPassword = '""' | ConvertTo-SecureString -AsPlainText -Force # replace "" with an actual password or leave "" for it to be blank

#endregion

#region Create certificate to pass into new Application

$ExpiryDate = (Get-Date).AddDays(365) # You can change this to whatever fits your security profile better, default is 1 year

$Cert = New-SelfSignedCertificate -CertStoreLocation $CertStore -Subject $CertSubject -KeySpec KeyExchange -NotAfter $ExpiryDate

Write-Verbose -Message "Certificate ""$($Cert.Subject)"" with start date ""$($Cert.NotBefore)"" and end date ""$($Cert.NotAfter)"" created at ""$($PfxFilePathFull)""."

#endregion

#region Get a cert object from a .pfx file - you need it to create the SPN to begin with

$Cert = Get-PfxCertificate -FilePath $PfxFilePathFull -Password $PfxPassword

#endregion

#region Optional steps

#region Export the certificate so that you can import it on other environments

try {

    Export-PfxCertificate -Cert $Cert.PsPath -FilePath $PfxFilePathFull -Password $PfxPassword -ErrorAction Stop | Out-Null

} catch {

    throw "Failed to export certificate to ""$($PfxFilePathFull)"":`n$($_.Exception.Message)"

}

#endregion

#region Import the certificate into the certificate store on another environment

Import-PfxCertificate -CertStoreLocation $CertStore -FilePath $PfxFilePathFull -Password $PfxPassword -Exportable

#endregion

#endregion

Create Azure Stack Hub SPN that uses certificate credential

#region Declare variables

$CertificateName = "ADFSAutomationCert"

$PfxFilePath = "C:\Temp"

$PfxFilePathFull = Join-Path -Path $PfxFilePath -ChildPath "$($CertificateName).pfx"

$PfxPassword = '""' | ConvertTo-SecureString -AsPlainText -Force

$CertificateObject = Get-PfxCertificate -FilePath $PfxFilePathFull -Password $PfxPassword

$CertificateThumbprint = $CertificateObject.Thumbprint

if (!$CertificateThumbprint) {

    throw "Failed to obtain a thumbprint from certificate: $($PfxFilePathFull)"

}

$CloudAdminUsername = "CloudAdmin@azurestack.local"

[SecureString]$CloudAdminPassword = ConvertTo-SecureString "Password123!" -AsPlainText -Force

$ApplicationName = "ADFSAppCert"

$AzureStackRole = "Owner"

$ADGroupName = "AzureStackHubOwners"

$AzureStackAdminArmEndpoint = "https://adminmanagement.local.azurestack.external/"

$EnvironmentName = "AzureStackAdmin"

$PepCreds = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $CloudAdminUsername, $CloudAdminPassword

$PepIPAddress = "x.x.x.224" # e.g. 10.5.30.224

#endregion

#region Register and set an Az environment that targets your Azure Stack Hub instance

Write-Output -InputObject "Connecting to Azure Stack Hub Admin Management Endpoint - $(AzureStackAdminArmEndpoint)"

$null = Add-AzEnvironment -Name $EnvironmentName -ARMEndpoint $AzureStackAdminArmEndpoint

$null = Connect-AzAccount -Environment $EnvironmentName -UseDeviceAuthentication # Interactive prompt

if (((Get-AzContext).Subscription).Name -notlike "Default Provider Subscription") {

    throw "Failed to obtain access to the 'Default Provider Subscription'. Please verify the user has been assigned the '$($AzureStackRole)' role for the 'Default Provider Subscription'."

}

#endregion

#region Create a PSSession to the Privileged Endpoint VM

Write-Output -InputObject "Create a PowerShell Session to the Privileged Endpoint VM"

$PepSession = New-PSSession -ComputerName $PepIPAddress -ConfigurationName PrivilegedEndpoint -Credential $PepCreds -SessionOption (New-PSSessionOption -Culture en-US -UICulture en-US)

#endregion

#region Check for existing SPN

Write-Output -InputObject "Check for existing SPN '$($ApplicationName)'"

$SPNObjectCheckJob = Invoke-Command -Session $PepSession -ScriptBlock { Get-GraphApplication } -AsJob | Wait-Job

if ($SPNObjectCheckJob.State -ne "Completed") {

    throw "$($SPNObjectCheckJob.ChildJobs | Receive-Job)"

}

$SPNObjectCheck = $SPNObjectCheckJob.ChildJobs.Output | Where-Object { $_.Name -like "Azurestack-$ApplicationName*" } | Select-Object -Last 1

#endregion

#region Create new SPN if one does not exist

if ($SPNObjectCheck) {

    Write-Output -InputObject "SPN details`n$($ApplicationName): $($SPNObjectCheck | Out-String)"

} else {

    Write-Output -InputObject "No existing SPN found"

    Write-Output -InputObject "Create new SPN '$($ApplicationName)'"

    $SPNObjectJob = Invoke-Command -Session $PepSession -ScriptBlock { New-GraphApplication -Name $using:ApplicationName -ClientCertificates $using:CertificateObject } -AsJob | Wait-Job

    if ($SPNObjectJob.State -ne "Completed") {

        throw "$($SPNObjectJob.ChildJobs | Receive-Job)"

    }

    $SPNObject = $SPNObjectJob.ChildJobs.Output

    Write-Output -InputObject "SPN details`n$($ApplicationName): $($SPNObject | Out-String)"

    $FullApplicationName = $SPNObject.ApplicationName

    #endregion

}

#region Assign SPN the 'Owner' role for the 'Default Provider Subscription'

Write-Output -InputObject "Assign SPN '$($ApplicationName)' the '$($AzureStackRole)' role for the 'Default Provider Subscription'"

if ($FullApplicationName) {

    $SPNADFSApp = Get-AzADServicePrincipal | Where-Object { $_.DisplayName -like "$($FullApplicationName)" }

} else {

    $SPNADFSApp = Get-AzADServicePrincipal | Where-Object { $_.DisplayName -like "*$($ApplicationName)*" } | Select-Object -Last 1

}

$SPNRoleAssignmentCheck = Get-AzRoleAssignment -ObjectId $SPNADFSApp.AdfsId

if (!($SPNRoleAssignmentCheck) -or ($SPNRoleAssignmentCheck.RoleDefinitionName -ne $AzureStackRole)) {

    $null = New-AzRoleAssignment -RoleDefinitionName $AzureStackRole -ServicePrincipalName $SPNADFSApp.ApplicationId.Guid

    #region Verify SPN has been assigned the 'Owner' role for the 'Default Provider Subscription'

    $SPNRoleAssignment = Get-AzRoleAssignment -ObjectId $SPNADFSApp.AdfsId

    if (!($SPNRoleAssignment) -or ($SPNRoleAssignment.RoleDefinitionName -ne $AzureStackRole)) {

        throw "Failed to assign SPN '$($ApplicationName)' the '$($AzureStackRole)' role for the Default Provider Subscription"

    }

    #endregion

}

#endregion

#region Assign AD group 'AzureStackOwners' the 'Owner' role for the 'Default Provider Subscription'

Write-Output -InputObject "Assign AD group '$($ADGroupName)' the '$($AzureStackRole)' role for the 'Default Provider Subscription'"

$ADGroup = Get-AzADGroup -DisplayNameStartsWith $ADGroupName

$SubId = (Get-AzSubscription -SubscriptionName "Default Provider Subscription").Id

$OwnerRoleId = (Get-AzRoleDefinition -Name $AzureStackRole).Id

$APIPayloadHash = @{

    "properties" = @{

        "roleDefinitionId" = "/subscriptions/$($SubId)/providers/Microsoft.Authorization/roleDefinitions/$($OwnerRoleId)"
        "principalId"      = "$($ADGroup.AdfsId)"

    }

} | ConvertTo-Json -Depth 50

$APIPath = "/subscriptions/$($SubId)/providers/Microsoft.Authorization/roleAssignments/$($OwnerRoleId)?api-version=2015-07-01"

$APIResponse = Invoke-AzRestMethod -Path $APIPath -Method "PUT" -Payload $APIPayloadHash

if ($APIResponse.StatusCode -ne "201") {

    throw "Failed to create role assignment for ""$($ADGroup.DisplayName)"" in subscription ""$($SubId)"" with role ""$($AzureStackRole)"" and role ID ""$($OwnerRoleId)"""

}

#endregion

#region Verify AD group 'AzureStackOwners' has been assigned the 'Owner' role for the 'Default Provider Subscription'

$ADGroupRoleAssignment = Get-AzRoleAssignment -ObjectId $ADGroup.AdfsId

if (!($ADGroupRoleAssignment) -or ($ADGroupRoleAssignment.RoleDefinitionName -ne $AzureStackRole)) {

    throw "Failed to assign AD group '$($ADGroupName)' the '$($AzureStackRole)' role for the 'Default Provider Subscription'"

}

#endregion

#region Obtain authentication information

# GUID of the directory tenant
$TenantId = (Get-AzContext).Tenant.Id

Write-Output -InputObject "TenantId: $($TenantId)"
Write-Output -InputObject ""
Write-Output -InputObject "ApplicationName: $($SPNADFSApp.DisplayName)"
Write-Output -InputObject ""
Write-Output -InputObject "ApplicationId: $($SPNADFSApp.ApplicationId.Guid)"
Write-Output -InputObject ""
Write-Output -InputObject "CertificateThumbprint: $($CertificateThumbprint)"
Write-Output -InputObject ""
Write-Output -InputObject "Admin ARM Endpoint: $($AzureStackAdminArmEndpoint)"

#endregion

#region Verify if SPN can authenticate to Azure Stack Hub Admin Management Endpoint

Write-Output -InputObject "Verify if SPN can authenticate to Azure Stack Hub Admin Management Endpoint"

$null = Clear-AzContext -Force

$null = Connect-AzAccount -Environment $EnvironmentName -ServicePrincipal -Tenant $TenantId -ApplicationId $SPNADFSApp.ApplicationId.Guid -CertificateThumbprint $CertificateThumbprint

if (((Get-AzContext).Subscription).Name -notlike "Default Provider Subscription") {

    throw "Failed to obtain access to the 'Default Provider Subscription'. Please verify the SPN has been assigned the '$($AzureStackRole)' role for the 'Default Provider Subscription'."

} else {

    Write-Output -InputObject "Your SPN can successfully authenticate with ARM Endpoint $($AzureStackAdminArmEndpoint) and has got access to the 'Default Provider Subscription'"

}

#endregion

#region Remove sessions

if ($PepSession) {

    Write-Output -InputObject "Removing PSSSession to the Privileged Endpoint"

    Remove-PSSession -Session $PepSession

}

$CheckContext = Get-AzContext | Where-Object { $_.Environment -like $EnvironmentName }

if ($CheckContext) {

    Write-Output -InputObject "Disconnecting from AzS Hub Admin Management Endpoint: $($CheckContext.Environment.ResourceManagerUrl)"

    $null = Disconnect-AzAccount

}

#endregion

#region Declare variables

$CloudAdminUsername = "CloudAdmin@azurestack.local"

[SecureString]$CloudAdminPassword = ConvertTo-SecureString "Password123!" -AsPlainText -Force

$ApplicationName = "ADFSAppCert"

$AzureStackRole = "Owner"

$ADGroupName = "AzureStackHubOwners"

$AzureStackAdminArmEndpoint = "https://adminmanagement.local.azurestack.external/"

$EnvironmentName = "AzureStackAdmin"

$PepCreds = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $CloudAdminUsername, $CloudAdminPassword

$PepIPAddress = "x.x.x.224" # e.g. 10.5.30.224

#endregion

#region Register and set an Az environment that targets your Azure Stack Hub instance

Write-Output -InputObject "Connecting to Azure Stack Hub Admin Management Endpoint - $(AzureStackAdminArmEndpoint)"

$null = Add-AzEnvironment -Name $EnvironmentName -ARMEndpoint $AzureStackAdminArmEndpoint

$null = Connect-AzAccount -Environment $EnvironmentName -UseDeviceAuthentication # Interactive prompt

if (((Get-AzContext).Subscription).Name -notlike "Default Provider Subscription") {

    throw "Failed to obtain access to the 'Default Provider Subscription'. Please verify the user has been assigned the '$($AzureStackRole)' role for the 'Default Provider Subscription'."

}

#endregion

#region Create a PSSession to the Privileged Endpoint VM

Write-Output -InputObject "Create a PowerShell Session to the Privileged Endpoint VM"

$PepSession = New-PSSession -ComputerName $PepIPAddress -ConfigurationName PrivilegedEndpoint -Credential $PepCreds -SessionOption (New-PSSessionOption -Culture en-US -UICulture en-US)

#endregion

#region Check for existing SPN

Write-Output -InputObject "Check for existing SPN '$($ApplicationName)'"

$SPNObjectCheckJob = Invoke-Command -Session $PepSession -ScriptBlock { Get-GraphApplication } -AsJob | Wait-Job

if ($SPNObjectCheckJob.State -ne "Completed") {

    throw "$($SPNObjectCheckJob.ChildJobs | Receive-Job)"

}

$SPNObjectCheck = $SPNObjectCheckJob.ChildJobs.Output | Where-Object { $_.Name -like "Azurestack-$ApplicationName*" } | Select-Object -Last 1

#endregion

#region Create new SPN if one does not exist

if ($SPNObjectCheck) {

    Write-Output -InputObject "SPN details`n$($ApplicationName): $($SPNObjectCheck | Out-String)"

} else {

    Write-Output -InputObject "No existing SPN found"

    Write-Output -InputObject "Create new SPN '$($ApplicationName)'"

    $SPNObjectJob = Invoke-Command -Session $PepSession -ScriptBlock { New-GraphApplication -Name $using:ApplicationName -GenerateClientSecret } -AsJob | Wait-Job

    if ($SPNObjectJob.State -ne "Completed") {

        throw "$($SPNObjectJob.ChildJobs | Receive-Job)"

    }

    $SPNObject = $SPNObjectJob.ChildJobs.Output

    Write-Output -InputObject "SPN details`n$($ApplicationName): $($SPNObject | Out-String)"

    $FullApplicationName = $SPNObject.ApplicationName

    $SPNClientId = $SPNObject.ClientId

    $SPNClientSecret = $SPNObject.ClientSecret | ConvertTo-SecureString -AsPlainText -Force

    $SPNCreds = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $SPNClientId, $SPNClientSecret

    #endregion

}

#region Assign SPN the 'Owner' role for the 'Default Provider Subscription'

Write-Output -InputObject "Assign SPN '$($ApplicationName)' the '$($AzureStackRole)' role for the 'Default Provider Subscription'"

if ($FullApplicationName) {

    $SPNADFSApp = Get-AzADServicePrincipal | Where-Object { $_.DisplayName -like "$($FullApplicationName)" }

} else {

    $SPNADFSApp = Get-AzADServicePrincipal | Where-Object { $_.DisplayName -like "*$($ApplicationName)*" } | Select-Object -Last 1

}

$SPNRoleAssignmentCheck = Get-AzRoleAssignment -ObjectId $SPNADFSApp.AdfsId

if (!($SPNRoleAssignmentCheck) -or ($SPNRoleAssignmentCheck.RoleDefinitionName -ne $AzureStackRole)) {

    $null = New-AzRoleAssignment -RoleDefinitionName $AzureStackRole -ServicePrincipalName $SPNADFSApp.ApplicationId.Guid

    #region Verify SPN has been assigned the 'Owner' role for the 'Default Provider Subscription'

    $SPNRoleAssignment = Get-AzRoleAssignment -ObjectId $SPNADFSApp.AdfsId

    if (!($SPNRoleAssignment) -or ($SPNRoleAssignment.RoleDefinitionName -ne $AzureStackRole)) {

        throw "Failed to assign SPN '$($ApplicationName)' the '$($AzureStackRole)' role for the Default Provider Subscription"

    }

    #endregion

}

#endregion

#region Assign AD group 'AzureStackOwners' the 'Owner' role for the 'Default Provider Subscription'

Write-Output -InputObject "Assign AD group '$($ADGroupName)' the '$($AzureStackRole)' role for the 'Default Provider Subscription'"

$ADGroup = Get-AzADGroup -DisplayNameStartsWith $ADGroupName

$SubId = (Get-AzSubscription -SubscriptionName "Default Provider Subscription").Id

$OwnerRoleId = (Get-AzRoleDefinition -Name $AzureStackRole).Id

$APIPayloadHash = @{

    "properties" = @{

        "roleDefinitionId" = "/subscriptions/$($SubId)/providers/Microsoft.Authorization/roleDefinitions/$($OwnerRoleId)"
        "principalId"      = "$($ADGroup.AdfsId)"

    }

} | ConvertTo-Json -Depth 50

$APIPath = "/subscriptions/$($SubId)/providers/Microsoft.Authorization/roleAssignments/$($OwnerRoleId)?api-version=2015-07-01"

$APIResponse = Invoke-AzRestMethod -Path $APIPath -Method "PUT" -Payload $APIPayloadHash

if ($APIResponse.StatusCode -ne "201") {

    throw "Failed to create role assignment for ""$($ADGroup.DisplayName)"" in subscription ""$($SubId)"" with role ""$($AzureStackRole)"" and role ID ""$($OwnerRoleId)"""

}

#endregion

#region Verify AD group 'AzureStackOwners' has been assigned the 'Owner' role for the 'Default Provider Subscription'

$ADGroupRoleAssignment = Get-AzRoleAssignment -ObjectId $ADGroup.AdfsId

if (!($ADGroupRoleAssignment) -or ($ADGroupRoleAssignment.RoleDefinitionName -ne $AzureStackRole)) {

    throw "Failed to assign AD group '$($ADGroupName)' the '$($AzureStackRole)' role for the 'Default Provider Subscription'"

}

#endregion

#region Obtain authentication information

# GUID of the directory tenant
$TenantId = (Get-AzContext).Tenant.Id

Write-Output -InputObject "TenantId: $($TenantId)"
Write-Output -InputObject ""
Write-Output -InputObject "ApplicationName: $($SPNADFSApp.DisplayName)"
Write-Output -InputObject ""
Write-Output -InputObject "ApplicationId: $($SPNADFSApp.ApplicationId.Guid)"
Write-Output -InputObject ""
Write-Output -InputObject "ClientSecret: $($SPNObject.ClientSecret)"
Write-Output -InputObject ""
Write-Output -InputObject "Admin ARM Endpoint: $($AzureStackAdminArmEndpoint)"

#endregion

#region Verify if SPN can authenticate to Azure Stack Hub Admin Management Endpoint

Write-Output -InputObject "Verify if SPN can authenticate to Azure Stack Hub Admin Management Endpoint"

$null = Clear-AzContext -Force

$null = Connect-AzAccount -Environment $EnvironmentName -ServicePrincipal -Tenant $TenantId -Credential $SPNCreds

if (((Get-AzContext).Subscription).Name -notlike "Default Provider Subscription") {

    throw "Failed to obtain access to the 'Default Provider Subscription'. Please verify the SPN has been assigned the '$($AzureStackRole)' role for the 'Default Provider Subscription'."

} else {

    Write-Output -InputObject "Your SPN can successfully authenticate with ARM Endpoint $($AzureStackAdminArmEndpoint) and has got access to the 'Default Provider Subscription'"

}

#endregion

#region Remove sessions

if ($PepSession) {

    Write-Output -InputObject "Removing PSSSession to the Privileged Endpoint"

    Remove-PSSession -Session $PepSession

}

$CheckContext = Get-AzContext | Where-Object { $_.Environment -like $EnvironmentName }

if ($CheckContext) {

    Write-Output -InputObject "Disconnecting from AzS Hub Admin Management Endpoint: $($CheckContext.Environment.ResourceManagerUrl)"

    $null = Disconnect-AzAccount

}

#endregion


Last modified March 29, 2023: Pub/minor adfs spn typo (#21) (4299b2ea)