Public/Connect-Infisical.ps1
|
# Connect-Infisical.ps1 # Authenticates to the Infisical API and establishes a module-scoped session. # Supports UniversalAuth (machine identity), static Token, and pre-obtained AccessToken. # Called by: User directly. First command to run before any secret operations. # Dependencies: InfisicalSession class function Connect-Infisical { <# .SYNOPSIS Connects to the Infisical secrets management API. .DESCRIPTION Establishes an authenticated session to the Infisical API. Supports three authentication methods: UniversalAuth (machine identity with client credentials), Token (static API token), and AccessToken (pre-obtained JWT). The session is stored at module scope and used by all subsequent commands. .PARAMETER ClientId The Machine Identity Client ID for UniversalAuth authentication. .PARAMETER ClientSecret The Machine Identity Client Secret as a SecureString for UniversalAuth authentication. .PARAMETER Token A static API token as a SecureString for Token-based authentication. .PARAMETER AccessToken A pre-obtained JWT access token as a SecureString, e.g. from a CI system. .PARAMETER ApiUrl The Infisical base URL (without trailing /api). Defaults to "https://app.infisical.com". Use this parameter for self-hosted Infisical instances. .PARAMETER ProjectId The Infisical workspace/project ID. Optional at connect time; required by most secret and project-scoped operations. Can be set later or passed per-command. .PARAMETER Environment The default environment slug (e.g. "dev", "staging", "prod"). Defaults to "prod". Can be overridden per-command. .PARAMETER PassThru If specified, returns the InfisicalSession object. .EXAMPLE $secret = Read-Host -AsSecureString -Prompt 'Client Secret' Connect-Infisical -ClientId 'my-client-id' -ClientSecret $secret -ProjectId 'proj-123' Connects using UniversalAuth machine identity credentials. .EXAMPLE $token = Read-Host -AsSecureString -Prompt 'API Token' Connect-Infisical -Token $token -ProjectId 'proj-123' -ApiUrl 'https://infisical.mycompany.com' Connects to a self-hosted Infisical instance using a static token. .OUTPUTS [InfisicalSession] when -PassThru is specified; otherwise, no output. .NOTES Client credentials are stored in the session for automatic token refresh when using UniversalAuth. Static tokens cannot be refreshed automatically. .LINK Disconnect-Infisical .LINK Get-InfisicalSecret #> [CmdletBinding(SupportsShouldProcess, DefaultParameterSetName = 'UniversalAuth')] [OutputType([InfisicalSession])] param( [Parameter(Mandatory, ParameterSetName = 'UniversalAuth')] [ValidateNotNullOrEmpty()] [string] $ClientId, [Parameter(Mandatory, ParameterSetName = 'UniversalAuth')] [ValidateNotNull()] [System.Security.SecureString] $ClientSecret, [Parameter(Mandatory, ParameterSetName = 'Token')] [ValidateNotNull()] [System.Security.SecureString] $Token, [Parameter(Mandatory, ParameterSetName = 'AccessToken')] [ValidateNotNull()] [System.Security.SecureString] $AccessToken, # AWS Auth [Parameter(Mandatory, ParameterSetName = 'AWSAuth')] [ValidateNotNullOrEmpty()] [string] $AWSIdentityDocument, # Azure Auth [Parameter(Mandatory, ParameterSetName = 'AzureAuth')] [ValidateNotNull()] [System.Security.SecureString] $AzureJwt, # GCP Auth [Parameter(Mandatory, ParameterSetName = 'GCPAuth')] [ValidateNotNull()] [System.Security.SecureString] $GCPIdentityToken, # Kubernetes Auth [Parameter(Mandatory, ParameterSetName = 'KubernetesAuth')] [ValidateNotNull()] [System.Security.SecureString] $KubernetesServiceAccountToken, [Parameter(Mandatory, ParameterSetName = 'KubernetesAuth')] [ValidateNotNullOrEmpty()] [string] $KubernetesIdentityId, # OIDC Auth [Parameter(Mandatory, ParameterSetName = 'OIDCAuth')] [ValidateNotNull()] [System.Security.SecureString] $OIDCToken, [Parameter(Mandatory, ParameterSetName = 'OIDCAuth')] [ValidateNotNullOrEmpty()] [string] $OIDCIdentityId, # JWT Auth [Parameter(Mandatory, ParameterSetName = 'JWTAuth')] [ValidateNotNull()] [System.Security.SecureString] $Jwt, [Parameter(Mandatory, ParameterSetName = 'JWTAuth')] [ValidateNotNullOrEmpty()] [string] $JwtIdentityId, # LDAP Auth [Parameter(Mandatory, ParameterSetName = 'LDAPAuth')] [ValidateNotNullOrEmpty()] [string] $LDAPUsername, [Parameter(Mandatory, ParameterSetName = 'LDAPAuth')] [ValidateNotNull()] [System.Security.SecureString] $LDAPPassword, # Email/Password Auth [Parameter(Mandatory, ParameterSetName = 'EmailAuth')] [ValidateNotNullOrEmpty()] [string] $Email, [Parameter(Mandatory, ParameterSetName = 'EmailAuth')] [ValidateNotNull()] [System.Security.SecureString] $Password, [Parameter()] [ValidateNotNullOrEmpty()] [string] $ApiUrl = 'https://app.infisical.com', [Parameter()] [ValidateNotNullOrEmpty()] [string] $OrganizationId, [Parameter()] [ValidateNotNullOrEmpty()] [string] $ProjectId, [Parameter()] [ValidateNotNullOrEmpty()] [ValidatePattern('^[a-zA-Z0-9_-]+$')] [string] $Environment = 'prod', [Parameter()] [switch] $PassThru ) # Normalize API URL — remove trailing slash $ApiUrl = $ApiUrl.TrimEnd('/') if ($PSCmdlet.ShouldProcess("Connecting to $ApiUrl with auth method '$($PSCmdlet.ParameterSetName)'")) { # Dispose SecureStrings from any existing session before replacing if ($null -ne $script:InfisicalSession) { if ($null -ne $script:InfisicalSession.AccessToken) { $script:InfisicalSession.AccessToken.Dispose() } if ($null -ne $script:InfisicalSession.ClientSecret) { $script:InfisicalSession.ClientSecret.Dispose() } } $script:InfisicalSession = $null Write-Verbose "Connect-Infisical: Connecting to $ApiUrl with auth method '$($PSCmdlet.ParameterSetName)'" $session = [InfisicalSession]::new() $session.ApiUrl = $ApiUrl $session.OrganizationId = $OrganizationId $session.ProjectId = $ProjectId $session.DefaultEnvironment = $Environment $session.AuthMethod = $PSCmdlet.ParameterSetName switch ($PSCmdlet.ParameterSetName) { 'UniversalAuth' { # Store credentials for automatic re-auth $session.ClientId = $ClientId $session.ClientSecret = $ClientSecret # Authenticate via the universal-auth endpoint $clientSecretPlainText = [System.Net.NetworkCredential]::new('', $ClientSecret).Password $authBody = @{ clientId = $ClientId clientSecret = $clientSecretPlainText } $bodyJson = $authBody | ConvertTo-Json -Compress $authUri = "$ApiUrl/api/v1/auth/universal-auth/login" Write-Verbose "Connect-Infisical: Authenticating via POST $authUri" try { $authResponse = Invoke-RestMethod -Uri $authUri -Method POST -Body $bodyJson -ContentType 'application/json' -TimeoutSec 30 -ErrorAction Stop } catch { $errorRecord = [System.Management.Automation.ErrorRecord]::new( [System.Security.Authentication.AuthenticationException]::new( "UniversalAuth login failed: $($_.Exception.Message)" ), 'InfisicalUniversalAuthFailed', [System.Management.Automation.ErrorCategory]::AuthenticationError, $authUri ) $PSCmdlet.ThrowTerminatingError($errorRecord) } # Validate response contains an access token if (-not $authResponse -or [string]::IsNullOrEmpty($authResponse.accessToken)) { $errorRecord = [System.Management.Automation.ErrorRecord]::new( [System.Security.Authentication.AuthenticationException]::new( 'UniversalAuth login succeeded but the response did not contain an access token.' ), 'InfisicalUniversalAuthNoToken', [System.Management.Automation.ErrorCategory]::AuthenticationError, $authUri ) $PSCmdlet.ThrowTerminatingError($errorRecord) } # Store token as SecureString $secureToken = [System.Security.SecureString]::new() foreach ($char in $authResponse.accessToken.ToCharArray()) { $secureToken.AppendChar($char) } $secureToken.MakeReadOnly() $session.AccessToken = $secureToken if ($authResponse.expiresIn) { $session.TokenExpiry = [datetime]::UtcNow.AddSeconds($authResponse.expiresIn) } # Clear auth response to reduce plaintext token exposure window $authResponse = $null Write-Verbose "Connect-Infisical: UniversalAuth authentication successful." } 'Token' { $session.AccessToken = $Token # Static tokens do not have a known expiry $session.TokenExpiry = $null } 'AccessToken' { $session.AccessToken = $AccessToken # Pre-obtained JWTs may expire, but we don't know when without decoding $session.TokenExpiry = $null } 'AWSAuth' { $authBody = @{ iamHttpRequestMethod = 'POST'; iamRequestBody = $AWSIdentityDocument } $authResponse = Invoke-InfisicalAuthEndpoint -ApiUrl $ApiUrl -AuthPath 'aws-auth' -Body $authBody -CallerCmdlet $PSCmdlet Set-InfisicalSessionToken -Session $session -AuthResponse $authResponse $authResponse = $null } 'AzureAuth' { $jwt = [System.Net.NetworkCredential]::new('', $AzureJwt).Password $authBody = @{ jwt = $jwt } $authResponse = Invoke-InfisicalAuthEndpoint -ApiUrl $ApiUrl -AuthPath 'azure-auth' -Body $authBody -CallerCmdlet $PSCmdlet Set-InfisicalSessionToken -Session $session -AuthResponse $authResponse $authResponse = $null } 'GCPAuth' { $token = [System.Net.NetworkCredential]::new('', $GCPIdentityToken).Password $authBody = @{ jwt = $token } $authResponse = Invoke-InfisicalAuthEndpoint -ApiUrl $ApiUrl -AuthPath 'gcp-auth' -Body $authBody -CallerCmdlet $PSCmdlet Set-InfisicalSessionToken -Session $session -AuthResponse $authResponse $authResponse = $null } 'KubernetesAuth' { $saToken = [System.Net.NetworkCredential]::new('', $KubernetesServiceAccountToken).Password $authBody = @{ jwt = $saToken; identityId = $KubernetesIdentityId } $authResponse = Invoke-InfisicalAuthEndpoint -ApiUrl $ApiUrl -AuthPath 'kubernetes-auth' -Body $authBody -CallerCmdlet $PSCmdlet Set-InfisicalSessionToken -Session $session -AuthResponse $authResponse $authResponse = $null } 'OIDCAuth' { $oidcJwt = [System.Net.NetworkCredential]::new('', $OIDCToken).Password $authBody = @{ jwt = $oidcJwt; identityId = $OIDCIdentityId } $authResponse = Invoke-InfisicalAuthEndpoint -ApiUrl $ApiUrl -AuthPath 'oidc-auth' -Body $authBody -CallerCmdlet $PSCmdlet Set-InfisicalSessionToken -Session $session -AuthResponse $authResponse $authResponse = $null } 'JWTAuth' { $jwtValue = [System.Net.NetworkCredential]::new('', $Jwt).Password $authBody = @{ jwt = $jwtValue; identityId = $JwtIdentityId } $authResponse = Invoke-InfisicalAuthEndpoint -ApiUrl $ApiUrl -AuthPath 'jwt-auth' -Body $authBody -CallerCmdlet $PSCmdlet Set-InfisicalSessionToken -Session $session -AuthResponse $authResponse $authResponse = $null } 'LDAPAuth' { $ldapPass = [System.Net.NetworkCredential]::new('', $LDAPPassword).Password $authBody = @{ username = $LDAPUsername; password = $ldapPass } $authResponse = Invoke-InfisicalAuthEndpoint -ApiUrl $ApiUrl -AuthPath 'ldap-auth' -Body $authBody -CallerCmdlet $PSCmdlet Set-InfisicalSessionToken -Session $session -AuthResponse $authResponse $authResponse = $null } 'EmailAuth' { $passwordPlainText = [System.Net.NetworkCredential]::new('', $Password).Password $authBody = @{ email = $Email password = $passwordPlainText } $bodyJson = $authBody | ConvertTo-Json -Compress $authUri = "$ApiUrl/api/v3/auth/login" Write-Verbose "Connect-Infisical: Authenticating via POST $authUri" try { $authResponse = Invoke-RestMethod -Uri $authUri -Method POST -Body $bodyJson -ContentType 'application/json' -TimeoutSec 30 -ErrorAction Stop } catch { $errorRecord = [System.Management.Automation.ErrorRecord]::new( [System.Security.Authentication.AuthenticationException]::new( "Email/password login failed: $($_.Exception.Message)" ), 'InfisicalEmailAuthFailed', [System.Management.Automation.ErrorCategory]::AuthenticationError, $authUri ) $PSCmdlet.ThrowTerminatingError($errorRecord) } if (-not $authResponse -or [string]::IsNullOrEmpty($authResponse.accessToken)) { $errorRecord = [System.Management.Automation.ErrorRecord]::new( [System.Security.Authentication.AuthenticationException]::new( 'Email/password login succeeded but the response did not contain an access token.' ), 'InfisicalEmailAuthNoToken', [System.Management.Automation.ErrorCategory]::AuthenticationError, $authUri ) $PSCmdlet.ThrowTerminatingError($errorRecord) } Set-InfisicalSessionToken -Session $session -AuthResponse $authResponse # Email login response has no expiresIn — extract exp from JWT claims if ($null -eq $session.TokenExpiry) { try { $tokenStr = $authResponse.accessToken $parts = $tokenStr.Split('.') if ($parts.Count -ge 2) { $payload = $parts[1].Replace('-', '+').Replace('_', '/') switch ($payload.Length % 4) { 2 { $payload += '==' } 3 { $payload += '=' } } $jwtClaims = [System.Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($payload)) | ConvertFrom-Json if ($null -ne $jwtClaims.PSObject.Properties['exp']) { $session.TokenExpiry = [datetime]::UnixEpoch.AddSeconds($jwtClaims.exp) } } } catch { Write-Verbose "Connect-Infisical: Could not extract token expiry from JWT: $($_.Exception.Message)" } } $authResponse = $null } } $session.UpdateConnectionStatus() $script:InfisicalSession = $session # Auto-resolve OrganizationId from JWT if not explicitly provided if ([string]::IsNullOrEmpty($session.OrganizationId) -and $null -ne $session.AccessToken) { Write-Verbose 'Connect-Infisical: Auto-resolving OrganizationId from JWT claims...' try { $tokenPlain = $session.GetAccessTokenPlainText() $jwtParts = $tokenPlain.Split('.') if ($jwtParts.Count -ge 2) { $jwtPayload = $jwtParts[1].Replace('-', '+').Replace('_', '/') switch ($jwtPayload.Length % 4) { 2 { $jwtPayload += '==' } 3 { $jwtPayload += '=' } } $claims = [System.Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($jwtPayload)) | ConvertFrom-Json if ($claims.identityId) { # Machine identity JWT — look up identity to get orgId $identityResponse = Invoke-InfisicalApi -Method GET -Endpoint "/api/v1/identities/$($claims.identityId)" -Session $session if ($null -ne $identityResponse -and $null -ne $identityResponse.identity) { $orgId = if ($identityResponse.identity -is [hashtable]) { $identityResponse.identity['orgId'] } else { $identityResponse.identity.orgId } if (-not [string]::IsNullOrEmpty($orgId)) { $session.OrganizationId = $orgId Write-Verbose "Connect-Infisical: Auto-resolved OrganizationId to '$orgId'" } } } elseif ($claims.userId) { # User JWT (email/password login) — get orgs from user endpoint $orgsResponse = Invoke-InfisicalApi -Method GET -Endpoint '/api/v1/organization' -Session $session if ($null -ne $orgsResponse -and $null -ne $orgsResponse.organizations -and @($orgsResponse.organizations).Count -gt 0) { $firstOrg = @($orgsResponse.organizations)[0] $orgId = if ($firstOrg -is [hashtable]) { $firstOrg['id'] } else { $firstOrg.id } if (-not [string]::IsNullOrEmpty($orgId)) { $session.OrganizationId = $orgId Write-Verbose "Connect-Infisical: Auto-resolved OrganizationId to '$orgId' from user organizations" } } } } } catch { Write-Verbose "Connect-Infisical: Unable to auto-resolve OrganizationId: $($_.Exception.Message)" } } # Probe server API capabilities for version gating Write-Verbose 'Connect-Infisical: Probing server API capabilities...' try { $session.ApiCapabilities = Test-InfisicalApiCapability -Session $session } catch { Write-Warning "Connect-Infisical: Unable to detect server API capabilities: $($_.Exception.Message). Version checks will be skipped." $session.ApiCapabilities = @{} } Write-Verbose "Connect-Infisical: Session established. Connected=$($session.Connected)" if ($PassThru.IsPresent) { return $session } } } |