@@ -3,49 +3,103 @@ function Add-CIPPSSOAppSecret {
33 . SYNOPSIS
44 Creates a client secret on the CIPP-SSO app registration with retry.
55 . DESCRIPTION
6- Adds a new password credential to the given app object via Graph. Retries up to
7- MaxRetries times with backoff because Entra propagation can take a few seconds
8- after the app is freshly created or its app-management-policy exemption is set.
9- Throws on final failure so callers can persist Status=error + LastError.
6+ Adds a new password credential to the given app object via Graph. Before adding the
7+ secret it ensures the app is exempt from the tenant default app-management policy (so a
8+ 'passwordAddition' restriction can't block the secret) via Update-AppManagementPolicy,
9+ and honours any 'passwordLifetime' restriction when building the credential body.
10+ Retries up to MaxRetries times with backoff because Entra propagation can take a few
11+ seconds after the app is freshly created or its app-management-policy exemption is set:
12+ replication misses back off 3s, and credential-policy blocks back off min(30, 5*attempt)s
13+ while the exemption propagates. Throws on final failure so callers can persist
14+ Status=error + LastError.
1015 . PARAMETER ObjectId
1116 Graph object ID of the application (NOT the appId/clientId).
17+ . PARAMETER AppId
18+ AppId/clientId of the application, used to target the app-management-policy exemption.
19+ Resolved from ObjectId when not supplied.
1220 . PARAMETER DisplayName
1321 Display name to set on the password credential. Defaults to 'CIPP-SSO-Secret'.
1422 . PARAMETER MaxRetries
15- Number of secret-creation attempts before giving up. Defaults to 5 .
23+ Number of secret-creation attempts before giving up. Defaults to 6 .
1624 #>
1725 [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute (' PSAvoidUsingConvertToSecureStringWithPlainText' , ' ' )]
1826 [CmdletBinding ()]
1927 param (
2028 [Parameter (Mandatory = $true )]
2129 [string ]$ObjectId ,
2230
31+ [Parameter (Mandatory = $false )]
32+ [string ]$AppId ,
33+
2334 [Parameter (Mandatory = $false )]
2435 [string ]$DisplayName = ' CIPP-SSO-Secret' ,
2536
2637 [Parameter (Mandatory = $false )]
27- [int ]$MaxRetries = 5
38+ [int ]$MaxRetries = 6
2839 )
2940
41+ # Update-AppManagementPolicy targets the app by appId/clientId; resolve it from the object id when not supplied.
42+ if (-not $AppId ) {
43+ try {
44+ $SSOApp = New-GraphGetRequest - uri " https://graph.microsoft.com/v1.0/applications/$ObjectId `?`$ select=id,appId" - NoAuthCheck $true - AsApp $true
45+ $AppId = $SSOApp.appId
46+ } catch {
47+ Write-Warning " [SSO-Secret] Failed to resolve appId for objectId $ObjectId : $ ( $_.Exception.Message ) "
48+ }
49+ }
50+
51+ # Ensure the app is exempt from any credential-addition restriction before adding the secret.
52+ if ($AppId ) {
53+ try {
54+ $PolicyUpdate = Update-AppManagementPolicy - ApplicationId $AppId
55+ Write-Information " [SSO-Secret] App management policy: $ ( $PolicyUpdate.PolicyAction ) "
56+ } catch {
57+ Write-Information " [SSO-Secret] Failed to update app management policy: $ ( $_.Exception.Message ) "
58+ }
59+ }
60+
61+ # Honour the tenant password-lifetime restriction (if enforced) when building the credential body.
62+ $AppManagementPolicy = New-GraphGetRequest - uri ' https://graph.microsoft.com/v1.0/policies/defaultAppManagementPolicy' - AsApp $true - NoAuthCheck $true
63+ $PasswordExpirationPolicy = $AppManagementPolicy.applicationRestrictions.passwordcredentials |
64+ Where-Object { $_.restrictionType -eq ' passwordLifetime' }
65+ if (-not ($PasswordExpirationPolicy.state -eq ' disabled' -or $null -eq $PasswordExpirationPolicy.state )) {
66+ $TimeToExpiration = [System.Xml.XmlConvert ]::ToTimeSpan($PasswordExpirationPolicy.maxLifetime )
67+ $ExpirationDate = (Get-Date ).AddDays($TimeToExpiration.Days ).ToUniversalTime().ToString(' yyyy-MM-ddTHH:mm:ss.fffZ' )
68+ $PasswordBody = " {`" passwordCredential`" :{`" displayName`" :`" $DisplayName `" ,`" endDateTime`" :`" $ExpirationDate `" }}"
69+ } else {
70+ $PasswordBody = " {`" passwordCredential`" :{`" displayName`" :`" $DisplayName `" }}"
71+ }
72+
3073 $SecretText = $null
31- $SecretAttempt = 0
32- $BackoffSchedule = @ (2 , 5 , 10 , 15 , 30 )
3374 $LastException = $null
34-
35- while ($SecretAttempt -lt $MaxRetries -and -not $SecretText ) {
75+ for ($Attempt = 1 ; $Attempt -le $MaxRetries ; $Attempt ++ ) {
3676 try {
37- $PasswordBody = @ { passwordCredential = @ { displayName = $DisplayName } } | ConvertTo-Json - Compress
38- $PasswordResult = New-GraphPOSTRequest - uri " https://graph.microsoft.com/v1.0/applications/$ObjectId /addPassword" - body $PasswordBody - type POST - NoAuthCheck $true - AsApp $true
77+ $PasswordResult = New-GraphPOSTRequest - uri " https://graph.microsoft.com/v1.0/applications/$ObjectId /addPassword" - AsApp $true - NoAuthCheck $true - type POST - body $PasswordBody - maxRetries 3
3978 $SecretText = $PasswordResult.secretText
4079 Write-Information " [SSO-Secret] Client secret created on objectId $ObjectId "
80+ break
4181 } catch {
42- $SecretAttempt ++
4382 $LastException = $_
44- Write-Warning " [SSO-Secret] Secret creation attempt $SecretAttempt /$MaxRetries failed: $ ( $_.Exception.Message ) "
45- if ($SecretAttempt -lt $MaxRetries ) {
46- $Delay = $BackoffSchedule [[Math ]::Min ($SecretAttempt - 1 , $BackoffSchedule.Count - 1 )]
47- Start-Sleep - Seconds $Delay
83+ $ExceptionMessage = $_.Exception.Message
84+ $IsNotReplicatedYet = $ExceptionMessage -match " Resource '.*' does not exist or one of its queried reference-property objects are not present"
85+ $IsCredentialPolicyBlocked = $ExceptionMessage -match ' Credential type not allowed as per assigned policy'
86+ Write-Warning " [SSO-Secret] Secret creation attempt $Attempt /$MaxRetries failed: $ExceptionMessage "
87+
88+ if ($IsNotReplicatedYet -and $Attempt -lt $MaxRetries ) {
89+ $DelaySeconds = 3
90+ Write-Information " [SSO-Secret] Application object not yet replicated for addPassword (attempt $Attempt of $MaxRetries ). Retrying in $DelaySeconds second(s)."
91+ Start-Sleep - Seconds $DelaySeconds
92+ continue
93+ }
94+
95+ if ($IsCredentialPolicyBlocked -and $Attempt -lt $MaxRetries ) {
96+ $DelaySeconds = [Math ]::Min(30 , 5 * $Attempt )
97+ Write-Information " [SSO-Secret] Credential policy still blocks addPassword (attempt $Attempt of $MaxRetries ). Waiting for policy propagation and retrying in $DelaySeconds second(s)."
98+ Start-Sleep - Seconds $DelaySeconds
99+ continue
48100 }
101+
102+ throw
49103 }
50104 }
51105
0 commit comments