DSInternals.Passkeys.Okta.psm1
|
# HTTP Authorization scheme used by an Okta access token. # 'Bearer' is used by both OAuth client-credentials and authorization-code flows; # 'SSWS' is the scheme for static Okta API tokens. enum OktaTokenScheme { Bearer SSWS } # Normalizes an Okta access token across all supported authentication flows # so that downstream cmdlets do not need to care which flow produced it. class OktaToken { [OktaTokenScheme] $Scheme [string] $AccessToken [Uri] $Tenant OktaToken([OktaTokenScheme] $scheme, [string] $accessToken, [Uri] $tenant) { $this.Scheme = $scheme $this.AccessToken = $accessToken $this.Tenant = $tenant } } # State needed to revoke an OAuth-issued access token via /oauth2/v1/revoke. # Not used for static SSWS API tokens, which are managed in the Okta admin console. class OktaRevocationInfo { [string] $ClientId # JWT client assertion used for the private_key_jwt flow. [string] $RevocationToken # Plaintext client secret used for the client_secret_post flow. [string] $ClientSecret OktaRevocationInfo([string] $clientId) { $this.ClientId = $clientId } } # Variables used for Okta connection lifecycle management [OktaToken] $Script:OktaToken = $null [OktaRevocationInfo] $Script:OktaRevocationInfo = $null <# .SYNOPSIS Retrieves creation options required to generate and register an Okta compatible passkey. .DESCRIPTION Retrieves a server-issued challenge and the associated WebAuthn parameters needed to register (attest) a new passkey for the specified Okta user. The returned object can be piped to New-Passkey to drive the local authenticator and then to Register-OktaPasskey to complete enrollment. For end-to-end passkey registration in Okta, calling Register-OktaPasskey directly is recommended; it performs the challenge request, authenticator ceremony, and activation in a single step. Use Get-OktaPasskeyRegistrationOptions only when you need to inspect or customize the intermediate options. Requires an active Okta connection (Connect-Okta). .PARAMETER UserId The unique identifier of the Okta user. .PARAMETER Login The Okta user login (typically an email address such as 'user@example.com'). Resolved to a UserId through an API call. .PARAMETER ChallengeTimeout Overrides the timeout of the server-generated challenge returned in the request. The default value is 5 minutes, with the accepted range being between 1 second and 1 day. .EXAMPLE Connect-Okta -Tenant example.okta.com -ClientId 0oakmj8hvxvtvCy3P5d7 Get-OktaPasskeyRegistrationOptions -UserId 00eDuihq64pgP1gVD0x7 Fetches default creation options for the specified Okta user, identified by their Okta id. .EXAMPLE Connect-Okta -Tenant example.okta.com -ClientId 0oakmj8hvxvtvCy3P5d7 Get-OktaPasskeyRegistrationOptions -Login 'user@example.com' Resolves the Okta user by login and then fetches creation options, avoiding the need to look up the Okta id manually. .EXAMPLE Connect-Okta -Tenant example.okta.com -ClientId 0oakmj8hvxvtvCy3P5d7 Get-OktaPasskeyRegistrationOptions -UserId 00eDuihq64pgP1gVD0x7 -ChallengeTimeout (New-TimeSpan -Minutes 1) Fetches creation options with a shorter 1-minute challenge timeout to tighten the registration window. .EXAMPLE Connect-Okta -Tenant example.okta.com -ClientId 0oakmj8hvxvtvCy3P5d7 Get-OktaPasskeyRegistrationOptions -Login 'user@example.com' | New-Passkey | Register-OktaPasskey Performs end-to-end passkey registration in Okta in a single pipeline. .LINK Register-OktaPasskey .LINK New-Passkey .LINK Connect-Okta .LINK https://developer.okta.com/docs/api/openapi/okta-management/management/tag/UserFactor/#tag/UserFactor/operation/enrollFactor #> function Get-OktaPasskeyRegistrationOptions { [CmdletBinding(DefaultParameterSetName = 'UserId')] [OutputType([DSInternals.Win32.WebAuthn.Okta.OktaWebauthnCredentialCreationOptions])] param( [Parameter(Mandatory = $true, ValueFromPipeline = $true, ParameterSetName = 'UserId')] [ValidatePattern('^[A-Za-z0-9_-]{20}$')] [Alias('User')] [string] $UserId, [Parameter(Mandatory = $true, ParameterSetName = 'Login')] [ValidateNotNullOrEmpty()] [Alias('UserPrincipalName','UPN','UserName','Email')] [string] $Login, [Parameter(Mandatory = $false)] [ValidateScript({ if ($PSItem -is [TimeSpan]) { [timespan] $min = New-TimeSpan -Seconds 1 [timespan] $max = New-TimeSpan -Days 1 return $PSItem -ge $min -and $PSItem -le $max } else { throw 'Parameter must be a TimeSpan object.' } })] [Alias('Timeout')] [TimeSpan] $ChallengeTimeout = (New-TimeSpan -Minutes 5) ) begin { if ($null -eq $Script:OktaToken) { $PSCmdlet.ThrowTerminatingError( [System.Management.Automation.ErrorRecord]::new( [System.InvalidOperationException]::new('Not connected to Okta, call Connect-Okta to get started.'), 'NotConnectedToOkta', [System.Management.Automation.ErrorCategory]::ConnectionError, $null)) } } process { try { if ($PSCmdlet.ParameterSetName -eq 'Login') { $UserId = Get-OktaUserId -Login $Login -ErrorAction Stop } Write-Debug "In Get-OktaPasskeyRegistrationOptions with ${UserId} and ${ChallengeTimeout}" [int] $TokenLifetimeSeconds = $ChallengeTimeout.TotalSeconds Write-Debug "TokenLifetimeSeconds ${TokenLifetimeSeconds}" [string] $credentialOptionsPath = "/api/v1/users/${UserId}/factors" [string] $credentialOptionsQuery = "tokenLifetimeSeconds=${TokenLifetimeSeconds}&activate=true" Write-Debug ('Credential options path: ' + $credentialOptionsPath) Write-Debug ('Credential options query: ' + $credentialOptionsQuery) [string] $body = @{ factorType = 'webauthn' provider = 'FIDO' } | ConvertTo-Json -Compress Write-Debug ('Credential options payload: ' + $body) [string] $response = Invoke-OktaWebRequest -Path $credentialOptionsPath ` -Query $credentialOptionsQuery ` -Body $body ` -ErrorAction Stop Write-Debug ('Credential options response: ' + $response) # Parse JSON response. Okta omits the relying party id from server-issued options; # carry the live tenant host on the options object so downstream WebAuthn API calls # can forward it as the `hostName` argument. [DSInternals.Win32.WebAuthn.Okta.OktaWebauthnCredentialCreationOptions] $options = [DSInternals.Win32.WebAuthn.Okta.OktaWebauthnCredentialCreationOptions]::Create($response) $options.Tenant = $Script:OktaToken.Tenant.Host Write-Debug ('Credential options: ' + ($options | Out-String)) return $options } catch { throw $PSItem } } } <# .SYNOPSIS Registers a new passkey in Okta. .DESCRIPTION Registers a new passkey for the specified user in Okta by submitting the attestation that activates the corresponding webauthn factor. The cmdlet supports three usage patterns: - Pass only -UserId to perform the full ceremony end-to-end: request a challenge, drive the local authenticator, and submit the attestation. - Pipe an attestation from a previous New-Passkey call against Okta options. - Pass -UserId, -FactorId, and a raw -AttestationPublicKeyCredential when the challenge was issued and the credential ceremony was run separately. Requires an active Okta connection (Connect-Okta). .PARAMETER UserId The unique identifier of the Okta user. .PARAMETER Login The Okta user login (typically an email address such as 'user@example.com'). Resolved to a UserId through an API call. .PARAMETER ChallengeTimeout Overrides the timeout of the server-generated challenge returned in the request. The default value is 5 minutes, with the accepted range being between 1 second and 1 day. .PARAMETER Passkey The passkey to be registered. .PARAMETER FactorId The Okta factor identifier returned by Get-OktaPasskeyRegistrationOptions, used together with -AttestationPublicKeyCredential as an alternative to -Passkey. .PARAMETER AttestationPublicKeyCredential The raw attestation credential produced by the local WebAuthn authenticator (e.g. via New-Passkey), used together with -UserId and -FactorId as an alternative to -Passkey. .EXAMPLE Connect-Okta -Tenant example.okta.com -ClientId 0oakmj8hvxvtvCy3P5d7 Register-OktaPasskey -UserId 00eDuihq64pgP1gVD0x7 Performs the full registration ceremony in one step: enrolls a webauthn factor, prompts the local authenticator, and activates the factor in Okta. .EXAMPLE Connect-Okta -Tenant example.okta.com -ClientId 0oakmj8hvxvtvCy3P5d7 Register-OktaPasskey -Login 'user@example.com' Resolves the Okta user by login and then performs the full registration ceremony, avoiding the need to look up the Okta id manually. .EXAMPLE Connect-Okta -Tenant example.okta.com -ClientId 0oakmj8hvxvtvCy3P5d7 Register-OktaPasskey -UserId 00eDuihq64pgP1gVD0x7 -ChallengeTimeout (New-TimeSpan -Minutes 1) Registers a passkey using a shorter 1-minute challenge timeout to tighten the registration window. .EXAMPLE Connect-Okta -Tenant example.okta.com -ClientId 0oakmj8hvxvtvCy3P5d7 Get-OktaPasskeyRegistrationOptions -UserId 00eDuihq64pgP1gVD0x7 | New-Passkey | Register-OktaPasskey Splits the registration into explicit pipeline stages: enroll the factor, create the credential locally, and activate. Equivalent to the single-step form but lets the caller inspect intermediate values. .EXAMPLE Connect-Okta -Tenant example.okta.com -ClientId 0oakmj8hvxvtvCy3P5d7 $options = Get-OktaPasskeyRegistrationOptions -UserId 00eDuihq64pgP1gVD0x7 $credential = New-Passkey -Options $options.PublicKeyOptions -HostName $options.Tenant Register-OktaPasskey -UserId 00eDuihq64pgP1gVD0x7 -FactorId $options.FactorId -AttestationPublicKeyCredential $credential Drives the WebAuthn ceremony with a raw AttestationPublicKeyCredential and assembles the activation manually. The tenant host carried on the options object is forwarded to New-Passkey via -HostName to substitute for the rpId that Okta omits. .LINK Get-OktaPasskeyRegistrationOptions .LINK New-Passkey .LINK Connect-Okta .LINK https://developer.okta.com/docs/api/openapi/okta-management/management/tag/UserFactor/#tag/UserFactor/operation/activateFactor #> function Register-OktaPasskey { [CmdletBinding(DefaultParameterSetName = 'New')] [OutputType([DSInternals.Win32.WebAuthn.Okta.OktaFido2AuthenticationMethod])] param ( [Parameter(Mandatory = $true, ParameterSetName = 'New')] [Parameter(Mandatory = $true, ParameterSetName = 'AttestationCredential')] [ValidatePattern('^[A-Za-z0-9_-]{20}$')] [Alias('User')] [string] $UserId, [Parameter(Mandatory = $true, ParameterSetName = 'NewByLogin')] [Parameter(Mandatory = $true, ParameterSetName = 'AttestationCredentialByLogin')] [ValidateNotNullOrEmpty()] [Alias('UserPrincipalName','UPN','UserName','Email')] [string] $Login, [Parameter(Mandatory = $true, ParameterSetName = 'Existing', ValueFromPipeline = $true)] [DSInternals.Win32.WebAuthn.Okta.OktaWebauthnAttestationResponse] $Passkey, [Parameter(Mandatory = $true, ParameterSetName = 'AttestationCredential')] [Parameter(Mandatory = $true, ParameterSetName = 'AttestationCredentialByLogin')] [ValidatePattern('^[A-Za-z0-9_-]{20}$')] [Alias('Factor')] [string] $FactorId, [Parameter(Mandatory = $true, ParameterSetName = 'AttestationCredential', ValueFromPipeline = $true)] [Parameter(Mandatory = $true, ParameterSetName = 'AttestationCredentialByLogin', ValueFromPipeline = $true)] [Alias('Attestation','Credential')] [DSInternals.Win32.WebAuthn.AttestationPublicKeyCredential] $AttestationPublicKeyCredential, [Parameter(Mandatory = $false, ParameterSetName = 'New')] [Parameter(Mandatory = $false, ParameterSetName = 'NewByLogin')] [ValidateScript({ if ($PSItem -is [TimeSpan]) { [timespan] $min = New-TimeSpan -Seconds 1 [timespan] $max = New-TimeSpan -Days 1 return $PSItem -ge $min -and $PSItem -le $max } else { throw 'Parameter must be a TimeSpan object.' } })] [Alias('Timeout')] [timespan] $ChallengeTimeout = (New-TimeSpan -Minutes 5) ) begin { if ($null -eq $Script:OktaToken) { $PSCmdlet.ThrowTerminatingError( [System.Management.Automation.ErrorRecord]::new( [System.InvalidOperationException]::new('Not connected to Okta, call Connect-Okta to get started.'), 'NotConnectedToOkta', [System.Management.Automation.ErrorCategory]::ConnectionError, $null)) } } process { try { # Translate login to ID if ($PSCmdlet.ParameterSetName -in 'NewByLogin','AttestationCredentialByLogin') { $UserId = Get-OktaUserId -Login $Login -ErrorAction Stop } if ($PSCmdlet.ParameterSetName -in 'New','NewByLogin') { # Fetch the challenge [DSInternals.Win32.WebAuthn.Okta.OktaWebauthnCredentialCreationOptions] $options = Get-OktaPasskeyRegistrationOptions -UserId $UserId -ChallengeTimeout $ChallengeTimeout -ErrorAction Stop # Display the passkey registration prompt. Pass the live tenant host name as `hostName`, # which the WebAuthn API uses to derive the origin and to fill in the missing rpId # that Okta omits from its server-issued options. [DSInternals.Win32.WebAuthn.WebAuthnApi] $api = [DSInternals.Win32.WebAuthn.WebAuthnApi]::new() [DSInternals.Win32.WebAuthn.AttestationPublicKeyCredential] $credential = $api.AuthenticatorMakeCredential($options.PublicKeyOptions, $Script:OktaToken.Tenant.Host) $FactorId = $options.FactorId $Passkey = [DSInternals.Win32.WebAuthn.Okta.OktaWebauthnAttestationResponse]::new( $credential, $UserId, $FactorId) } elseif ($PSCmdlet.ParameterSetName -in 'AttestationCredential','AttestationCredentialByLogin') { $Passkey = [DSInternals.Win32.WebAuthn.Okta.OktaWebauthnAttestationResponse]::new( $AttestationPublicKeyCredential, $UserId, $FactorId) } else { # 'Existing' parameter set: $Passkey was bound from the pipeline $UserId = $Passkey.UserId $FactorId = $Passkey.FactorId } # Activate the factor with the attestation response [string] $registrationPath = "/api/v1/users/${UserId}/factors/${FactorId}/lifecycle/activate" Write-Debug ('Registration path: ' + $registrationPath) [string] $response = Invoke-OktaWebRequest -Path $registrationPath ` -Body $Passkey.ToString() ` -ErrorAction Stop Write-Debug ('Registration response: ' + $response) return [DSInternals.Win32.WebAuthn.Okta.OktaFido2AuthenticationMethod]::FromJsonString($response) } catch { throw $PSItem } } } <# .SYNOPSIS Resolves an Okta user login to its Okta user id. .DESCRIPTION Queries the Okta users endpoint with a server-side filter on profile.login to return the first matching user's Okta id. Throws if no user matches the supplied login. Useful as a bridge when callers know the user's login (email) but the downstream cmdlets, like Register-OktaPasskey, require the Okta id. Requires an active Okta connection (Connect-Okta). .PARAMETER Login The Okta user login (typically an email address such as 'user@example.com'). .EXAMPLE Connect-Okta -Tenant example.okta.com -ClientId 0oakmj8hvxvtvCy3P5d7 Get-OktaUserId -Login 'user@example.com' Looks up the Okta id for the user with the given login and returns it as a string. .EXAMPLE Connect-Okta -Tenant example.okta.com -ClientId 0oakmj8hvxvtvCy3P5d7 $userId = Get-OktaUserId -Login 'user@example.com' Register-OktaPasskey -UserId $userId Resolves the login to an Okta id and reuses it to register a passkey. .LINK Connect-Okta .LINK Get-OktaPasskeyRegistrationOptions .LINK Register-OktaPasskey .LINK https://developer.okta.com/docs/api/openapi/okta-management/management/tag/User/#tag/User/operation/listUsers #> function Get-OktaUserId { [OutputType([string])] param( [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [ValidateNotNullOrEmpty()] [Alias('UserPrincipalName','UPN','UserName','Email')] [string] $Login ) begin { if ($null -eq $Script:OktaToken) { $PSCmdlet.ThrowTerminatingError( [System.Management.Automation.ErrorRecord]::new( [System.InvalidOperationException]::new('Not connected to Okta, call Connect-Okta to get started.'), 'NotConnectedToOkta', [System.Management.Automation.ErrorCategory]::ConnectionError, $null)) } } process { try { Write-Debug "In Get-OktaUserId with ${Login}" # `filter` targets the indexed profile.login property for an exact, server-side match, # and `limit=1` caps the response to a single user record. ConvertTo-Json wraps the login # in a SCIM-compatible JSON string literal (surrounding quotes plus standard escapes). [string] $filterExpression = 'profile.login eq {0}' -f (ConvertTo-Json -InputObject $Login -Compress) [string] $usersQuery = 'filter={0}&limit=1' -f [uri]::EscapeDataString($filterExpression) [string] $usersPath = '/api/v1/users' Write-Debug ('User lookup path: ' + $usersPath) Write-Debug ('User lookup query: ' + $usersQuery) [string] $response = Invoke-OktaWebRequest -Path $usersPath ` -Query $usersQuery ` -Method ([Microsoft.PowerShell.Commands.WebRequestMethod]::Get) ` -ErrorAction Stop Write-Debug ('User lookup response: ' + $response) [object[]] $users = ConvertFrom-Json -InputObject $response if ($null -eq $users -or $users.Count -eq 0) { throw ('Okta user not found: {0}' -f $Login) } return [string] $users[0].id } catch { throw $PSItem } } } <# .SYNOPSIS Retrieves an access token to interact with Okta APIs. .DESCRIPTION Acquires an Okta access token via one of four authentication flows, depending on which parameters are supplied, and caches it for subsequent cmdlets in this module: - Interactive authorization code (public client) when only -Tenant and -ClientId are supplied. - Client credentials with private_key_jwt when -JsonWebKey is supplied. - Client credentials with client_secret_post when -ClientSecret is supplied. - Static API token (SSWS) when -ApiToken is supplied. The cached token is reused by Get-OktaPasskeyRegistrationOptions, Register-OktaPasskey, and Disconnect-Okta. Call Disconnect-Okta to revoke the token (for OAuth flows) or clear it from the session (for SSWS). .PARAMETER Tenant The unique identifier of Okta tenant, like 'example.okta.com'. .PARAMETER ClientId The client id of the Okta application used to obtain an access token. .PARAMETER Scopes Scopes to request for the access token. Defaults to 'okta.users.manage'. .PARAMETER JsonWebKey The JSON Web Key used to authenticate to the Okta application, in order to obtain access token using the client credentials OAuth flow (private_key_jwt). .PARAMETER ClientSecret The client secret used to authenticate to the Okta application, in order to obtain access token using the client credentials OAuth flow (client_secret_post). .PARAMETER ApiToken A static Okta API token (SSWS). Issued from the Okta admin console under Security > API > Tokens. .EXAMPLE Connect-Okta -Tenant example.okta.com -ClientId 0oakmj8hvxvtvCy3P5d7 Connects to the `example.okta.com` tenant using the application with client id `0oakmj8hvxvtvCy3P5d7` via the authorization code flow with PKCE. .EXAMPLE Connect-Okta -Tenant example.okta.com -ClientId 0oakmj8hvxvtvCy3P5d7 -Scopes @('okta.users.manage','okta.something.else') Connects to the `example.okta.com` tenant using the application with client id `0oakmj8hvxvtvCy3P5d7` via the authorization code flow with PKCE, requesting scopes `'okta.users.manage'` and `'okta.something.else'`. .EXAMPLE $jwk = '{"kty":"RSA","kid":"EE3QB0WvhuOwR9DuR6717OERKbDrBemrDKOK4Xvbf8c","d":"TmljZSB0cnkhICBCdXQgdGhpcyBpc...' Connect-Okta -Tenant example.okta.com -ClientId 0oakmj8hvxvtvCy3P5d7 -Scopes @('okta.users.manage','okta.something.else') -JsonWebKey $jwk Connects to the `example.okta.com` tenant using the application with client id `0oakmj8hvxvtvCy3P5d7` via the client credentials flow with private_key_jwt, signing the client assertion with `$jwk` and requesting scopes `'okta.users.manage'` and `'okta.something.else'`. .EXAMPLE $secret = Read-Host -AsSecureString -Prompt 'Client secret' Connect-Okta -Tenant example.okta.com -ClientId 0oakmj8hvxvtvCy3P5d7 -Scopes @('okta.users.manage') -ClientSecret $secret Connects to the `example.okta.com` tenant using the application with client id `0oakmj8hvxvtvCy3P5d7` via the client credentials flow with client_secret_post, authenticating with the SecureString-protected `$secret`. .EXAMPLE $apiToken = Read-Host -AsSecureString -Prompt 'API token' Connect-Okta -Tenant example.okta.com -ApiToken $apiToken Connects to the `example.okta.com` tenant using a static SSWS API token issued in the Okta admin console, bypassing the OAuth flow entirely. .LINK Disconnect-Okta .LINK Register-OktaPasskey .LINK https://developer.okta.com/docs/guides/create-an-api-token/main/ .LINK https://developer.okta.com/docs/api/openapi/okta-oauth/guides/client-auth #> function Connect-Okta { param( [Parameter(Mandatory = $true, ParameterSetName = 'ClientCredentials')] [Parameter(Mandatory = $true, ParameterSetName = 'ClientSecret')] [Parameter(Mandatory = $true, ParameterSetName = 'AuthorizationCode')] [Parameter(Mandatory = $true, ParameterSetName = 'ApiToken')] [ValidatePattern('^[a-zA-Z0-9-]+\.okta(?:-emea|preview|\.mil)?\.com$')] [Alias('Organization','OktaOrganization','OktaDomain')] [string] $Tenant, [Parameter(Mandatory = $true, ValueFromPipeline = $true, ParameterSetName = 'ClientCredentials')] [Parameter(Mandatory = $true, ValueFromPipeline = $true, ParameterSetName = 'ClientSecret')] [Parameter(Mandatory = $true, ValueFromPipeline = $true, ParameterSetName = 'AuthorizationCode')] [ValidatePattern('^[A-Za-z0-9_-]{20}$')] [string] $ClientId, [Parameter(Mandatory = $false, ParameterSetName = 'ClientCredentials')] [Parameter(Mandatory = $false, ParameterSetName = 'ClientSecret')] [Parameter(Mandatory = $false, ParameterSetName = 'AuthorizationCode')] [string[]] $Scopes = @('okta.users.manage'), [Parameter(Mandatory = $true, ValueFromPipeline = $true, ParameterSetName = 'ClientCredentials')] [Alias('jwk')] [string] $JsonWebKey, [Parameter(Mandatory = $true, ValueFromPipeline = $true, ParameterSetName = 'ClientSecret')] [ValidateNotNull()] [Alias('Secret')] [securestring] $ClientSecret, [Parameter(Mandatory = $true, ValueFromPipeline = $true, ParameterSetName = 'ApiToken')] [ValidateNotNull()] [Alias('ApiKey','SswsToken','SSWS')] [securestring] $ApiToken ) try { [Uri] $tenantUri = [System.UriBuilder]::new([System.Uri]::UriSchemeHttps, $Tenant).Uri $Script:OktaRevocationInfo = [OktaRevocationInfo]::new($ClientId) switch ($PSCmdlet.ParameterSetName){ 'ApiToken' { Write-Debug 'SSWS API token provided, using static API token authentication' [string] $plaintextApiToken = ConvertFrom-SecureStringPlainText -SecureString $ApiToken $Script:OktaToken = [OktaToken]::new([OktaTokenScheme]::SSWS, $plaintextApiToken, $tenantUri) } 'AuthorizationCode' { Write-Debug 'No JWK found, assuming public client intended' [Microsoft.Identity.Client.IPublicClientApplication] $publicClientApp = [Microsoft.Identity.Client.PublicClientApplicationBuilder]::Create($ClientId). WithExperimentalFeatures(). WithOidcAuthority($tenantUri.ToString()). WithRedirectUri('http://localhost:8080/login/callback'). Build() [Microsoft.Identity.Client.AuthenticationResult] $authResult = $publicClientApp.AcquireTokenInteractive($Scopes).ExecuteAsync().GetAwaiter().GetResult() if ($null -ne $authResult) { $Script:OktaToken = [OktaToken]::new([OktaTokenScheme]::Bearer, $authResult.AccessToken, $tenantUri) Write-Verbose 'Okta access token successfully retrieved.' } } 'ClientSecret' { Write-Debug 'Client secret provided, using confidential client with client_secret_post' [string] $plaintextSecret = ConvertFrom-SecureStringPlainText -SecureString $ClientSecret $Script:OktaRevocationInfo.ClientSecret = $plaintextSecret [Microsoft.Identity.Client.IConfidentialClientApplication] $confidentialClientApp = [Microsoft.Identity.Client.ConfidentialClientApplicationBuilder]::Create($ClientId). WithClientSecret($plaintextSecret). WithOidcAuthority($tenantUri.ToString()). Build() [Microsoft.Identity.Client.AuthenticationResult] $authResult = $confidentialClientApp.AcquireTokenForClient($Scopes).ExecuteAsync().GetAwaiter().GetResult() if ($null -ne $authResult) { $Script:OktaToken = [OktaToken]::new([OktaTokenScheme]::Bearer, $authResult.AccessToken, $tenantUri) Write-Verbose 'Okta access token successfully retrieved.' } } 'ClientCredentials' { Write-Debug 'JWK found, assuming confidential client intended' [Microsoft.IdentityModel.Tokens.JsonWebKey] $jwk = [Microsoft.IdentityModel.Tokens.JsonWebKey]::new($JsonWebKey) [Microsoft.IdentityModel.Tokens.SigningCredentials] $signingCredentials = [Microsoft.IdentityModel.Tokens.SigningCredentials]::new($jwk, 'RS256') [string] $issuer = $ClientId [System.UriBuilder] $audienceUri = [System.UriBuilder]::new([System.Uri]::UriSchemeHttps, $Tenant, -1, '/oauth2/v1/token') [System.UriBuilder] $revocationAudienceUri = [System.UriBuilder]::new([System.Uri]::UriSchemeHttps, $Tenant, -1, '/oauth2/v1/revoke') [string] $audience = $audienceUri.ToString() [System.Security.Claims.ClaimsIdentity] $subject = [System.Security.Claims.ClaimsIdentity]::new() $subject.Claims.Add([System.Security.Claims.Claim]::new('sub', $ClientId)) [datetime] $notBefore = Get-Date [datetime] $expires = $notBefore.AddMinutes(60) [datetime] $issuedAt = $notBefore [System.IdentityModel.Tokens.Jwt.JwtSecurityTokenHandler] $tokenHandler = [System.IdentityModel.Tokens.Jwt.JwtSecurityTokenHandler]::new() [System.IdentityModel.Tokens.Jwt.JwtSecurityToken] $securityToken = $tokenHandler.CreateJwtSecurityToken($issuer, $audience, $subject, $notBefore, $expires, $issuedAt, $signingCredentials) [System.IdentityModel.Tokens.Jwt.JwtSecurityToken] $revocationToken = $tokenHandler.CreateJwtSecurityToken($issuer, $revocationAudienceUri.ToString(), $subject, $notBefore, $expires, $issuedAt, $signingCredentials) [string] $assertion = $tokenHandler.WriteToken($securityToken) $Script:OktaRevocationInfo.RevocationToken = $tokenHandler.WriteToken($revocationToken) [Microsoft.Identity.Client.IConfidentialClientApplication] $confidentialClientApp = [Microsoft.Identity.Client.ConfidentialClientApplicationBuilder]::Create($ClientId). WithClientAssertion($assertion). WithOidcAuthority($tenantUri.ToString()). Build() [Microsoft.Identity.Client.AuthenticationResult] $authResult = $confidentialClientApp.AcquireTokenForClient($Scopes).ExecuteAsync().GetAwaiter().GetResult() if ($null -ne $authResult -and $null -ne $Script:OktaRevocationInfo.RevocationToken) { $Script:OktaToken = [OktaToken]::new([OktaTokenScheme]::Bearer, $authResult.AccessToken, $tenantUri) Write-Verbose 'Okta access and revocation tokens successfully retrieved.' } } } } catch { throw } } <# .SYNOPSIS Revokes Okta access token. .DESCRIPTION Revokes the Okta access token cached from the call to Connect-Okta and clears it from the session. For OAuth-issued Bearer tokens, this calls the /oauth2/v1/revoke endpoint using the same client authentication method that was used to obtain the token (client_assertion or client_secret). For static SSWS API tokens, the cached token is simply discarded from the session because Okta does not expose a revoke endpoint for static tokens; revocation for those is managed in the Okta admin console. If no token is cached, this cmdlet is a no-op. .EXAMPLE Disconnect-Okta Revokes the cached OAuth access token (or clears the cached SSWS token) and removes any associated revocation state from the session. .LINK Connect-Okta .LINK Register-OktaPasskey .LINK https://developer.okta.com/docs/guides/revoke-tokens/main/ #> function Disconnect-Okta { if ($null -ne $Script:OktaToken) { if ($Script:OktaToken.Scheme -eq [OktaTokenScheme]::SSWS) { # Static SSWS API tokens are managed in the Okta admin console; the OAuth revoke endpoint does not accept them. $Script:OktaToken = $null $Script:OktaRevocationInfo = $null Write-Verbose 'Okta SSWS API token cleared from the session.' return } [string] $revocationPath = '/oauth2/v1/revoke' Write-Debug ('Revocation path: ' + $revocationPath) [hashtable] $body = @{ client_id = $Script:OktaRevocationInfo.ClientId token = $Script:OktaToken.AccessToken token_type_hint = 'access_token' } if ($null -ne $Script:OktaRevocationInfo.RevocationToken) { $body.Add('client_assertion_type','urn:ietf:params:oauth:client-assertion-type:jwt-bearer') $body.Add('client_assertion',$Script:OktaRevocationInfo.RevocationToken) } elseif (-not [string]::IsNullOrEmpty($Script:OktaRevocationInfo.ClientSecret)) { $body.Add('client_secret',$Script:OktaRevocationInfo.ClientSecret) } Write-Debug ('Revocation payload: ' + ($body | ConvertTo-Json)) [string] $response = Invoke-OktaWebRequest -Path $revocationPath ` -ContentType 'application/x-www-form-urlencoded' ` -Body $body ` -ErrorAction Stop $Script:OktaToken = $null $Script:OktaRevocationInfo.RevocationToken = $null $Script:OktaRevocationInfo.ClientSecret = $null if ($response.Length -eq 0 -and $null -eq $Script:OktaToken) { Write-Verbose 'Okta access token successfully revoked.' } } } function Invoke-OktaWebRequest { [OutputType([Microsoft.PowerShell.Commands.WebResponseObject])] param( [Parameter(Mandatory = $true)] [string] $Path, [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()] [string] $Query, [Parameter(Mandatory = $false)] [Microsoft.PowerShell.Commands.WebRequestMethod] $Method = [Microsoft.PowerShell.Commands.WebRequestMethod]::Post, [Parameter(Mandatory = $false)] [ValidateNotNull()] $Body, [Parameter(Mandatory = $false)] [ValidateNotNullOrEmpty()] [string] $ContentType = 'application/json' ) [hashtable] $headers = @{ 'Accept' = 'application/json' 'Authorization' = '{0} {1}' -f $Script:OktaToken.Scheme, $Script:OktaToken.AccessToken } [System.UriBuilder] $requestUri = [System.UriBuilder]::new([System.Uri]::UriSchemeHttps, $Script:OktaToken.Tenant.Host, -1, $Path) if (-not [string]::IsNullOrEmpty($Query)) { $requestUri.Query = $Query } [hashtable] $extraParams = @{} if ($PSVersionTable.PSEdition -eq 'Desktop') { # Avoid dependency on Internet Explorer's DOM parser on Windows PowerShell. $extraParams['UseBasicParsing'] = $true } return Invoke-WebRequest -Uri $requestUri.ToString() ` -Method $Method ` -Headers $headers ` -ContentType $ContentType ` -Body $Body ` @extraParams } # Unwraps a SecureString to a plaintext value via unmanaged memory. # The unmanaged buffer is always zeroed, so the secret only lives in the returned managed string. function ConvertFrom-SecureStringPlainText { [OutputType([string])] param( [Parameter(Mandatory = $true)] [ValidateNotNull()] [securestring] $SecureString ) [IntPtr] $ptr = [System.Runtime.InteropServices.Marshal]::SecureStringToGlobalAllocUnicode($SecureString) try { return [System.Runtime.InteropServices.Marshal]::PtrToStringUni($ptr) } finally { [System.Runtime.InteropServices.Marshal]::ZeroFreeGlobalAllocUnicode($ptr) } } # Functions are filtered by FunctionsToExport in the parent manifest. |