Private/KeycloakRegistration.ps1
|
# Copyright (c) Tetron Limited. All rights reserved. # Licensed under the Tetron Commercial License. See LICENSE file in the project root. # Registering JIM with Keycloak (#2364), for Register-JIMIdentityProvider -Provider Keycloak. # # Keycloak is configured through its admin REST API with Invoke-RestMethod, so nothing beyond PowerShell is needed. # As for AD FS, what the realm holds is read once, the plan is worked out here (a pure function, tested without # Keycloak), and each planned change is then applied on its own, so -WhatIf and -Confirm see each change separately. # The client scope that puts JIM's audience in an access token: JIM_SSO_API_SCOPE. $script:JIMKeycloakApiScope = 'jim-api' $script:JIMKeycloakAudienceMapper = 'jim-api-audience' # Keycloak's own scopes JIM signs people in with, as default scopes of both clients. basic carries sub from Keycloak # 25; a realm from an earlier version has no basic scope, and puts sub in every token anyway. $script:JIMKeycloakDefaultScopes = @('basic', 'profile', 'email') $script:JIMKeycloakScopeKindText = @{ Default = 'a default scope'; Optional = 'an optional scope' } # What each SetClient property means, for -WhatIf and the Changes JIM returns. $script:JIMKeycloakClientPropertyText = [ordered]@{ enabled = { 'enable it' } publicClient = { param($v) if ($v) { 'make it a public client' } else { 'make it a confidential client, with a client secret' } } standardFlowEnabled = { 'turn on the standard flow' } implicitFlowEnabled = { 'turn off the implicit flow' } directAccessGrantsEnabled = { 'turn off direct access grants' } serviceAccountsEnabled = { 'turn off service accounts' } redirectUris = { param($v) "set the redirect URIs to $($v -join ', ')" } PostLogoutRedirectUris = { param($v) "set the post-logout redirect URIs to $($v -join ', ')" } webOrigins = { param($v) "set the web origins to $($v -join ', ')" } Pkce = { 'require PKCE with S256' } } function ConvertTo-JIMKeycloakAuthority { <# .SYNOPSIS Splits a Keycloak authority, https://server/realms/realm, into the server's address and the realm. #> [CmdletBinding()] param([Parameter(Mandatory)][string]$Authority) $uri = $null if (-not [uri]::TryCreate($Authority, [UriKind]::Absolute, [ref]$uri) -or -not ($uri.Scheme -eq 'https' -or ($uri.Scheme -eq 'http' -and $uri.IsLoopback))) { throw "The Keycloak authority must be an absolute https address, such as https://sso.example.com/realms/jim, not '$Authority'." } $trimmed = $Authority.TrimEnd('/') $at = $trimmed.LastIndexOf('/realms/', [StringComparison]::Ordinal) $realm = if ($at -ge 0) { $trimmed.Substring($at + '/realms/'.Length) } else { '' } if (-not $realm -or $realm.Contains('/')) { throw "The Keycloak authority must name the realm JIM signs people in from, as https://sso.example.com/realms/jim does, not '$Authority'." } [PSCustomObject]@{ Authority = $trimmed ServerUrl = $trimmed.Substring(0, $at) Realm = $realm } } function Get-JIMKeycloakAdminToken { <# .SYNOPSIS Signs in to Keycloak to use its admin REST API: as an administrator through admin-cli, or as a client's service account. #> [CmdletBinding()] param( [Parameter(Mandatory)][string]$ServerUrl, [Parameter(Mandatory)][string]$AdminRealm, [pscredential]$Credential, [pscredential]$ClientCredential ) $body = if ($ClientCredential) { @{ grant_type = 'client_credentials'; client_id = $ClientCredential.UserName client_secret = $ClientCredential.GetNetworkCredential().Password } } else { @{ grant_type = 'password'; client_id = 'admin-cli' username = $Credential.UserName; password = $Credential.GetNetworkCredential().Password } } $uri = "$ServerUrl/realms/$([uri]::EscapeDataString($AdminRealm))/protocol/openid-connect/token" try { $response = Invoke-RestMethod -Method Post -Uri $uri -Body $body -ContentType 'application/x-www-form-urlencoded' -ErrorAction Stop } catch { $who = if ($ClientCredential) { "the client '$($ClientCredential.UserName)'" } else { "'$($Credential.UserName)'" } throw "Keycloak did not sign in $who in the realm '$AdminRealm': $($_.Exception.Message)" } [PSCustomObject]@{ AccessToken = [string]$response.access_token ExpiresAt = [datetime]::UtcNow.AddSeconds([int]$response.expires_in) } } function Invoke-JIMKeycloakAdminApi { <# .SYNOPSIS Calls the admin REST API for the connection's realm, signing in again when the token is about to expire. .PARAMETER Path The path under /admin/realms/{realm}, such as /clients. #> [CmdletBinding()] param( [Parameter(Mandatory)][hashtable]$Connection, [ValidateSet('Get', 'Post', 'Put', 'Delete')][string]$Method = 'Get', [Parameter(Mandatory)][string]$Path, [object]$Body ) # admin-cli tokens in the master realm last a minute, which a run with -Confirm can outlast. if ($Connection.Token.ExpiresAt -lt [datetime]::UtcNow.AddSeconds(15)) { $Connection.Token = Get-JIMKeycloakAdminToken -ServerUrl $Connection.ServerUrl -AdminRealm $Connection.AdminRealm ` -Credential $Connection.Credential -ClientCredential $Connection.ClientCredential } $request = @{ Method = $Method Uri = "$($Connection.ServerUrl)/admin/realms/$([uri]::EscapeDataString($Connection.Realm))$Path" Headers = @{ Authorization = "Bearer $($Connection.Token.AccessToken)" } ErrorAction = 'Stop' } if ($PSBoundParameters.ContainsKey('Body')) { $request.Body = ConvertTo-Json -InputObject $Body -Depth 10 -Compress $request.ContentType = 'application/json' } try { # Invoke-RestMethod returns a JSON array as one object; hand back its items, as callers filter them. $response = Invoke-RestMethod @request $response } catch { $status = [int]$_.Exception.Response.StatusCode if ($status -eq 403) { throw "Keycloak refused $Method $Path in the realm '$($Connection.Realm)'. The account needs the realm-admin role of that realm's realm-management client, or the master realm's admin role." } if ($status -eq 404 -and $Path -eq '/clients') { throw "Keycloak has no realm '$($Connection.Realm)'. Create it, or give the authority of the realm JIM signs people in from." } throw } } function Get-JIMKeycloakRegistrationState { <# .SYNOPSIS Reads what the realm holds that JIM's registration depends on: its clients, client scopes and default scopes. .PARAMETER SecretOf The client whose secret to read too, for comparing with one given. No other client's secret is read. #> [CmdletBinding()] param( [Parameter(Mandatory)][hashtable]$Connection, [string]$SecretOf ) $clients = [System.Collections.Generic.List[object]]::new() # Keycloak returns every client when asked without first and max. foreach ($client in @(Invoke-JIMKeycloakAdminApi -Connection $Connection -Path '/clients')) { $defaults = $client.defaultClientScopes $optionals = $client.optionalClientScopes if ($null -eq $defaults -or $null -eq $optionals) { $defaults = @(Invoke-JIMKeycloakAdminApi -Connection $Connection -Path "/clients/$($client.id)/default-client-scopes").name $optionals = @(Invoke-JIMKeycloakAdminApi -Connection $Connection -Path "/clients/$($client.id)/optional-client-scopes").name } $attributes = $client.attributes $postLogout = [string]$attributes.'post.logout.redirect.uris' $clients.Add([PSCustomObject]@{ ClientId = [string]$client.clientId PublicClient = [bool]$client.publicClient Enabled = [bool]$client.enabled StandardFlowEnabled = [bool]$client.standardFlowEnabled ImplicitFlowEnabled = [bool]$client.implicitFlowEnabled DirectAccessGrantsEnabled = [bool]$client.directAccessGrantsEnabled ServiceAccountsEnabled = [bool]$client.serviceAccountsEnabled RedirectUris = @($client.redirectUris) WebOrigins = @($client.webOrigins) PostLogoutRedirectUris = @(if ($postLogout) { $postLogout -split '##' }) Pkce = [string]$attributes.'pkce.code.challenge.method' DefaultClientScopes = @($defaults) OptionalClientScopes = @($optionals) Secret = if ($SecretOf -and $client.clientId -ceq $SecretOf) { [string](Invoke-JIMKeycloakAdminApi -Connection $Connection -Path "/clients/$($client.id)/client-secret").value } }) } $scopes = @(Invoke-JIMKeycloakAdminApi -Connection $Connection -Path '/client-scopes' | ForEach-Object { [PSCustomObject]@{ Name = [string]$_.name Protocol = [string]$_.protocol AudienceMappers = @($_.protocolMappers | Where-Object { $_.protocolMapper -eq 'oidc-audience-mapper' } | ForEach-Object { [PSCustomObject]@{ Name = [string]$_.name IncludedClientAudience = [string]$_.config.'included.client.audience' AccessTokenClaim = [string]$_.config.'access.token.claim' IdTokenClaim = [string]$_.config.'id.token.claim' } }) } }) [PSCustomObject]@{ Clients = $clients.ToArray() ClientScopes = $scopes RealmDefaultScopes = @(@(Invoke-JIMKeycloakAdminApi -Connection $Connection -Path '/default-default-client-scopes').name) RealmOptionalScopes = @(@(Invoke-JIMKeycloakAdminApi -Connection $Connection -Path '/default-optional-client-scopes').name) } } function New-JIMKeycloakChange { param([string]$Action, [string]$Target, [string]$Description, [hashtable]$Arguments) [PSCustomObject]@{ Action = $Action; Target = $Target; Description = $Description; Arguments = $Arguments } } function Get-JIMKeycloakRegistrationPlan { <# .SYNOPSIS Works out what has to change in the realm for JIM's registration to be as it should be. .PARAMETER Url JIM's addresses, as people reach it. .PARAMETER ClientId The portal's client, JIM_SSO_CLIENT_ID. .PARAMETER PublicClientId The PowerShell module's client, JIM_SSO_PUBLIC_CLIENT_ID, which the API reference's Try It signs in with too. .PARAMETER Current What the realm holds, as Get-JIMKeycloakRegistrationState reads it, with the portal's secret when -ClientSecret is given. .PARAMETER ResetClientSecret Plan a new client secret for the portal even if it has one. .PARAMETER ClientSecret The portal's client secret, in place of one Keycloak makes. .OUTPUTS The client identifiers and API scope, and the changes in the order they must be made. #> [CmdletBinding()] param( [Parameter(Mandatory)][string[]]$Url, [Parameter(Mandatory)][string]$ClientId, [Parameter(Mandatory)][string]$PublicClientId, [Parameter(Mandatory)][object]$Current, [switch]$ResetClientSecret, [securestring]$ClientSecret ) $baseUrls = @($Url | ForEach-Object { $_.TrimEnd('/') }) # The API reference's Try It signs in from the browser (#2594): Keycloak redirects back to the page itself, and # answers its token request only from an origin the client lists. The page is /api/reference/, with the slash: # Scalar redirects the bare path there and sends the page's own URL as redirect_uri. $tryItRedirects = @($baseUrls | ForEach-Object { "$_/api/reference/" }) $tryItOrigins = @($baseUrls | ForEach-Object { ([uri]$_).GetLeftPart([UriPartial]::Authority) } | Select-Object -Unique) $apiScope = $script:JIMKeycloakApiScope $changes = [System.Collections.Generic.List[object]]::new() $available = @($Current.ClientScopes.Name) # The API scope: its audience is the portal's client, which JIM requires in every access token's aud. $scope = $Current.ClientScopes | Where-Object Name -eq $apiScope | Select-Object -First 1 if (-not $scope) { $changes.Add((New-JIMKeycloakChange -Action 'NewClientScope' -Target $apiScope ` -Description "Create the client scope, putting $ClientId in the access token's audience" ` -Arguments @{ Name = $apiScope; AudienceClientId = $ClientId })) } else { if ($scope.Protocol -ne 'openid-connect') { throw "The realm has a client scope called '$apiScope' for $($scope.Protocol), not OpenID Connect. Rename or remove it, and run this again." } $mappers = @($scope.AudienceMappers) $mapper = @($mappers | Where-Object Name -eq $script:JIMKeycloakAudienceMapper) + $mappers | Select-Object -First 1 if (-not $mapper) { $changes.Add((New-JIMKeycloakChange -Action 'NewAudienceMapper' -Target $apiScope ` -Description "Put $ClientId in the access token's audience" ` -Arguments @{ Scope = $apiScope; AudienceClientId = $ClientId })) } elseif ($mapper.IncludedClientAudience -cne $ClientId -or $mapper.AccessTokenClaim -ne 'true') { $changes.Add((New-JIMKeycloakChange -Action 'SetAudienceMapper' -Target $apiScope ` -Description "Put $ClientId in the access token's audience, in place of '$($mapper.IncludedClientAudience)'" ` -Arguments @{ Scope = $apiScope; MapperName = $mapper.Name; AudienceClientId = $ClientId })) } } # A realm default scope is added to every new client, which would let any of them ask for JIM's audience (#2097). foreach ($kind in 'Default', 'Optional') { $realmScopes = if ($kind -eq 'Default') { $Current.RealmDefaultScopes } else { $Current.RealmOptionalScopes } if ($apiScope -in @($realmScopes)) { $changes.Add((New-JIMKeycloakChange -Action 'RemoveRealmDefaultScope' -Target $apiScope ` -Description "Stop adding it to every new client as a realm $($kind.ToLowerInvariant()) scope" ` -Arguments @{ Scope = $apiScope; Kind = $kind })) } } $wanted = @( @{ ClientId = $ClientId; Label = 'the portal'; Name = 'JIM'; Optional = @($apiScope) Properties = [ordered]@{ enabled = $true; publicClient = $false; standardFlowEnabled = $true; implicitFlowEnabled = $false directAccessGrantsEnabled = $false; serviceAccountsEnabled = $false redirectUris = @($baseUrls | ForEach-Object { "$_/signin-oidc" }) PostLogoutRedirectUris = @($baseUrls | ForEach-Object { "$_/signout-callback-oidc" }); Pkce = 'S256' } }, @{ # offline_access gives the module a refresh token, to renew its sign-in without the browser. ClientId = $PublicClientId; Label = 'the PowerShell module'; Name = 'JIM PowerShell module' Optional = @($apiScope, 'offline_access') Properties = [ordered]@{ enabled = $true; publicClient = $true; standardFlowEnabled = $true; implicitFlowEnabled = $false directAccessGrantsEnabled = $false; serviceAccountsEnabled = $false redirectUris = @(Get-JIMLoopbackRedirectUri) + $tryItRedirects; webOrigins = $tryItOrigins; Pkce = 'S256' } } ) foreach ($entry in $wanted) { $client = $Current.Clients | Where-Object ClientId -ceq $entry.ClientId | Select-Object -First 1 $properties = $entry.Properties if (-not $client) { $description = if ($properties.publicClient) { "Add $($entry.Label) as a public client" } elseif ($ClientSecret) { "Add $($entry.Label) as a confidential client, with the client secret given" } else { "Add $($entry.Label) as a confidential client, with a new client secret" } $arguments = @{ ClientId = $entry.ClientId; Name = $entry.Name; Properties = $properties } if ($ClientSecret -and -not $properties.publicClient) { $arguments.Secret = $ClientSecret } $changes.Add((New-JIMKeycloakChange -Action 'NewClient' -Target $entry.ClientId -Description $description -Arguments $arguments)) $currentDefaults = @() $currentOptionals = @() } else { $drift = [ordered]@{} foreach ($key in $properties.Keys) { $have = $client.$key $want = $properties[$key] $same = if ($want -is [array]) { Test-JIMSameSet $have $want } else { "$have" -eq "$want" } if (-not $same) { $drift[$key] = $want } } if ($drift.Count -gt 0) { $description = ($drift.Keys | ForEach-Object { & $script:JIMKeycloakClientPropertyText[$_] $drift[$_] }) -join ', ' $changes.Add((New-JIMKeycloakChange -Action 'SetClient' -Target $entry.ClientId ` -Description ($description.Substring(0, 1).ToUpperInvariant() + $description.Substring(1)) ` -Arguments @{ ClientId = $entry.ClientId; Properties = [hashtable]$drift })) } if ($ResetClientSecret -and -not $properties.publicClient) { $changes.Add((New-JIMKeycloakChange -Action 'ResetClientSecret' -Target $entry.ClientId -Description 'Replace the client secret' ` -Arguments @{ ClientId = $entry.ClientId })) } if ($ClientSecret -and -not $properties.publicClient -and $client.Secret -cne [System.Net.NetworkCredential]::new('', $ClientSecret).Password) { $changes.Add((New-JIMKeycloakChange -Action 'SetClientSecret' -Target $entry.ClientId -Description 'Set the client secret to the one given' ` -Arguments @{ ClientId = $entry.ClientId; Secret = $ClientSecret })) } $currentDefaults = @($client.DefaultClientScopes) $currentOptionals = @($client.OptionalClientScopes) } $links = @( @($script:JIMKeycloakDefaultScopes | Where-Object { $_ -in $available } | ForEach-Object { @{ Scope = $_; Kind = 'Default' } }) @($entry.Optional | Where-Object { $_ -eq $apiScope -or $_ -in $available } | ForEach-Object { @{ Scope = $_; Kind = 'Optional' } }) ) foreach ($link in $links) { $have = if ($link.Kind -eq 'Default') { $currentDefaults } else { $currentOptionals } $other = if ($link.Kind -eq 'Default') { $currentOptionals } else { $currentDefaults } if ($link.Scope -in $have) { continue } if ($link.Scope -in $other) { $otherKind = if ($link.Kind -eq 'Default') { 'Optional' } else { 'Default' } $changes.Add((New-JIMKeycloakChange -Action 'RemoveClientScope' -Target $entry.ClientId ` -Description "Remove $($link.Scope) as $($script:JIMKeycloakScopeKindText[$otherKind])" ` -Arguments @{ ClientId = $entry.ClientId; Scope = $link.Scope; Kind = $otherKind })) } $changes.Add((New-JIMKeycloakChange -Action 'AddClientScope' -Target $entry.ClientId ` -Description "Add $($link.Scope) as $($script:JIMKeycloakScopeKindText[$link.Kind])" ` -Arguments @{ ClientId = $entry.ClientId; Scope = $link.Scope; Kind = $link.Kind })) } } # Who may ask for JIM's audience: JIM's two clients, and nobody else (#2097). foreach ($client in @($Current.Clients | Where-Object { $_.ClientId -cnotin $ClientId, $PublicClientId })) { foreach ($kind in 'Default', 'Optional') { $linked = if ($kind -eq 'Default') { $client.DefaultClientScopes } else { $client.OptionalClientScopes } if ($apiScope -in @($linked)) { $changes.Add((New-JIMKeycloakChange -Action 'RemoveClientScope' -Target $client.ClientId ` -Description "Remove $apiScope, which only JIM's clients may ask for" ` -Arguments @{ ClientId = $client.ClientId; Scope = $apiScope; Kind = $kind })) } } } [PSCustomObject]@{ ClientId = $ClientId PublicClientId = $PublicClientId ApiScope = $apiScope Changes = $changes.ToArray() } } function Get-JIMKeycloakClientUuid { # Keycloak addresses a client by its internal id, not its client ID. param([hashtable]$Connection, [string]$ClientId) $client = @(Invoke-JIMKeycloakAdminApi -Connection $Connection -Path "/clients?clientId=$([uri]::EscapeDataString($ClientId))") | Where-Object clientId -ceq $ClientId | Select-Object -First 1 if (-not $client) { throw "Keycloak has no client '$ClientId'." } [string]$client.id } function Get-JIMKeycloakClientScope { param([hashtable]$Connection, [string]$Name) $scope = @(Invoke-JIMKeycloakAdminApi -Connection $Connection -Path '/client-scopes') | Where-Object name -eq $Name | Select-Object -First 1 if (-not $scope) { throw "Keycloak has no client scope '$Name'." } $scope } function New-JIMKeycloakAudienceMapper { param([string]$AudienceClientId) @{ name = $script:JIMKeycloakAudienceMapper; protocol = 'openid-connect'; protocolMapper = 'oidc-audience-mapper' config = @{ 'included.client.audience' = $AudienceClientId; 'access.token.claim' = 'true'; 'id.token.claim' = 'false' 'introspection.token.claim' = 'true' } } } function Set-JIMKeycloakClientProperty { # Applies planned properties to a client representation: most are its own fields, two are attributes. param([object]$Representation, [System.Collections.IDictionary]$Properties) if (-not $Representation.attributes) { $Representation | Add-Member -NotePropertyName attributes -NotePropertyValue ([PSCustomObject]@{}) -Force } foreach ($key in $Properties.Keys) { $value = $Properties[$key] switch ($key) { 'PostLogoutRedirectUris' { $Representation.attributes | Add-Member -NotePropertyName 'post.logout.redirect.uris' -NotePropertyValue ($value -join '##') -Force } 'Pkce' { $Representation.attributes | Add-Member -NotePropertyName 'pkce.code.challenge.method' -NotePropertyValue $value -Force } default { $Representation | Add-Member -NotePropertyName $key -NotePropertyValue $value -Force } } } } function Invoke-JIMKeycloakRegistrationChange { <# .SYNOPSIS Makes one planned change in the realm. Returns the client secret when the change made one. #> [CmdletBinding()] [OutputType([string])] param( [Parameter(Mandatory)][hashtable]$Connection, [Parameter(Mandatory)][object]$Change ) $arguments = $Change.Arguments switch ($Change.Action) { 'NewClientScope' { # Created through the API, a scope is no realm default: the Type None the guide sets in the console. $null = Invoke-JIMKeycloakAdminApi -Connection $Connection -Method Post -Path '/client-scopes' -Body @{ name = $arguments.Name; protocol = 'openid-connect'; description = "JIM's API" attributes = @{ 'include.in.token.scope' = 'true'; 'display.on.consent.screen' = 'false' } protocolMappers = @(New-JIMKeycloakAudienceMapper -AudienceClientId $arguments.AudienceClientId) } } 'NewAudienceMapper' { $scope = Get-JIMKeycloakClientScope -Connection $Connection -Name $arguments.Scope $null = Invoke-JIMKeycloakAdminApi -Connection $Connection -Method Post -Path "/client-scopes/$($scope.id)/protocol-mappers/models" ` -Body (New-JIMKeycloakAudienceMapper -AudienceClientId $arguments.AudienceClientId) } 'SetAudienceMapper' { $scope = Get-JIMKeycloakClientScope -Connection $Connection -Name $arguments.Scope $mapper = $scope.protocolMappers | Where-Object name -eq $arguments.MapperName | Select-Object -First 1 $mapper.config | Add-Member -NotePropertyName 'included.client.audience' -NotePropertyValue $arguments.AudienceClientId -Force $mapper.config | Add-Member -NotePropertyName 'access.token.claim' -NotePropertyValue 'true' -Force $null = Invoke-JIMKeycloakAdminApi -Connection $Connection -Method Put -Path "/client-scopes/$($scope.id)/protocol-mappers/models/$($mapper.id)" -Body $mapper } 'RemoveRealmDefaultScope' { $scope = Get-JIMKeycloakClientScope -Connection $Connection -Name $arguments.Scope $list = if ($arguments.Kind -eq 'Default') { 'default-default-client-scopes' } else { 'default-optional-client-scopes' } $null = Invoke-JIMKeycloakAdminApi -Connection $Connection -Method Delete -Path "/$list/$($scope.id)" } 'NewClient' { $representation = [PSCustomObject]@{ clientId = $arguments.ClientId; name = $arguments.Name; protocol = 'openid-connect' clientAuthenticatorType = 'client-secret'; attributes = [PSCustomObject]@{} } Set-JIMKeycloakClientProperty -Representation $representation -Properties $arguments.Properties if ($arguments.Secret) { $representation | Add-Member -NotePropertyName secret -NotePropertyValue ([System.Net.NetworkCredential]::new('', $arguments.Secret).Password) } $null = Invoke-JIMKeycloakAdminApi -Connection $Connection -Method Post -Path '/clients' -Body $representation # Keycloak makes a confidential client's secret itself, unless it is given one. if (-not $arguments.Properties.publicClient) { $uuid = Get-JIMKeycloakClientUuid -Connection $Connection -ClientId $arguments.ClientId [string](Invoke-JIMKeycloakAdminApi -Connection $Connection -Path "/clients/$uuid/client-secret").value } } 'SetClient' { # Keycloak replaces a client with what it is sent, so send everything it holds with the changes made. $uuid = Get-JIMKeycloakClientUuid -Connection $Connection -ClientId $arguments.ClientId $representation = Invoke-JIMKeycloakAdminApi -Connection $Connection -Path "/clients/$uuid" Set-JIMKeycloakClientProperty -Representation $representation -Properties $arguments.Properties $null = Invoke-JIMKeycloakAdminApi -Connection $Connection -Method Put -Path "/clients/$uuid" -Body $representation } 'ResetClientSecret' { $uuid = Get-JIMKeycloakClientUuid -Connection $Connection -ClientId $arguments.ClientId [string](Invoke-JIMKeycloakAdminApi -Connection $Connection -Method Post -Path "/clients/$uuid/client-secret").value } 'SetClientSecret' { $uuid = Get-JIMKeycloakClientUuid -Connection $Connection -ClientId $arguments.ClientId $representation = Invoke-JIMKeycloakAdminApi -Connection $Connection -Path "/clients/$uuid" $secret = [System.Net.NetworkCredential]::new('', $arguments.Secret).Password $representation | Add-Member -NotePropertyName secret -NotePropertyValue $secret -Force $null = Invoke-JIMKeycloakAdminApi -Connection $Connection -Method Put -Path "/clients/$uuid" -Body $representation $secret } { $_ -in 'AddClientScope', 'RemoveClientScope' } { $uuid = Get-JIMKeycloakClientUuid -Connection $Connection -ClientId $arguments.ClientId $scope = Get-JIMKeycloakClientScope -Connection $Connection -Name $arguments.Scope $list = if ($arguments.Kind -eq 'Default') { 'default-client-scopes' } else { 'optional-client-scopes' } $method = if ($Change.Action -eq 'AddClientScope') { 'Put' } else { 'Delete' } $null = Invoke-JIMKeycloakAdminApi -Connection $Connection -Method $method -Path "/clients/$uuid/$list/$($scope.id)" } default { throw "Unknown Keycloak change '$($Change.Action)'." } } } |