param( [string]$ApiBaseUrl = 'http://localhost:5286/api', [string]$ClientId = 'ssp-demo-worker', [string]$ClientSecret = 'DemoOnly-DoNotUseInProduction!', [int]$ApiTimeoutSec = 30 ) $ErrorActionPreference = 'Stop' function Invoke-TestApi { param( [ValidateSet('GET', 'POST', 'DELETE')] [string]$Method, [string]$Path, [object]$Body, [string]$BearerToken ) $uri = ('{0}/{1}' -f $ApiBaseUrl.TrimEnd('/'), $Path.TrimStart('/')) $parameters = @{ Method = $Method Uri = $uri TimeoutSec = $ApiTimeoutSec ErrorAction = 'Stop' } if ($Body) { $parameters.Body = ($Body | ConvertTo-Json -Depth 10) $parameters.ContentType = 'application/json' } if ($BearerToken) { $parameters.Headers = @{ Authorization = "Bearer $BearerToken" } } try { Invoke-RestMethod @parameters } catch { $responseBody = '' if ($_.Exception.Response) { try { $stream = $_.Exception.Response.GetResponseStream() if ($stream) { $reader = [System.IO.StreamReader]::new($stream) $text = $reader.ReadToEnd() if (-not [string]::IsNullOrWhiteSpace($text)) { $responseBody = $text } } } catch { $responseBody = '' } } throw "API request failed. Method=[$Method], Uri=[$uri], Response=[$responseBody]. $($_.Exception.Message)" } } function Invoke-TestApiExpectFailure { param( [ValidateSet('GET', 'POST', 'DELETE')] [string]$Method, [string]$Path, [object]$Body, [string]$BearerToken ) $uri = ('{0}/{1}' -f $ApiBaseUrl.TrimEnd('/'), $Path.TrimStart('/')) $parameters = @{ Method = $Method Uri = $uri TimeoutSec = $ApiTimeoutSec ErrorAction = 'Stop' } if ($Body) { $parameters.Body = ($Body | ConvertTo-Json -Depth 10) $parameters.ContentType = 'application/json' } if ($BearerToken) { $parameters.Headers = @{ Authorization = "Bearer $BearerToken" } } try { Invoke-RestMethod @parameters | Out-Null throw "API request was expected to fail but succeeded. Method=[$Method], Uri=[$uri]." } catch { if ($_.Exception.Response -and $_.Exception.Response.StatusCode) { return [int]$_.Exception.Response.StatusCode } throw } } Describe 'On-prem client credentials API authentication' { It 'issues a bearer token for the seeded demo worker client' { $token = Invoke-TestApi -Method POST -Path 'auth/token' -Body @{ clientId = $ClientId clientSecret = $ClientSecret scope = 'configuration.read' } $token.accessToken | Should Not BeNullOrEmpty $token.tokenType | Should Be 'Bearer' $token.expiresIn | Should BeGreaterThan 0 $token.scope | Should Be 'configuration.read' } It 'allows bearer-token API calls without Windows authentication' { $token = Invoke-TestApi -Method POST -Path 'auth/token' -Body @{ clientId = $ClientId clientSecret = $ClientSecret scope = 'configuration.read' } $definitions = @(Invoke-TestApi -Method GET -Path 'configuration-definitions?kind=Parameter' -BearerToken $token.accessToken) @($definitions).Count | Should BeGreaterThan 0 @($definitions | Where-Object { $_.kind -ne 'Parameter' }).Count | Should Be 0 } It 'rejects bearer-token API calls without the required scope' { $token = Invoke-TestApi -Method POST -Path 'auth/token' -Body @{ clientId = $ClientId clientSecret = $ClientSecret scope = 'deployment.read' } $statusCode = Invoke-TestApiExpectFailure -Method GET -Path 'configuration-definitions?kind=Parameter' -BearerToken $token.accessToken @(401, 403) -contains $statusCode | Should Be $true } It 'creates, lists and revokes managed tokens through the token API' { $adminToken = Invoke-TestApi -Method POST -Path 'auth/token' -Body @{ clientId = $ClientId clientSecret = $ClientSecret scope = 'token.manage token.admin deployment.read' } $managedToken = Invoke-TestApi -Method POST -Path 'tokens/my' -BearerToken $adminToken.accessToken -Body @{ name = 'Pester managed token' scope = 'deployment.read' } $managedToken.id | Should Not BeNullOrEmpty $managedToken.accessToken | Should Not BeNullOrEmpty $managedToken.scope | Should Be 'deployment.read' $myTokens = @(Invoke-TestApi -Method GET -Path 'tokens/my' -BearerToken $adminToken.accessToken) @($myTokens | Where-Object { $_.id -eq $managedToken.id }).Count | Should Be 1 $allTokens = @(Invoke-TestApi -Method GET -Path 'tokens/admin' -BearerToken $adminToken.accessToken) @($allTokens | Where-Object { $_.id -eq $managedToken.id }).Count | Should Be 1 $deletedToken = Invoke-TestApi -Method DELETE -Path ('tokens/my/{0}' -f $managedToken.id) -BearerToken $adminToken.accessToken $deletedToken.id | Should Be $managedToken.id $deletedToken.revokedAt | Should Not BeNullOrEmpty $deletedToken.isActive | Should Be $false $myTokensAfterDelete = @(Invoke-TestApi -Method GET -Path 'tokens/my' -BearerToken $adminToken.accessToken) $myTokenAfterDelete = $myTokensAfterDelete | Where-Object { [string]$_.id -eq [string]$managedToken.id } | Select-Object -First 1 if ($myTokenAfterDelete) { $myTokenAfterDelete.isActive | Should Be $false } $adminTokensAfterDelete = @(Invoke-TestApi -Method GET -Path 'tokens/admin' -BearerToken $adminToken.accessToken) $revokedToken = $adminTokensAfterDelete | Where-Object { [string]$_.id -eq [string]$managedToken.id } | Select-Object -First 1 if ($revokedToken) { $revokedToken.revokedAt | Should Not BeNullOrEmpty $revokedToken.isActive | Should Be $false } } }