keycloakTokenManager.ps1
class KeycloakTokenManagerVersion { [int]$Major = 1 [int]$Minor = 4 [int]$Build = 9 [int]$Revision = 861 [string]ToString() { return "keycloakTokenManager v$($this.Major).$($this.Minor).$($this.Build).$($this.Revision)" } } class KeycloakTokenManager { hidden [uri]$realmUri = [string]::Empty hidden [uri]$realmAdminUri = [string]::Empty hidden [PSCustomObject]$realmWellKnownEndpoints = $null hidden [string]$realmClientName = "admin-cli" hidden [string]$realmClientSecret = [string]::Empty hidden [string]$realmName = [string]::Empty hidden [string]$userName = [string]::Empty hidden [string]$password = [string]::Empty hidden [string]$grant_type = "password" hidden [hashtable]$impersonations = @{} hidden [KeycloakTokenManagerVersion]$version = [KeycloakTokenManagerVersion]::new() [string]$default_scope = "openid email profile" [KeycloakTokenManagerVersion]Version() { return $this.version } [string]getVersion() { return $this.version.ToString() } hidden [void]getKeycloakUris() { $local:keycloakHostUrl = "$($this.realmUri.Scheme)://$($this.realmUri.Host)" $local:segments = ($this.realmUri.Segments) [int]$local:realmsIndex = $local:segments.IndexOf("realms/") $this.realmName = $local:segments[$local:realmsIndex + 1].TrimEnd("/") $this.realmUri = "$($local:keycloakHostUrl)$($local:segments[0..$($local:realmsIndex + 1)] -join '')".TrimEnd("/") $this.realmAdminUri = "$($local:keycloakHostUrl)$($local:segments[0..$($local:realmsIndex - 1)] -join '')admin/$($local:segments[$($local:realmsIndex)..$($local:realmsIndex + 1)] -join '')".TrimEnd("/") $local:wellKnownEndpoints = "$($this.realmUri)/.well-known/openid-configuration" $local:result = $null if ($global:PSVersionTable.PSVersion.Major -eq 7) { $local:result = Invoke-WebRequest -Method Get -Uri $local:wellKnownEndpoints -SkipHttpErrorCheck } else { try { $local:result = Invoke-WebRequest -Method Get -Uri $local:wellKnownEndpoints } catch { $local:result = $_ } } if ($local:result.StatusCode -eq [System.Net.HttpStatusCode]::OK) { $this.realmWellKnownEndpoints = $local:result.content | ConvertFrom-Json # -Depth 100 } else { throw "Unable to get data from: $($local:wellKnownEndpoints)",$local:result.Exception } } hidden [PSCustomObject]parseJWTtoken([string]$token) { if (!$token.Contains(".") -or !$token.StartsWith("eyJ")) { throw "Invalid token" } $local:tokenheader = $token.Split(".")[0].Replace('-', '+').Replace('_', '/') while ($local:tokenheader.Length % 4) { $local:tokenheader += "=" } $local:tokenPayload = $token.Split(".")[1].Replace('-', '+').Replace('_', '/') while ($local:tokenPayload.Length % 4) { $local:tokenPayload += "=" } $local:tokenByteArray = [System.Convert]::FromBase64String($local:tokenPayload) $local:tokenArray = [System.Text.Encoding]::ASCII.GetString($local:tokenByteArray) $local:tokenObj = $local:tokenArray | ConvertFrom-Json return $local:tokenObj } hidden [bool]isTokenValid($decodedToken) { [datetime]$local:now = (Get-Date).ToUniversalTime() [datetime]$local:tokenExpiresAt = (Get-date 01-01-1970).AddSeconds($decodedToken.exp).AddSeconds(-10) return ($local:tokenExpiresAt -gt $local:now) } hidden [void]requestNewToken() { $this.requestNewToken($this.userName) } hidden [void]requestNewToken([string]$userName) { $local:body = $null if ($userName -eq $this.userName) { switch ($this.grant_type) { "authorization_code" { $this.requestNewTokenByBrowser() return } "password" { $local:body = New-Object "System.Collections.Generic.Dictionary[[String],[String]]" $local:body.Add("grant_type", "password") $local:body.Add("client_id", $($this.realmClientName)) if (-not [string]::IsNullOrEmpty($this.realmClientSecret)) { $local:body.Add("client_secret", $($this.realmClientSecret)) } if (-not [string]::IsNullOrEmpty($this.default_scope)) { $local:body.Add("scope", $this.default_scope) } $local:body.Add("username", $($this.userName)) $local:body.Add("password", $($this.password)) } } } else { $local:impersonationAccessToken = $this.getAccessToken() if ($this.impersonations[$this.userName].decodedAccessToken.resource_access.'realm-management'.roles.Contains("impersonation")) { $local:body = New-Object "System.Collections.Generic.Dictionary[[String],[String]]" $local:body.Add("grant_type", "urn:ietf:params:oauth:grant-type:token-exchange") $local:body.Add("client_id", $($this.realmClientName)) if (-not [string]::IsNullOrEmpty($this.realmClientSecret)) { $local:body.Add("client_secret", $($this.realmClientSecret)) } if (-not [string]::IsNullOrEmpty($this.default_scope)) { $local:body.Add("scope", $this.default_scope) } $local:body.Add("requested_subject", $($userName)) $local:body.Add("subject_token", $($local:impersonationAccessToken)) } else { throw "ERROR: User '$($this.userName)' does not own impersonation role." } } $local:result = $null if ($global:PSVersionTable.PSVersion.Major -eq 7) { $local:result = Invoke-WebRequest -Method Post -Uri $this.realmWellKnownEndpoints.token_endpoint -Body $local:body -SkipHttpErrorCheck } else { try { $local:result = Invoke-WebRequest -Method Post -Uri $this.realmWellKnownEndpoints.token_endpoint -Body $local:body } catch { $local:result = $_.Exception.Response } } if ($local:result.StatusCode -eq [System.Net.HttpStatusCode]::OK) { $local:tokenObject = ($local:result.Content | ConvertFrom-Json) $local:impersonation = @{ tokenObject = $local:tokenObject decodedAccessToken = $this.parseJWTtoken($local:tokenObject.access_token) decodedRefreshToken = $this.parseJWTtoken($local:tokenObject.refresh_token) } $this.impersonations[$userName] = $local:impersonation } else { if ($userName -eq $this.userName) { throw "ERROR: Failed to authenticate '$($userName)'" } else { throw "ERROR: Failed to impersonate '$($userName)'" } } } hidden [void]requestNewTokenByBrowser() { [uri]$local:redirect_url = "http://localhost:8081" try { $local:httpsrvlsnr = New-Object System.Net.HttpListener $local:httpsrvlsnr.Prefixes.Add($local:redirect_url.AbsoluteUri) $local:httpsrvlsnr.Start() } catch { throw $_ # throw $_.ErrorDetails.Message } $local:authorizationUrl = "$($this.realmWellKnownEndpoints.authorization_endpoint)" # $local:authorizationUrl += "?redirect_uri=$([System.Web.HttpUtility]::UrlEncode($local:redirect_url))" # $local:authorizationUrl += "?redirect_uri=$([uri]::EscapeDataString($local:redirect_url))" $local:authorizationUrl += "?redirect_uri=$([uri]::EscapeDataString($local:redirect_url))" $local:authorizationUrl += "&client_id=$($this.realmClientName)" $local:authorizationUrl += "&response_type=code" Start-Process -FilePath $local:authorizationUrl while ($local:httpsrvlsnr.IsListening) { try { $local:ctx = $local:httpsrvlsnr.GetContext(); $local:ht_query = @{} $local:queryString = $local:ctx.Request.Url.Query.TrimStart("?") foreach ($queryParameter in ($local:queryString -split '&')) { $local:keyValuePair = $($local:queryParameter + '=') -split '=' $local:key = [uri]::UnescapeDataString($local:keyValuePair[0]).Trim() $local:ht_query[$key] = [uri]::UnescapeDataString($local:keyValuePair[1]) } if ($local:ht_query.Contains("code")) { $local:body = New-Object "System.Collections.Generic.Dictionary[[String],[String]]" $local:body.Add("grant_type", "authorization_code") $local:body.Add("code", $local:ht_query["code"]) $local:body.Add("redirect_uri", $local:redirect_url) $local:body.Add("client_id", $this.realmClientName) if (-not [string]::IsNullOrEmpty($this.realmClientSecret)) { $local:body.Add("client_secret", $($this.realmClientSecret)) } if (-not [string]::IsNullOrEmpty($this.default_scope)) { $local:body.Add("scope", $this.default_scope) } $local:result = $null if ($global:PSVersionTable.PSVersion.Major -eq 7) { $local:result = Invoke-WebRequest -Method Post -Uri $($this.realmWellKnownEndpoints.token_endpoint) -Body $local:body -SkipHttpErrorCheck } else { try { $local:result = Invoke-WebRequest -Method Post -Uri $($this.realmWellKnownEndpoints.token_endpoint) -Body $local:body } catch { $local:result = $_.Exception.Response } } if ($local:result.StatusCode -eq [System.Net.HttpStatusCode]::OK) { [byte[]]$local:buffer = $null $local:buffer = [System.Text.Encoding]::UTF8.GetBytes("<html><h1>You can now return to the application.</h1></html>"); $local:ctx.Response.ContentLength64 = $local:buffer.Length; $local:ctx.Response.OutputStream.WriteAsync($local:buffer, 0, $local:buffer.Length) $local:httpsrvlsnr.Stop() $local:tokenObject = ($local:result.Content | ConvertFrom-Json) $local:impersonation = @{ tokenObject = $local:tokenObject decodedAccessToken = $this.parseJWTtoken($local:tokenObject.access_token) decodedRefreshToken = $this.parseJWTtoken($local:tokenObject.refresh_token) } $this.userName = $local:impersonation.decodedAccessToken.preferred_username $this.impersonations[$this.userName] = $local:impersonation } else { throw $local:result.Content } } else { throw "ERROR: Failed to receive authorization_code" } } catch [System.Net.HttpListenerException] { throw $_ } finally { $local:httpsrvlsnr.Stop() $local:httpsrvlsnr.dispose() } } $local:httpsrvlsnr.dispose() } hidden [void]refreshToken([string]$userName) { if ($this.impersonations.ContainsKey($userName)) { $local:impersonation = $this.impersonations[$userName] $local:body = New-Object "System.Collections.Generic.Dictionary[[String],[String]]" $local:body.Add("grant_type", "refresh_token") $local:body.Add("client_id", $($this.realmClientName)) if (-not [string]::IsNullOrEmpty($this.realmClientSecret)) { $local:body.Add("client_secret", $($this.realmClientSecret)) } if (-not [string]::IsNullOrEmpty($this.default_scope)) { $local:body.Add("scope", $this.default_scope) } $local:body.Add("refresh_token", $($local:impersonation.tokenObject.refresh_token)) $local:result = $null if ($global:PSVersionTable.PSVersion.Major -eq 7) { $local:result = Invoke-WebRequest -Method Post -Uri $this.realmWellKnownEndpoints.token_endpoint -Body $local:body -SkipHttpErrorCheck } else { try { $local:result = Invoke-WebRequest -Method Post -Uri $this.realmWellKnownEndpoints.token_endpoint -Body $local:body } catch { $local:result = $_.Exception.Response } } if ($local:result.StatusCode -eq [System.Net.HttpStatusCode]::OK) { $local:tokenObject = ($local:result.Content | ConvertFrom-Json) $local:impersonation = @{ tokenObject = $local:tokenObject decodedAccessToken = $this.parseJWTtoken($local:tokenObject.access_token) decodedRefreshToken = $this.parseJWTtoken($local:tokenObject.refresh_token) } $this.impersonations[$userName] = $local:impersonation } else { if ($local:result.StatusCode -eq [System.Net.HttpStatusCode]::BadRequest) { $local:error = $local:result.content | ConvertFrom-Json # -Depth 1 if ($local:error.error_description -eq "Session not active") { # {"error":"invalid_grant","error_description":"Session not active"} $this.requestNewToken($userName) } } else { throw $local:result } } } else { $this.requestNewToken($userName) } } hidden [PSCustomObject]getToken() { return $this.getToken($this.userName) } hidden [PSCustomObject]getToken($userName) { if ($this.impersonations.ContainsKey($userName)) { $local:impersonation = $this.impersonations[$userName] if ($this.isTokenValid($local:impersonation.decodedAccessToken) -eq $false) { if ($this.isTokenValid($local:impersonation.decodedRefreshToken) -eq $false) { $this.requestNewToken($userName) } else { $this.refreshToken($userName) } } } else { $this.requestNewToken($userName) } return $this.impersonations[$userName].tokenObject } [string]getAccessToken() { return $this.getAccessToken($this.userName) } [string]getAccessToken([string]$userName) { return ($this.getToken($userName)).access_token } [string]getRefreshToken() { return $this.getRefreshToken($this.userName) } [string]getRefreshToken([string]$userName) { return ($this.getToken($userName)).refresh_token } [bool]logout() { return $this.logout($this.userName) } [bool]logout([string]$userName) { if ($this.impersonations.ContainsKey($userName)) { $local:impersonation = $this.impersonations[$userName] $local:body = New-Object "System.Collections.Generic.Dictionary[[String],[String]]" $local:body.Add("client_id", $($this.realmClientName)) if (-not [string]::IsNullOrEmpty($this.realmClientSecret)) { $local:body.Add("client_secret", $($this.realmClientSecret)) } $local:body.Add("refresh_token", $($local:impersonation.tokenObject.refresh_token)) $local:url = $this.realmWellKnownEndpoints.end_session_endpoint $local:header = $this.getHeader($userName) $local:header.Remove("Content-Type") | Out-Null $local:result = $null if ($global:PSVersionTable.PSVersion.Major -eq 7) { $local:result = Invoke-WebRequest -Method Post -Uri $url -Headers $local:header -Body $local:body -SkipHttpErrorCheck } else { try { $local:result = Invoke-WebRequest -Method Post -Uri $url -Headers $local:header -Body $local:body } catch { $local:result = $_.Exception.Response } } if ($local:result.StatusCode -eq [System.Net.HttpStatusCode]::NoContent) { $this.impersonations.Remove($userName) } return ($local:result.StatusCode -eq [System.Net.HttpStatusCode]::NoContent) } return $false } [void]dispose() { $local:userNames = $this.impersonations.Keys | Select-Object foreach ($local:userName in $local:userNames) { if ($local:userName -ne $this.userName) { $this.logout($local:userName) | Out-Null } } $this.logout($this.userName) | Out-Null $this.realmUri = $null $this.realmAdminUri = $null $this.realmWellKnownEndpoints = $null $this.realmClientName = "admin-cli" $this.realmClientSecret = [string]::Empty $this.realmName = [string]::Empty $this.userName = [string]::Empty $this.password = [string]::Empty $this.impersonations = @{} [GC]::Collect() } [System.Collections.Generic.Dictionary[[String], [String]]]getHeader() { return $this.getHeader($this.userName) } [System.Collections.Generic.Dictionary[[String], [String]]]getHeader([string]$userName) { $local:header = New-Object "System.Collections.Generic.Dictionary[[String],[String]]" $local:header.Add("Content-Type", "application/json") $local:header.Add("Authorization", "Bearer $($this.getAccessToken($userName))") return $local:header } [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingPlainTextForPassword', '')] KeycloakTokenManager([string]$userName, [string]$password, [uri]$realmUri) { $this.userName = $userName $this.password = $password $this.realmUri = $realmUri $this.getKeycloakUris() } KeycloakTokenManager([string]$userName, [securestring]$password, [uri]$realmUri) { $this.userName = $userName if ($global:PSVersionTable.PSVersion.Major -eq 7) { $this.password = $password | ConvertFrom-SecureString -AsPlainText } else { $local:bstr = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($password) $this.password = [Runtime.InteropServices.Marshal]::PtrToStringBSTR($local:bstr) [Runtime.InteropServices.Marshal]::ZeroFreeBSTR($local:bstr) } $this.realmUri = $realmUri $this.getKeycloakUris() } [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingPlainTextForPassword', '')] KeycloakTokenManager([string]$userName, [string]$password, [uri]$realmUri, [string]$clientName, [string]$clientSecret) { $this.realmClientName = $clientName $this.realmClientSecret = $clientSecret $this.userName = $userName $this.password = $password $this.realmUri = $realmUri $this.getKeycloakUris() } KeycloakTokenManager([string]$userName, [securestring]$password, [uri]$realmUri, [string]$clientName, [string]$clientSecret) { $this.realmClientName = $clientName $this.realmClientSecret = $clientSecret $this.userName = $userName if ($global:PSVersionTable.PSVersion.Major -eq 7) { $this.password = $password | ConvertFrom-SecureString -AsPlainText } else { $local:bstr = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($password) $this.password = [Runtime.InteropServices.Marshal]::PtrToStringBSTR($local:bstr) [Runtime.InteropServices.Marshal]::ZeroFreeBSTR($local:bstr) } $this.realmUri = $realmUri $this.getKeycloakUris() } [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingPlainTextForPassword', '')] KeycloakTokenManager([string]$userName, [string]$password, [uri]$realmUri, [string]$clientName) { $this.realmClientName = $clientName $this.realmClientSecret = [string]::Empty $this.userName = $userName $this.password = $password $this.realmUri = $realmUri $this.getKeycloakUris() } KeycloakTokenManager([string]$userName, [securestring]$password, [uri]$realmUri, [string]$clientName) { $this.realmClientName = $clientName $this.realmClientSecret = [string]::Empty $this.userName = $userName if ($global:PSVersionTable.PSVersion.Major -eq 7) { $this.password = $password | ConvertFrom-SecureString -AsPlainText } else { $local:bstr = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($password) $this.password = [Runtime.InteropServices.Marshal]::PtrToStringBSTR($local:bstr) [Runtime.InteropServices.Marshal]::ZeroFreeBSTR($local:bstr) } $this.realmUri = $realmUri $this.getKeycloakUris() } KeycloakTokenManager([uri]$realmUri, [string]$clientName) { $this.realmClientName = $clientName $this.realmClientSecret = [string]::Empty $this.userName = [string]::Empty $this.password = [string]::Empty $this.grant_type = "authorization_code" $this.realmUri = $realmUri $this.getKeycloakUris() $this.requestNewTokenByBrowser() } [string]ToString() { return $this.getVersion() } } |