ADFS.ps1
# Decrypt ADFS RefreshToken # Oct 28th 2021 function Unprotect-ADFSRefreshToken { <# .SYNOPSIS Decrypts and verifies the given AD FS generated Refresh Token with the given certificates. .DESCRIPTION Decrypts and verifies the given AD FS generated Refresh Token with the given certificates. .PARAMETER RefreshToken AD FS generated RefreshToken. .PARAMETER PfxFileName_encryption Name of the PFX file of token encryption certificate. .PARAMETER PfxPassword_encryption Password of the token encryption PFX file. Optional. .PARAMETER PfxFileName_signing Name of the PFX file of token signing certificate. Optional. If not provided, refresh token is not verified. .PARAMETER PfxPassword_signing Password of the token signing PFX file. Optional. .Example PS C:\>Unprotect-ADFSRefreshToken -RefreshToken $token -PfxFileName_encryption .\ADFS_encryption.pfx -PfxFileName_signing .\ADFS_signing.pfx ClientID : 5846ec9c-1cd7-4040-8630-6ae82d6cdfd3 RedirectUri : Resource : urn:microsoft:userinfo Issuer : http://sts.company.com/adfs/services/trust NotBefore : 1635414030 ExpiresOn : 1635442830 SingleSignOnToken : {"TokenType":0,"StringToken":"vVV[redacted]W/gE=","Version":1} DeviceFlowDeviceId : IsDeviceFlow : False SessionKeyString : SSOToken : <SessionToken>[redacted]</SessionToken> #> [cmdletbinding()] Param( [Parameter(Mandatory=$True,ValueFromPipeLine)] [String]$RefreshToken, [Parameter(Mandatory=$True)] [string]$PfxFileName_encryption, [Parameter(Mandatory=$False)] [string]$PfxPassword_encryption, [Parameter(Mandatory=$False)] [string]$PfxFileName_signing, [Parameter(Mandatory=$False)] [string]$PfxPassword_signing ) Begin { $certificate_encryption = Load-Certificate -FileName $PfxFileName_encryption -Password $PfxPassword_encryption -Exportable [System.Security.Cryptography.RSACryptoServiceProvider]$privateKey_encryption = Load-PrivateKey -Certificate $certificate_encryption if($PfxFileName_signing) { $certificate_signing = Load-Certificate -FileName $PfxFileName_signing -Password $PfxPassword_signing -Exportable } } Process { # Separate token and signature $tokenParts = $RefreshToken.Split(".") $enc_refresh_token = (Convert-B64ToByteArray -B64 $tokenParts[0]) $signature = (Convert-B64ToByteArray -B64 $tokenParts[1]) # Verify the signature if the signing certificate provided if($certificate_signing) { $valid = $certificate_signing.PublicKey.Key.VerifyData($enc_refresh_token,"SHA256",$signature) if(!$valid) { Write-Warning "Invalid signature or signing certificate!" } Write-Verbose "Refresh token signature validated." } # Get the refresh token components $p = 0 $hash = $enc_refresh_token[$p..($p+32-1)] ; $p+=32 $enc_Key_IV_len = [bitconverter]::ToUInt32($enc_refresh_token[$p..($p+3)],0); $p+=4 $enc_Key_IV = $enc_refresh_token[($p)..($p + $enc_Key_IV_len -1)]; $p+= $enc_Key_IV_len $enc_token_len = [bitconverter]::ToUInt32($enc_refresh_token[$p..($p+3)],0); $p+=4 $enc_token = $enc_refresh_token[($p)..($p + $enc_token_len -1)] # Compare the hash $sha256 = [System.Security.Cryptography.SHA256]::Create() $comp_hash = $sha256.ComputeHash([text.encoding]::UTF8.GetBytes($privateKey_encryption.ToXmlString($false))) if(Compare-Object -ReferenceObject $hash -DifferenceObject $comp_hash -SyncWindow 0) { Write-Error "Invalid decryption certificate (hash doesn't match)." return } Write-Verbose "Decryption key hash validated." # Decrypt Key and IV $dec_Key_IV = $privateKey_encryption.Decrypt($enc_Key_IV, $True) $dec_Key = $dec_Key_IV[ 0..31] $dec_IV = $dec_Key_IV[32..48] # Decrypt the refresh token $Crypto = [System.Security.Cryptography.RijndaelManaged]::Create() $Crypto.Mode = "CBC" $Crypto.Padding = "PKCS7" $Crypto.Key = $dec_Key $Crypto.IV = $dec_IV $decryptor = $Crypto.CreateDecryptor() $ms = New-Object System.IO.MemoryStream $cs = New-Object System.Security.Cryptography.CryptoStream($ms,$decryptor,[System.Security.Cryptography.CryptoStreamMode]::Write) $cs.Write($enc_token,0,$enc_token.Count) $cs.Close() $cs.Dispose() $dec_refresh_token = [text.encoding]::UTF8.GetString($ms.ToArray()) $ms.Close() $ms.Dispose() # Convert from json $refresh_token = $dec_refresh_token | ConvertFrom-Json # Get the deflated SSOToken [byte[]]$def_SSOToken = Convert-B64ToByteArray(($refresh_token.SingleSignOnToken | ConvertFrom-Json).StringToken) # Get the binary xml SSOToken $bxml_SSOToken = Get-DeDeflatedByteArray -byteArray $def_SSOToken # Get the xml SSOTOken $xml_SSOToken = BinaryToXml -xml_bytes $bxml_SSOToken -Dictionary (Get-XmlDictionary -type Session) # Set the SSOToken and return $refresh_token | Add-Member -NotePropertyName "SSOToken" -NotePropertyValue $xml_SSOToken.outerxml $refresh_token } End { Unload-PrivateKey -PrivateKey $privateKey_encryption } } # Create a new ADFS RefreshToken # Oct 28th 2021 function New-ADFSRefreshToken { <# .SYNOPSIS Creates a new AD FS Refresh Token with the given certificate. .DESCRIPTION Creates a new AD FS Refresh Token with the given certificate. .PARAMETER NotBefore The time after the refresh token is valid. Defaults to current time. .PARAMETER ExpiresOn The time when the refresh token is invalidated. Defaults to 8 hours from the current time. .PARAMETER UserPrincipalName UserPrincipalName of the user. .PARAMETER Name DisplayName of the user. Optional. .PARAMETER ClientID GUID of the client id. The client MUST be configured in the target AD FS server. .PARAMETER Resource The resource (uri) of the refresh token. .PARAMETER Issuer The uri of the issuing party .PARAMETER RedirectUri The redirect uri. Optional. .PARAMETER PfxFileName_encryption Name of the PFX file of token encryption certificate. .PARAMETER PfxPassword_encryption Password of the token encryption PFX file. Optional. .PARAMETER PfxFileName_signing Name of the PFX file of token signing certificate. .PARAMETER PfxPassword_signing Password of the token signing PFX file. Optional. .Example $refresh_token = New-AADIntADFSRefreshToken -UserPrincipalName "user@company.com" -Resource "urn:microsoft:userinfo" -Issuer "http://sts.company.com/adfs/services/trust" -PfxFileName_encryption .\ADFS_encryption.pfx -PfxFileName_signing .\ADFS_signing.pfx -ClientID "5846ec9c-1cd7-4040-8630-6ae82d6cdfd3" $body=@{ "client_id" = "5846ec9c-1cd7-4040-8630-6ae82d6cdfd3" "refresh_token" = $refresh_token "grant_type" = "refresh_token" } $response = Invoke-RestMethod -UseBasicParsing -Uri "https://sts.company.com/adfs/services/trust/adfs/oauth2/token" -Method Post -Body $body $access_token = $response.access_token #> [cmdletbinding()] Param( [Parameter(Mandatory=$False)] [DateTime]$NotBefore = (Get-Date), [Parameter(Mandatory=$False)] [DateTime]$ExpiresOn = ((Get-Date).AddHours(8)), [Parameter(Mandatory=$True)] [String]$UserPrincipalName, [Parameter(Mandatory=$False)] [String]$Name, [Parameter(Mandatory=$True)] [guid]$ClientID, [Parameter(Mandatory=$True)] [String]$Resource, [Parameter(Mandatory=$True)] [String]$Issuer, [Parameter(Mandatory=$False)] [String]$RedirectUri, [Parameter(Mandatory=$True)] [String]$PfxFileName_encryption, [Parameter(Mandatory=$False)] [String]$PfxPassword_encryption, [Parameter(Mandatory=$True)] [String]$PfxFileName_signing, [Parameter(Mandatory=$False)] [String]$PfxPassword_signing ) Begin { $certificate_encryption = Load-Certificate -FileName $PfxFileName_encryption -Password $PfxPassword_encryption $certificate_signing = Load-Certificate -FileName $PfxFileName_signing -Password $PfxPassword_signing -Exportable $privateKey_signing = Load-PrivateKey -Certificate $certificate_signing } Process { # Generate Session Token $Key = Get-RandomBytes -Bytes 16 [xml]$xml_SessionToken =@" <SessionToken> <Version>1</Version> <SecureConversationVersion>http://docs.oasis-open.org/ws-sx/ws-secureconversation/200512</SecureConversationVersion> <Id>_$((New-Guid).ToString())-$(Convert-ByteArrayToHex -Bytes (Get-RandomBytes -Bytes 16))</Id> <ContextId>urn:uuid:$((New-Guid).ToString())</ContextId> <Key>$(Convert-ByteArrayToB64 -Bytes $Key)</Key> <KeyGeneration>urn:uuid:$((New-Guid).ToString())</KeyGeneration> <EffectiveTime>$($NotBefore.Ticks)</EffectiveTime> <ExpiryTime>$($ExpiresOn.Ticks)</ExpiryTime> <KeyEffectiveTime>$($NotBefore.Ticks)</KeyEffectiveTime> <KeyExpiryTime>$($ExpiresOn.Ticks)</KeyExpiryTime> <ClaimsPrincipal> <Identities> <Identity NameClaimType="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name" RoleClaimType="http://schemas.microsoft.com/ws/2008/06/identity/claims/role"> <ClaimCollection> <Claim Issuer="AD AUTHORITY" OriginalIssuer="AD AUTHORITY" Type="http://schemas.microsoft.com/ws/2014/01/identity/claims/anchorclaimtype" Value="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn" ValueType="http://www.w3.org/2001/XMLSchema#string"/> <Claim Issuer="AD AUTHORITY" OriginalIssuer="AD AUTHORITY" Type="http://schemas.microsoft.com/ws/2008/06/identity/claims/authenticationinstant" Value="$($NotBefore.ToUniversalTime().ToString("s", [cultureinfo]::InvariantCulture)+".000Z")" ValueType="http://www.w3.org/2001/XMLSchema#dateTime"/> <Claim Issuer="LOCAL AUTHORITY" OriginalIssuer="LOCAL AUTHORITY" Type="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn" Value="$UserPrincipalName" ValueType="http://www.w3.org/2001/XMLSchema#string"/> <Claim Issuer="LOCAL AUTHORITY" OriginalIssuer="LOCAL AUTHORITY" Type="http://schemas.microsoft.com/claims/authnmethodsreferences" Value="http://schemas.microsoft.com/claims/multipleauthn" ValueType="http://www.w3.org/2001/XMLSchema#string"/> </ClaimCollection> </Identity> </Identities> </ClaimsPrincipal> <EndpointId/> </SessionToken> "@ $sessionToken = Get-DeflatedByteArray -byteArray (XmlToBinary -xml_doc $xml_SessionToken -Dictionary (Get-XmlDictionary -Type Session)) # Construct the refresh token $refresh_token = [ordered]@{ "ClientID" = $ClientID.ToString() "RedirectUri" = $RedirectUri "Resource" = $Resource "Issuer" = $Issuer "NotBefore" = [int]($NotBefore-$epoch).TotalSeconds "ExpiresOn" = [int]($ExpiresOn-$epoch).TotalSeconds "SingleSignOnToken" = @{ "TokenType" = 0 "StringToken" = Convert-ByteArrayToB64 -Bytes $sessionToken "Version" = 1 } | ConvertTo-Json -Compress "DeviceFlowDeviceId" = $null "IsDeviceFlow" = $false "SessionKeyString" = $null } | ConvertTo-Json -Compress $dec_token = [text.encoding]::UTF8.GetBytes($refresh_token) # Create IV and key $dec_IV = Get-RandomBytes -Bytes 16 $dec_Key = Get-RandomBytes -Bytes 32 # Encrypt the refresh token $Crypto = [System.Security.Cryptography.RijndaelManaged]::Create() $Crypto.Mode = "CBC" $Crypto.Padding = "PKCS7" $Crypto.Key = $dec_Key $Crypto.IV = $dec_IV $decryptor = $Crypto.CreateEncryptor() $ms = New-Object System.IO.MemoryStream $cs = New-Object System.Security.Cryptography.CryptoStream($ms,$decryptor,[System.Security.Cryptography.CryptoStreamMode]::Write) $cs.Write($dec_token,0,$dec_token.Count) $cs.Close() $cs.Dispose() $enc_token = $ms.ToArray() $ms.Close() $ms.Dispose() # Encrypt Key Iv block $enc_Key_IV = New-Object Byte[] 48 [Array]::Copy($dec_Key,$enc_Key_IV,32) [Array]::Copy($dec_IV,0,$enc_Key_IV,32,16) $dec_Key_IV=$certificate_encryption.PublicKey.Key.Encrypt($enc_Key_IV, $True) # Get the encryption key hash $sha256 = [System.Security.Cryptography.SHA256]::Create() $hash = $sha256.ComputeHash([text.encoding]::UTF8.GetBytes($certificate_encryption.PublicKey.Key.ToXmlString($false))) # Create the block $buffer = New-Object System.IO.MemoryStream $buffer.Write($hash,0,$hash.Length) $buffer.Write([bitconverter]::GetBytes([uint32]$dec_Key_IV.Length),0,4) $buffer.Write($dec_Key_IV,0,$dec_Key_IV.Length) $buffer.Write([bitconverter]::GetBytes([uint32]$enc_token.Length),0,4) $buffer.Write($enc_token,0,$enc_token.Length) $buffer.Flush() $enc_refresh_token = $buffer.ToArray() $buffer.Dispose() # Sign the token # Store the public key $cspParameters = [System.Security.Cryptography.CspParameters]::new() $cspParameters.ProviderName = "Microsoft Enhanced RSA and AES Cryptographic Provider" $cspParameters.ProviderType = 24 $cspParameters.KeyContainerName ="AADInternals" # Get the private key from the certificate $publicKey = [System.Security.Cryptography.RSACryptoServiceProvider]::new(2048,$cspParameters) $publicKey.ImportParameters($certificate_signing.PublicKey.Key.ExportParameters($False)) $signature = $privateKey_signing.SignData($enc_refresh_token,"SHA256") # Return return "$(Convert-ByteArrayToB64 -Bytes $enc_refresh_token -UrlEncode).$(Convert-ByteArrayToB64 -Bytes $signature -UrlEncode)" } End { Unload-PrivateKey -PrivateKey $privateKey_signing } } # Create a new ADFS Access Token # Nov 1st 2021 function New-ADFSAccessToken { <# .SYNOPSIS Creates a new AccessToken and signs it with the given certificates. .DESCRIPTION Creates a new AccessToken and signs it with the given certificates. #> [cmdletbinding()] Param( [Parameter(Mandatory=$False)] [DateTime]$NotBefore = (Get-Date), [Parameter(Mandatory=$False)] [DateTime]$ExpiresOn = ((Get-Date).AddHours(8)), [Parameter(Mandatory=$False)] [String]$UserPrincipalName, [Parameter(Mandatory=$False)] [String]$Name, [Parameter(Mandatory=$True)] [guid]$ClientID, [Parameter(Mandatory=$True)] [String]$Resource, [Parameter(Mandatory=$False)] [String]$Scope="openid", [Parameter(Mandatory=$True)] [String]$Issuer, [Parameter(Mandatory=$True)] [String]$PfxFileName_signing, [Parameter(Mandatory=$False)] [String]$PfxPassword_signing ) Begin { $certificate_signing = Load-Certificate -FileName $PfxFileName_signing -Password $PfxPassword_signing -Exportable $privateKey_signing = Load-PrivateKey -Certificate $certificate_signing } Process { # Construct the refresh token $payLoad = [ordered]@{ "aud" = $Resource "iss" = $Issuer "iat" = [int]($NotBefore-$epoch).TotalSeconds "nbf" = [int]($NotBefore-$epoch).TotalSeconds "exp" = [int]($ExpiresOn-$epoch).TotalSeconds "sub" = $Name "apptype" = "Public" "appid" = $ClientID "authmethod" = "urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport" "auth_time" = "$($NotBefore.ToUniversalTime().ToString("s", [cultureinfo]::InvariantCulture)+".000Z")" "ver" = "1.0" "scp" = $Scope } $certHash = Convert-ByteArrayToB64 -bytes $certificate_signing.GetCertHash() -UrlEncode -NoPadding $header = [ordered]@{ "typ" = "JWT" "alg" = "RS256" "x5t" = $certHash "kid" = $certHash } $jwt = New-JWT -PrivateKey $privateKey_signing -Header $header -Payload $payLoad return $jwt } End { Unload-PrivateKey -PrivateKey $privateKey_signing } } |