Public/Connection/Connect-JIM.ps1
|
# Copyright (c) Tetron Limited. All rights reserved. # Licensed under the Tetron Commercial License. See LICENSE file in the project root. function Connect-JIM { <# .SYNOPSIS Connects to a JIM instance for administration. .DESCRIPTION Establishes a connection to a JIM (Junctional Identity Manager) instance. This connection is required before using any other JIM cmdlets. Supports two authentication methods: 1. Interactive browser-based SSO authentication (default for interactive sessions) 2. API key authentication (for automation, CI/CD, and scripting) For interactive (SSO) sign-ins, the refresh token is persisted in the operating system's credential store (Credential Manager on Windows, login Keychain on macOS, libsecret on Linux) so that opening a new terminal reconnects silently without a browser. Only the refresh token is stored, never the access token. On systems with no usable credential store (typically headless Linux without a keyring), the module falls back to in-memory tokens for the session. Use -NoPersist to opt out. .PARAMETER Url The base URL of the JIM instance, e.g., 'https://jim.company.com' or 'http://localhost:5200'. .PARAMETER ApiKey The API key for authentication. API keys can be created in the JIM web interface under Admin > API Keys. When specified, skips interactive authentication. API key connections never read or write the credential store. .PARAMETER Force Forces re-authentication even if a valid session exists. Ignores any persisted refresh token and overwrites it with the newly obtained one. .PARAMETER NoPersist Authenticates for this session only, without reading from or writing to the operating system credential store. Useful on shared machines. .PARAMETER TimeoutSeconds How long to wait for interactive authentication to complete. Defaults to 300 (5 minutes). .OUTPUTS Returns the connection information on success. .EXAMPLE Connect-JIM -Url "https://jim.company.com" Connects using interactive browser-based SSO authentication. Opens the default browser for authentication. .EXAMPLE Connect-JIM -Url "https://jim.company.com" -ApiKey "jim_ak_abc123..." Connects using an API key (for automation scenarios). .EXAMPLE Connect-JIM -Url "http://localhost:5200" -ApiKey $env:JIM_API_KEY Connects to a local JIM instance using an API key from an environment variable. .EXAMPLE Connect-JIM -Url "https://jim.company.com" -Force Forces re-authentication, ignoring any cached session. .EXAMPLE Connect-JIM -Url "https://jim.company.com" -NoPersist Connects interactively without persisting the refresh token to the OS credential store (in-memory for this session only). .NOTES Interactive authentication requires: - SSO to be configured on the JIM server - A browser to be available - The IDP to have localhost redirect URIs configured For automation scenarios (CI/CD, scripts), use API key authentication. API keys can be created in the JIM web interface under Admin > API Keys. .LINK Disconnect-JIM Test-JIMConnection https://github.com/TetronIO/JIM #> [CmdletBinding(DefaultParameterSetName = 'Interactive')] [OutputType([PSCustomObject])] param( [Parameter(Mandatory, Position = 0)] [ValidateNotNullOrEmpty()] [string]$Url, [Parameter(Mandatory, ParameterSetName = 'ApiKey', Position = 1)] [ValidateNotNullOrEmpty()] [string]$ApiKey, [Parameter(ParameterSetName = 'Interactive')] [switch]$Force, [Parameter(ParameterSetName = 'Interactive')] [switch]$NoPersist, [Parameter(ParameterSetName = 'Interactive')] [ValidateRange(30, 600)] [int]$TimeoutSeconds = 300 ) Write-Verbose "Connecting to JIM at $Url" # Validate URL format if (-not ($Url -match '^https?://')) { throw "Invalid URL format. URL must start with http:// or https://" } $baseUrl = $Url.TrimEnd('/') # Check if we should use API key authentication if ($PSCmdlet.ParameterSetName -eq 'ApiKey') { return Connect-JIMWithApiKey -BaseUrl $baseUrl -ApiKey $ApiKey } # Interactive authentication return Connect-JIMInteractive -BaseUrl $baseUrl -Force:$Force -NoPersist:$NoPersist -TimeoutSeconds $TimeoutSeconds } function Connect-JIMWithApiKey { <# .SYNOPSIS Internal function to connect using API key authentication. #> [CmdletBinding()] [OutputType([PSCustomObject])] param( [Parameter(Mandatory)] [string]$BaseUrl, [Parameter(Mandatory)] [string]$ApiKey ) # Store connection info $script:JIMConnection = [PSCustomObject]@{ Url = $BaseUrl ApiKey = $ApiKey AccessToken = $null RefreshToken = $null TokenExpiresAt = $null AuthMethod = 'ApiKey' Connected = $false } # Test the connection try { Write-Verbose "Testing connection to JIM..." $health = Invoke-JIMApi -Endpoint '/api/v1/health' $script:JIMConnection.Connected = $true # Fetch server version $serverVersion = Get-JIMServerVersion Write-Verbose "Successfully connected to JIM using API key" Show-JIMBanner -ServerVersion $serverVersion -Url $BaseUrl # Note: Skip authorisation check for API keys - they are authorised by definition # (they have explicit roles assigned at creation time). The userinfo endpoint checks # for a MetaverseObject which only applies to interactive (SSO) users. # Return connection info (without exposing full API key) $keyPreview = if ($ApiKey.Length -gt 12) { $ApiKey.Substring(0, 8) + "..." + $ApiKey.Substring($ApiKey.Length - 4) } else { "***" } [PSCustomObject]@{ Url = $script:JIMConnection.Url AuthMethod = 'ApiKey' ApiKey = $keyPreview Connected = $true ServerVersion = $serverVersion Authorised = $true Status = $health.status ?? 'Connected' } } catch { $script:JIMConnection = $null throw "Failed to connect to JIM at $BaseUrl`: $_" } } function Connect-JIMInteractive { <# .SYNOPSIS Internal function to connect using interactive browser authentication. #> [CmdletBinding()] [OutputType([PSCustomObject])] param( [Parameter(Mandatory)] [string]$BaseUrl, [switch]$Force, [switch]$NoPersist, [int]$TimeoutSeconds = 300 ) # Check for existing valid session if (-not $Force -and $script:JIMConnection -and $script:JIMConnection.AuthMethod -eq 'OAuth') { if ($script:JIMConnection.Url -eq $BaseUrl -and $script:JIMConnection.Connected) { # Check if token is still valid (with 5 minute buffer) if ($script:JIMConnection.TokenExpiresAt -and $script:JIMConnection.TokenExpiresAt -gt (Get-Date).AddMinutes(5)) { Write-Verbose "Using existing valid OAuth session" return [PSCustomObject]@{ Url = $script:JIMConnection.Url AuthMethod = 'OAuth' Connected = $true ExpiresAt = $script:JIMConnection.TokenExpiresAt Status = 'Connected (cached)' } } # Try to refresh the token if ($script:JIMConnection.RefreshToken -and $script:JIMConnection.OAuthConfig) { try { Write-Verbose "Access token expired, attempting refresh..." $tokens = Invoke-OAuthTokenRefresh ` -TokenEndpoint $script:JIMConnection.OAuthConfig.TokenEndpoint ` -ClientId $script:JIMConnection.OAuthConfig.ClientId ` -RefreshToken $script:JIMConnection.RefreshToken ` -Scopes $script:JIMConnection.OAuthConfig.Scopes $script:JIMConnection.AccessToken = $tokens.AccessToken $script:JIMConnection.RefreshToken = $tokens.RefreshToken $script:JIMConnection.TokenExpiresAt = $tokens.ExpiresAt # Write the rotated refresh token back to the credential store so the # persisted copy stays valid (most IdPs rotate refresh tokens on use). if ($script:JIMConnection.Persisted) { try { Save-JIMToken -BaseUrl $script:JIMConnection.Url -RefreshToken $tokens.RefreshToken | Out-Null } catch { Write-Verbose "Failed to persist refreshed token: $_" } } Write-Verbose "Successfully refreshed access token" $serverVersion = Get-JIMServerVersion Show-JIMBanner -ServerVersion $serverVersion -Url $script:JIMConnection.Url return [PSCustomObject]@{ Url = $script:JIMConnection.Url AuthMethod = 'OAuth' Connected = $true ServerVersion = $serverVersion ExpiresAt = $tokens.ExpiresAt Status = 'Connected (refreshed)' } } catch { Write-Verbose "Token refresh failed, proceeding with full authentication: $_" } } } } # Get OAuth configuration from JIM Write-Verbose "Fetching OAuth configuration from JIM..." try { $authConfig = Invoke-RestMethod -Uri "$BaseUrl/api/v1/auth/config" -Method Get } catch { $statusCode = $_.Exception.Response.StatusCode.value__ if ($statusCode -eq 503) { throw "SSO is not configured on this JIM instance. Use Connect-JIM with -ApiKey parameter for API key authentication." } throw "Failed to get OAuth configuration from JIM: $_" } # Get OIDC discovery document Write-Verbose "Fetching OIDC discovery document from $($authConfig.authority)..." $discovery = Get-OidcDiscoveryDocument -Authority $authConfig.authority # Determine whether persistent token storage is in play for this session. # -NoPersist opts out; otherwise it depends on whether the OS has a usable # credential store (Windows/macOS always; Linux only with libsecret present). $persistenceAvailable = Test-JIMTokenPersistenceAvailable $usePersistence = (-not $NoPersist) -and $persistenceAvailable # Attempt a silent reconnect using a persisted refresh token before opening a # browser. Skipped when -Force is set (force always re-authenticates and then # overwrites the stored token). if ($usePersistence -and -not $Force) { $cachedRefreshToken = Get-JIMPersistedToken -BaseUrl $BaseUrl if ($cachedRefreshToken) { try { Write-Verbose "Found a persisted refresh token; attempting silent reconnect..." $tokens = Invoke-OAuthTokenRefresh ` -TokenEndpoint $discovery.TokenEndpoint ` -ClientId $authConfig.clientId ` -RefreshToken $cachedRefreshToken ` -Scopes $authConfig.scopes $script:JIMConnection = [PSCustomObject]@{ Url = $BaseUrl ApiKey = $null AccessToken = $tokens.AccessToken RefreshToken = $tokens.RefreshToken TokenExpiresAt = $tokens.ExpiresAt AuthMethod = 'OAuth' Connected = $false Persisted = $true OAuthConfig = @{ Authority = $authConfig.authority ClientId = $authConfig.clientId Scopes = $authConfig.scopes TokenEndpoint = $discovery.TokenEndpoint } } Write-Verbose "Testing connection to JIM with cached credentials..." $health = Invoke-JIMApi -Endpoint '/api/v1/health' $script:JIMConnection.Connected = $true $serverVersion = Get-JIMServerVersion Show-JIMBanner -ServerVersion $serverVersion -Url $BaseUrl -StatusLine "Connected to JIM using cached credentials (no browser sign-in required)." $userInfo = Test-JIMAuthorisation # Persist the (possibly rotated) refresh token. try { Save-JIMToken -BaseUrl $BaseUrl -RefreshToken $tokens.RefreshToken | Out-Null } catch { Write-Verbose "Failed to persist refreshed token: $_" } return [PSCustomObject]@{ Url = $script:JIMConnection.Url AuthMethod = 'OAuth' Connected = $true ServerVersion = $serverVersion ExpiresAt = $tokens.ExpiresAt Authorised = $userInfo.authorised ?? $null Status = 'Connected (cached)' } } catch { Write-Verbose "Silent reconnect with persisted token failed, falling back to browser sign-in: $_" $script:JIMConnection = $null # Drop the stale token so we don't keep retrying a dead refresh token. try { Remove-JIMToken -BaseUrl $BaseUrl | Out-Null } catch { Write-Verbose "Failed to remove stale persisted token: $_" } } } } # Tell the user when persistence was wanted but the platform has no usable store # (typically headless/SSH Linux without a keyring), and point them at -ApiKey. if (-not $NoPersist -and -not $persistenceAvailable) { Write-Host "Token persistence is unavailable on this system (no OS keyring detected); you will need to re-authenticate in new sessions. For unattended or headless use, connect with -ApiKey instead." -ForegroundColor DarkGray } # Perform browser-based authentication Write-Host "" Write-Host "Starting interactive authentication with JIM..." -ForegroundColor Cyan Write-Host "You will be redirected to your organisation's identity provider." -ForegroundColor Gray Write-Host "" $tokens = Invoke-OAuthBrowserFlow ` -AuthorizeEndpoint $discovery.AuthorizeEndpoint ` -TokenEndpoint $discovery.TokenEndpoint ` -ClientId $authConfig.clientId ` -Scopes $authConfig.scopes ` -TimeoutSeconds $TimeoutSeconds # Store connection info $script:JIMConnection = [PSCustomObject]@{ Url = $BaseUrl ApiKey = $null AccessToken = $tokens.AccessToken RefreshToken = $tokens.RefreshToken TokenExpiresAt = $tokens.ExpiresAt AuthMethod = 'OAuth' Connected = $false Persisted = $usePersistence OAuthConfig = @{ Authority = $authConfig.authority ClientId = $authConfig.clientId Scopes = $authConfig.scopes TokenEndpoint = $discovery.TokenEndpoint } } # Test the connection with the new token try { Write-Verbose "Testing connection to JIM with OAuth token..." $health = Invoke-JIMApi -Endpoint '/api/v1/health' $script:JIMConnection.Connected = $true # Fetch server version $serverVersion = Get-JIMServerVersion # When the user opted out of persistence, confirm it directly under the # connected line so it is obvious a new terminal will need to sign in again. $bannerStatus = @() if ($NoPersist) { $bannerStatus += "Auth persistence disabled (-NoPersist): a new terminal will require a fresh sign-in." } Show-JIMBanner -ServerVersion $serverVersion -Url $BaseUrl -StatusLine $bannerStatus # Verify the user is authorised to use JIM $userInfo = Test-JIMAuthorisation # Persist the refresh token so future sessions can reconnect without a browser. if ($usePersistence -and $tokens.RefreshToken) { try { Save-JIMToken -BaseUrl $BaseUrl -RefreshToken $tokens.RefreshToken | Out-Null Write-Verbose "Persisted refresh token for future sessions" } catch { Write-Warning "Connected, but failed to persist the refresh token for future sessions: $_" } } [PSCustomObject]@{ Url = $script:JIMConnection.Url AuthMethod = 'OAuth' Connected = $true ServerVersion = $serverVersion ExpiresAt = $tokens.ExpiresAt Authorised = $userInfo.authorised ?? $null Status = $health.status ?? 'Connected' } } catch { $script:JIMConnection = $null throw "Authentication succeeded but failed to connect to JIM API: $_" } } |