Providers/Okta/Public/Connect-OktaEnvironment.ps1
|
function Connect-OktaEnvironment { <# .SYNOPSIS Establishes the Okta connection every other function in the module uses .DESCRIPTION Two authentication modes, and the order between them is the whole point of the module's design: 1. -ApiToken, an SSWS token created by hand in the admin console. This is the bootstrap. It is the only credential that exists before anything has been created, and it is the one that can create the service app. 2. -ServiceApp, the OAuth service app created by New-OktaServiceApp. From the second run onward this is what you use, and the SSWS token can be revoked. The connection is validated before it is stored. A bad token that is accepted here and rejected on the first real call gives you an error about users when the problem is the credential, so the cheapest possible request is made up front. Prefix and EmailDomain are recorded on the connection rather than passed to every function. They are what teardown keys off, so having them set once, at connect time, removes the failure mode where an environment is seeded under one prefix and torn down under another. .PARAMETER OrgUrl The org URL, for example https://trial-123456.okta.com. Both the admin host (-admin.okta.com) and a trailing slash are tolerated and normalised away, because both are what you get from copying the address bar. .PARAMETER ApiToken An SSWS API token as a SecureString. Create one under Security > API > Tokens in the admin console. The token inherits the permissions of the admin who created it, so a super admin's token is needed to create the service app and assign it a role. .PARAMETER ServiceApp Authenticate with the saved service app credential instead of an SSWS token .PARAMETER CredentialPath Path to the service app credential file. Defaults to the per-user location for this org. .PARAMETER Prefix Name prefix and seed tag for everything this module creates. Everything created and everything removed is scoped by it. .PARAMETER EmailDomain Domain for seeded user logins and emails. The default is under example.com, which RFC 2606 reserves precisely so test data cannot deliver mail to a real recipient. Change it only if you own the domain you change it to. .PARAMETER ActiveUserLimit The tenant's active user ceiling. Ten matches the Okta Integrator Free Plan; raise it if this tenant is on a paid plan. .PARAMETER PassThru Return the connection object .OUTPUTS PSCustomObject describing the connection, when -PassThru is used .EXAMPLE $token = Read-Host 'SSWS token' -AsSecureString Connect-OktaEnvironment -OrgUrl https://trial-123456.okta.com -ApiToken $token First run: bootstrap with the API token .EXAMPLE Connect-OktaEnvironment -OrgUrl https://trial-123456.okta.com -ServiceApp Every run after that: authenticate as the app .EXAMPLE Connect-OktaEnvironment -OrgUrl https://trial-123456.okta.com -ApiToken $token ` -Prefix CONTOSO -EmailDomain contoso-lab.example.com Seed under a different prefix, so two labs can share one tenant .NOTES Author: Jeffrey Stuhr Version: 1.0.0 Last Updated: 2026-08-07 .LINK New-OktaEnvironment New-OktaServiceApp Disconnect-OktaEnvironment #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingPlainTextForPassword', 'CredentialPath', Justification = 'A file path, not a credential. The key it points at never appears here.')] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', 'ServiceApp', Justification = 'Selects the ServiceApp parameter set; the set name is what is read, not the switch.')] [CmdletBinding(DefaultParameterSetName = 'ApiToken')] [OutputType([PSCustomObject])] param( [Parameter(Mandatory = $true)] [ValidatePattern('^https://')] [string]$OrgUrl, [Parameter(Mandatory = $true, ParameterSetName = 'ApiToken')] [System.Security.SecureString]$ApiToken, [Parameter(Mandatory = $true, ParameterSetName = 'ServiceApp')] [switch]$ServiceApp, [Parameter(ParameterSetName = 'ServiceApp')] [string]$CredentialPath, [Parameter()] # Accepts the shared prefix with its trailing separator, which is stripped below. This # provider joins prefix and name with its own hyphen in a dozen places, so it holds the # prefix bare internally while the module-wide default carries the separator that the # other two providers concatenate directly. [ValidatePattern('^[A-Za-z0-9][A-Za-z0-9_-]{0,30}$|^[A-Za-z0-9][A-Za-z0-9_-]*[-_]$')] [string]$Prefix = $script:TestEnvironmentDefaultPrefix, [Parameter()] [ValidateNotNullOrEmpty()] [string]$EmailDomain = 'oktalab.example.com', [Parameter()] [ValidateRange(1, 10000)] [int]$ActiveUserLimit = 10, [Parameter()] [switch]$PassThru ) # The admin console lives on a different host from the API. Pasting the address bar is the # single most common way to get this wrong, and the resulting failure is a 404 on every # call rather than anything that names the cause. $normalisedOrg = $OrgUrl.TrimEnd('/') if ($normalisedOrg -match '^(?<scheme>https://)(?<org>[^.]+)-admin\.(?<rest>.+)$') { $normalisedOrg = '{0}{1}.{2}' -f $Matches.scheme, $Matches.org, $Matches.rest Write-Warning "Using the API host '$normalisedOrg' rather than the admin host you supplied." } $scopes = @() $tokenExpiry = $null if ($PSCmdlet.ParameterSetName -eq 'ApiToken') { $authorizationHeader = 'SSWS {0}' -f (ConvertFrom-TestSecureString -SecureString $ApiToken) $authType = 'ApiToken' $resolvedCredentialPath = $null } else { $resolvedCredentialPath = Get-OktaCredentialPath -OrgUrl $normalisedOrg -Path $CredentialPath $token = Get-OktaAccessToken -CredentialPath $resolvedCredentialPath -OrgUrl $normalisedOrg $authorizationHeader = "Bearer $($token.AccessToken)" $authType = 'ServiceApp' $scopes = @($token.Scopes) $tokenExpiry = $token.ExpiresUtc } # Two forms of the one prefix. The bare form is what this provider concatenates with its own # hyphen; the separator form is what Core derives the shared tag from, and what the other two # providers use directly. Normalised here so a caller may pass either. $barePrefix = $Prefix.TrimEnd('-', '_').ToUpperInvariant() $separatorPrefix = if ($Prefix -match '[-_]$') { $Prefix } else { '{0}-' -f $Prefix } $sharedMarker = Get-TestSeedMarker -Prefix $separatorPrefix $candidate = @{ OrgUrl = $normalisedOrg AuthorizationHeader = $authorizationHeader AuthType = $authType # Held bare, without the trailing separator: this provider supplies its own hyphen when # it joins prefix to name, so keeping the separator here would double it. Prefix = $barePrefix EmailDomain = $EmailDomain.TrimStart('@') ActiveUserLimit = $ActiveUserLimit # The bracketed tag Core defines, rather than this provider's old '[seed:OKTALAB]'. # Entra puts the same bracketed tag inside its description sentence, so one pattern now # finds a seeded object's metadata in either directory. SeedMarker = '[{0}]' -f $sharedMarker.Tag SeedTag = $sharedMarker.Tag CredentialPath = $resolvedCredentialPath Scopes = $scopes TokenExpiresUtc = $tokenExpiry ConnectedAt = Get-Date } # Cheapest call that proves both the host and the credential. limit=1 keeps it to a single # user's worth of response on a tenant that may only hold ten. try { $null = Invoke-OktaRequest -Method GET -Path '/api/v1/users' -Query @{ limit = 1 } ` -Connection $candidate } catch { throw ("Could not authenticate to $normalisedOrg as $authType`: $($_.Exception.Message)") } $script:OktaConnection = $candidate Write-TestMessage -Message "Connected to $normalisedOrg as $authType (prefix $($candidate.Prefix))" ` -Type Success if ($PassThru) { # The Authorization header is deliberately absent from what is returned. It is a live # credential and this object ends up in transcripts and PassThru result bundles. return [PSCustomObject]@{ OrgUrl = $candidate.OrgUrl AuthType = $candidate.AuthType Prefix = $candidate.Prefix # Surfaced because it is what a person checks when asking "is this object mine" - # it is written into every seeded user's labSeedTag and appended to every seeded # description, and it was invisible from the outside until now. SeedTag = $candidate.SeedTag SeedMarker = $candidate.SeedMarker EmailDomain = $candidate.EmailDomain ActiveUserLimit = $candidate.ActiveUserLimit Scopes = $candidate.Scopes TokenExpiresUtc = $candidate.TokenExpiresUtc ConnectedAt = $candidate.ConnectedAt } } } |