diff --git a/Tests/ConvertFrom-Base64UrlString.Tests.ps1 b/Tests/ConvertFrom-Base64UrlString.Tests.ps1 new file mode 100644 index 00000000..4edf91e4 --- /dev/null +++ b/Tests/ConvertFrom-Base64UrlString.Tests.ps1 @@ -0,0 +1,94 @@ +Describe $($PSCommandPath -Replace '.Tests.ps1') { + + BeforeAll { + #Get Current Directory + $Here = Split-Path -Parent $PSCommandPath + + #Assume ModuleName from Repository Root folder + $ModuleName = Split-Path (Split-Path $Here -Parent) -Leaf + + #Resolve Path to Module Directory + $ModulePath = Resolve-Path "$Here\..\$ModuleName" + + #Define Path to Module Manifest + $ManifestPath = Join-Path "$ModulePath" "$ModuleName.psd1" + + if ( -not (Get-Module -Name $ModuleName -All)) { + + Import-Module -Name "$ManifestPath" -ArgumentList $true -Force -ErrorAction Stop + + } + + $Script:RequestBody = $null + $psPASSession = [ordered]@{ + BaseURI = 'https://SomeURL/SomeApp' + User = $null + ExternalVersion = [System.Version]'0.0' + WebSession = New-Object Microsoft.PowerShell.Commands.WebRequestSession + StartTime = $null + ElapsedTime = $null + LastCommand = $null + LastCommandTime = $null + LastCommandResults = $null + } + + New-Variable -Name psPASSession -Value $psPASSession -Scope Script -Force + + } + + + AfterAll { + + $Script:RequestBody = $null + + } + + InModuleScope $(Split-Path (Split-Path (Split-Path -Parent $PSCommandPath) -Parent) -Leaf ) { + + Context 'Mandatory Parameters' { + + $Parameters = @{Parameter = 'InputString' } + + It 'specifies parameter as mandatory' -TestCases $Parameters { + + param($Parameter) + + (Get-Command ConvertFrom-Base64UrlString).Parameters["$Parameter"].Attributes.Mandatory | Should -Be $true + + } + + } + + Context 'Base64Url Decoding' { + + It 'decodes Base64Url string without padding' { + $base64Url = 'SGVsbG8gV29ybGQ' + $result = ConvertFrom-Base64UrlString -InputString $base64Url + $resultString = [System.Text.Encoding]::UTF8.GetString($result) + $resultString | Should -Be 'Hello World' + } + + It 'decodes Base64Url with URL-safe characters (dash and underscore)' { + $base64Url = 'PDw_Pz8-Pg' + $result = ConvertFrom-Base64UrlString -InputString $base64Url + $result | Should -Not -BeNullOrEmpty + } + + It 'handles padding correctly' { + $base64Url = 'YWJj' + $result = ConvertFrom-Base64UrlString -InputString $base64Url + $resultString = [System.Text.Encoding]::UTF8.GetString($result) + $resultString | Should -Be 'abc' + } + + It 'converts Base64Url to byte array' { + $base64Url = 'VGVzdA' + $result = ConvertFrom-Base64UrlString -InputString $base64Url + $result.GetType().BaseType.Name | Should -Be 'Array' + } + + } + + } + +} diff --git a/Tests/Invoke-FIDO2Authentication.Tests.ps1 b/Tests/Invoke-FIDO2Authentication.Tests.ps1 new file mode 100644 index 00000000..db053025 --- /dev/null +++ b/Tests/Invoke-FIDO2Authentication.Tests.ps1 @@ -0,0 +1,128 @@ +Describe $($PSCommandPath -Replace '.Tests.ps1') { + + BeforeAll { + #Get Current Directory + $Here = Split-Path -Parent $PSCommandPath + + #Assume ModuleName from Repository Root folder + $ModuleName = Split-Path (Split-Path $Here -Parent) -Leaf + + #Resolve Path to Module Directory + $ModulePath = Resolve-Path "$Here\..\$ModuleName" + + #Define Path to Module Manifest + $ManifestPath = Join-Path "$ModulePath" "$ModuleName.psd1" + + if ( -not (Get-Module -Name $ModuleName -All)) { + + Import-Module -Name "$ManifestPath" -ArgumentList $true -Force -ErrorAction Stop + + } + + $Script:RequestBody = $null + $psPASSession = [ordered]@{ + BaseURI = 'https://SomeURL/SomeApp' + User = $null + ExternalVersion = [System.Version]'0.0' + WebSession = New-Object Microsoft.PowerShell.Commands.WebRequestSession + StartTime = $null + ElapsedTime = $null + LastCommand = $null + LastCommandTime = $null + LastCommandResults = $null + } + + New-Variable -Name psPASSession -Value $psPASSession -Scope Script -Force + + } + + + AfterAll { + + $Script:RequestBody = $null + + } + + InModuleScope $(Split-Path (Split-Path (Split-Path -Parent $PSCommandPath) -Parent) -Leaf ) { + + Context 'Mandatory Parameters' { + + $Parameters = @( + @{Parameter = 'BaseURI' }, + @{Parameter = 'UserName' } + ) + + It 'specifies parameter as mandatory' -TestCases $Parameters { + + param($Parameter) + + (Get-Command Invoke-FIDO2Authentication).Parameters["$Parameter"].Attributes.Mandatory | Should -Be $true + + } + + It 'specifies parameter LogonRequest as optional' { + + (Get-Command Invoke-FIDO2Authentication).Parameters["LogonRequest"].Attributes.Mandatory | Should -Be $false + + } + + } + + Context 'Platform Requirements' { + + BeforeEach { + $IsWindowsPlatform = (-not (Test-IsCoreCLR)) -or $IsWindows + } + + It 'requires Windows platform' { + if (-not $IsWindowsPlatform) { + { Invoke-FIDO2Authentication -BaseURI 'https://pvwa.example.com' -UserName 'testuser' -LogonRequest @{} } | Should -Throw '*Windows*' + } + } + + } + + Context 'Input Validation' { + + It 'accepts BaseURI parameter' { + $params = (Get-Command Invoke-FIDO2Authentication).Parameters['BaseURI'] + $params | Should -Not -BeNullOrEmpty + $params.ParameterType.Name | Should -Be 'String' + } + + It 'accepts UserName parameter' { + $params = (Get-Command Invoke-FIDO2Authentication).Parameters['UserName'] + $params | Should -Not -BeNullOrEmpty + $params.ParameterType.Name | Should -Be 'String' + } + + It 'accepts LogonRequest parameter' { + $params = (Get-Command Invoke-FIDO2Authentication).Parameters['LogonRequest'] + $params | Should -Not -BeNullOrEmpty + $params.ParameterType.Name | Should -Be 'Hashtable' + } + + } + + Context 'Help Content' { + + It 'has a synopsis' { + $help = Get-Help Invoke-FIDO2Authentication + $help.Synopsis | Should -Not -BeNullOrEmpty + } + + It 'has a description' { + $help = Get-Help Invoke-FIDO2Authentication + $help.Description | Should -Not -BeNullOrEmpty + } + + It 'has examples' { + $help = Get-Help Invoke-FIDO2Authentication + $help.Examples | Should -Not -BeNullOrEmpty + } + + } + + } + +} diff --git a/Tests/New-PASSession.Tests.ps1 b/Tests/New-PASSession.Tests.ps1 index 96a49186..d9b99a62 100644 --- a/Tests/New-PASSession.Tests.ps1 +++ b/Tests/New-PASSession.Tests.ps1 @@ -1237,6 +1237,69 @@ Describe $($PSCommandPath -Replace '.Tests.ps1') { } + Context 'Gen2 with FIDO2' { + + BeforeEach { + + Mock Assert-VersionRequirement -MockWith {} + + Mock Invoke-FIDO2Authentication -MockWith { + [PSCustomObject]@{ + 'CyberArkLogonResult' = 'AAAAAAA\\\REEEAAAAALLLLYYYYY\\\\LOOOOONNNNGGGGG\\\ACCCCCEEEEEEEESSSSSSS\\\\\\TTTTTOOOOOKKKKKEEEEEN' + } + } + + Mock Get-PASServer -MockWith { + [PSCustomObject]@{ + ExternalVersion = '14.6' + } + } + + Mock Get-PASLoggedOnUser -MockWith { + @{'UserName' = 'TestUser' } + } + + $psPASSession.ExternalVersion = '14.6' + $psPASSession.WebSession = New-Object Microsoft.PowerShell.Commands.WebRequestSession + + } + + It 'sends request with UserName parameter' { + New-PASSession -BaseURI 'https://pvwa.cyberark.com' -type FIDO2 -UserName 'TestUser' + Assert-MockCalled Invoke-FIDO2Authentication -Times 1 -Exactly -Scope It + + } + + It 'sends request with expected parameters' { + New-PASSession -BaseURI 'https://pvwa.cyberark.com' -type FIDO2 -UserName 'TestUser' + Assert-MockCalled Invoke-FIDO2Authentication -ParameterFilter { + + $BaseURI -eq 'https://pvwa.cyberark.com/PasswordVault' -and $UserName -eq 'TestUser' + + } -Times 1 -Exactly -Scope It + + } + + It 'throws error when UserName is not provided' { + { New-PASSession -BaseURI 'https://pvwa.cyberark.com' -type FIDO2 } | Should -Throw 'Username is required for FIDO2 authentication. Use -UserName parameter.' + } + + It 'sets expected BaseURI' { + + New-PASSession -BaseURI 'https://pvwa.cyberark.com' -type FIDO2 -UserName 'TestUser' + $Script:psPASSession.BaseURI | Should -Be 'https://pvwa.cyberark.com/PasswordVault' + + } + + It 'sets expected authorization header' { + + New-PASSession -BaseURI 'https://pvwa.cyberark.com' -type FIDO2 -UserName 'TestUser' + $psPASSession.WebSession.Headers['Authorization'] | Should -Be 'AAAAAAA\\\REEEAAAAALLLLYYYYY\\\\LOOOOONNNNGGGGG\\\ACCCCCEEEEEEEESSSSSSS\\\\\\TTTTTOOOOOKKKKKEEEEEN' + + } + + } + } } diff --git a/docs/collections/_commands/New-PASSession.md b/docs/collections/_commands/New-PASSession.md index f25a33c2..7f8ec617 100644 --- a/docs/collections/_commands/New-PASSession.md +++ b/docs/collections/_commands/New-PASSession.md @@ -16,16 +16,18 @@ Authenticates a user to CyberArk Vault/API. ### Gen2 (Default) ``` -New-PASSession [-Credential ] -BaseURI [-newPassword ] [-type ] - [-concurrentSession ] [-PVWAAppName ] [-SkipVersionCheck] [-Certificate ] - [-CertificateThumbprint ] [-SkipCertificateCheck] [-WhatIf] [-Confirm] [] +New-PASSession [-Credential ] -BaseURI [-UserName ] + [-newPassword ] [-type ] [-concurrentSession ] [-PVWAAppName ] + [-SkipVersionCheck] [-Certificate ] [-CertificateThumbprint ] [-SkipCertificateCheck] + [-WhatIf] [-Confirm] [] ``` ### ISPSS-URL-ServiceUser ``` New-PASSession -Credential -IdentityTenantURL -PrivilegeCloudURL [-ServiceUser] [-PVWAAppName ] [-SkipVersionCheck] [-Certificate ] - [-CertificateThumbprint ] [-SkipCertificateCheck] [-WhatIf] [-Confirm] [] + [-CertificateThumbprint ] [-SkipCertificateCheck] [-WhatIf] + [-Confirm] [] ``` ### ISPSS-Subdomain-ServiceUser @@ -39,7 +41,8 @@ New-PASSession -Credential -TenantSubdomain [-ServiceUse ``` New-PASSession -Credential -IdentityTenantURL -PrivilegeCloudURL [-IdentityUser] [-PVWAAppName ] [-SkipVersionCheck] [-Certificate ] - [-CertificateThumbprint ] [-SkipCertificateCheck] [-WhatIf] [-Confirm] [] + [-CertificateThumbprint ] [-SkipCertificateCheck] [-WhatIf] + [-Confirm] [] ``` ### ISPSS-Subdomain-IdentityUser @@ -54,14 +57,16 @@ New-PASSession -Credential -TenantSubdomain [-IdentityUs New-PASSession -Credential -BaseURI [-UseGen1API] -useRadiusAuthentication [-OTP ] [-OTPMode ] [-OTPDelimiter ] [-RadiusChallenge ] [-connectionNumber ] [-PVWAAppName ] [-SkipVersionCheck] [-Certificate ] - [-CertificateThumbprint ] [-SkipCertificateCheck] [-WhatIf] [-Confirm] [] + [-CertificateThumbprint ] [-SkipCertificateCheck] [-WhatIf] + [-Confirm] [] ``` ### Gen1 ``` New-PASSession -Credential -BaseURI [-UseGen1API] [-newPassword ] [-connectionNumber ] [-PVWAAppName ] [-SkipVersionCheck] [-Certificate ] - [-CertificateThumbprint ] [-SkipCertificateCheck] [-WhatIf] [-Confirm] [] + [-CertificateThumbprint ] [-SkipCertificateCheck] [-WhatIf] + [-Confirm] [] ``` ### Gen2Radius @@ -82,8 +87,8 @@ New-PASSession -BaseURI [-UseDefaultCredentials] [-concurrentSession [-UseSharedAuthentication] [-PVWAAppName ] [-SkipVersionCheck] - [-Certificate ] [-CertificateThumbprint ] [-SkipCertificateCheck] [-WhatIf] - [-Confirm] [] + [-Certificate ] [-CertificateThumbprint ] [-SkipCertificateCheck] + [-WhatIf] [-Confirm] [] ``` ### Gen2SAML @@ -386,6 +391,13 @@ Requires IdentityCommand module to be installed for authentication flow to compl See: Get-Help IdentityCommand +### EXAMPLE 31 +``` +New-PASSession -BaseURI https://pvwa.company.com -type FIDO2 -UserName administrator +``` + +Authenticates to CyberArk using FIDO2/WebAuthn hardware security key authentication. + ## PARAMETERS ### -Credential @@ -521,13 +533,14 @@ Accept wildcard characters: False ### -type When using the Gen2 API, specify the type of authentication to use. -Valid values are: - CyberArk - +Valid values are: +- CyberArk - LDAP -- Windows -- Minimum version required 10.4 - RADIUS +- Windows (Minimum version required 10.4) +- RADIUS - PKI - PKIPN +- FIDO2 (Minimum version required 14.4) ```yaml Type: String @@ -907,6 +920,23 @@ Accept pipeline input: True (ByPropertyName) Accept wildcard characters: False ``` +### -UserName +The username for FIDO2 authentication. +When using `-type FIDO2`, specify the username with this parameter (required). +The username identifies the user and their registered security keys. + +```yaml +Type: String +Parameter Sets: Gen2 +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: True (ByPropertyName) +Accept wildcard characters: False +``` + ### CommonParameters This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable, -InformationAction, -InformationVariable, -OutVariable, -OutBuffer, -PipelineVariable, -Verbose, -WarningAction, and -WarningVariable. For more information, see [about_CommonParameters](http://go.microsoft.com/fwlink/?LinkID=113216). diff --git a/psPAS/Functions/Authentication/New-PASSession.ps1 b/psPAS/Functions/Authentication/New-PASSession.ps1 index 483283e9..12681c02 100644 --- a/psPAS/Functions/Authentication/New-PASSession.ps1 +++ b/psPAS/Functions/Authentication/New-PASSession.ps1 @@ -189,6 +189,15 @@ function New-PASSession { [Alias('UseClassicAPI')] [switch]$UseGen1API, + [Parameter( + Mandatory = $false, + ValueFromPipeline = $false, + ValueFromPipelinebyPropertyName = $true, + ParameterSetName = 'Gen2' + )] + [ValidateNotNullOrEmpty()] + [string]$UserName, + [Parameter( Mandatory = $false, ValueFromPipeline = $false, @@ -254,7 +263,7 @@ function New-PASSession { ValueFromPipelinebyPropertyName = $true, ParameterSetName = 'Gen2Radius' )] - [ValidateSet('CyberArk', 'LDAP', 'Windows', 'RADIUS', 'PKI', 'PKIPN')] + [ValidateSet('CyberArk', 'LDAP', 'Windows', 'RADIUS', 'PKI', 'PKIPN', 'FIDO2')] [string]$type = 'CyberArk', [Parameter( @@ -555,7 +564,7 @@ function New-PASSession { #Get request parameters $boundParameters = $PSBoundParameters | Get-PASParameter -ParametersToRemove Credential, SkipVersionCheck, SkipCertificateCheck, - UseDefaultCredentials, CertificateThumbprint, BaseURI, PVWAAppName, OTP, type, OTPMode, OTPDelimiter, RadiusChallenge, Certificate + UseDefaultCredentials, CertificateThumbprint, BaseURI, PVWAAppName, OTP, type, OTPMode, OTPDelimiter, RadiusChallenge, Certificate, UserName #deal with newPassword SecureString if ($PSBoundParameters.ContainsKey('newPassword')) { @@ -565,7 +574,15 @@ function New-PASSession { } - if ($type -ne 'PKIPN') { + if ($type -eq 'FIDO2') { + # FIDO2 authentication requires username but no password + Assert-VersionRequirement -SelfHosted + + if (-not $PSBoundParameters.Keys.Contains('UserName')) { + throw 'Username is required for FIDO2 authentication. Use -UserName parameter.' + } + + } elseif ($type -ne 'PKIPN') { if ($PSBoundParameters.Keys.Contains('Credential')) { #Add user name from credential object @@ -649,6 +666,11 @@ function New-PASSession { $PASSession = New-IDPlatformToken -tenant_url $LogonRequest['Uri'] -Credential $LogonRequest['Credential'] break } + ( { $type -eq 'FIDO2' } ) { + # Perform FIDO2 Authentication + $PASSession = Invoke-FIDO2Authentication -BaseURI $Uri -UserName $UserName -LogonRequest $LogonRequest + break + } default { #Send Logon Request $PASSession = Invoke-PASRestMethod @LogonRequest diff --git a/psPAS/Private/ConvertFrom-Base64UrlString.ps1 b/psPAS/Private/ConvertFrom-Base64UrlString.ps1 new file mode 100644 index 00000000..4d0717ee --- /dev/null +++ b/psPAS/Private/ConvertFrom-Base64UrlString.ps1 @@ -0,0 +1,45 @@ +function ConvertFrom-Base64UrlString { + <# + .SYNOPSIS + Converts a Base64Url-encoded string to bytes + + .DESCRIPTION + Converts a Base64Url-encoded string (URL-safe Base64 without padding) to a byte array. + This format is used in FIDO2/WebAuthn specifications. + + .PARAMETER InputString + The Base64Url-encoded string to convert + + .EXAMPLE + ConvertFrom-Base64UrlString -InputString 'SGVsbG8gV29ybGQ' + + Converts the Base64Url string to bytes + + .NOTES + Base64Url encoding uses - and _ instead of + and / and removes padding (=) + #> + [CmdletBinding()] + [OutputType([byte[]])] + param( + [Parameter( + Mandatory = $true, + ValueFromPipeline = $true, + ValueFromPipelinebyPropertyName = $true + )] + [string]$InputString + ) + + process { + # Convert Base64Url to standard Base64 + $base64 = $InputString.Replace('-', '+').Replace('_', '/') + + # Add padding if necessary + $padding = 4 - ($base64.Length % 4) + if ($padding -ne 4) { + $base64 += '=' * $padding + } + + # Convert to bytes + [System.Convert]::FromBase64String($base64) + } +} diff --git a/psPAS/Private/Invoke-FIDO2Authentication.ps1 b/psPAS/Private/Invoke-FIDO2Authentication.ps1 new file mode 100644 index 00000000..df135533 --- /dev/null +++ b/psPAS/Private/Invoke-FIDO2Authentication.ps1 @@ -0,0 +1,156 @@ +function Invoke-FIDO2Authentication { + <# + .SYNOPSIS + Performs FIDO2 authentication using DSInternals.Win32.WebAuthn + + .DESCRIPTION + Handles the two-step FIDO2 authentication flow: + 1. Request assertion options from CyberArk API + 2. Use FIDO2 device to generate assertion + 3. Submit assertion back to CyberArk API + + .PARAMETER BaseURI + The base URI for the CyberArk PVWA + + .PARAMETER UserName + The username for FIDO2 authentication + + .PARAMETER LogonRequest + Hashtable containing the logon request parameters + + .EXAMPLE + Invoke-FIDO2Authentication -BaseURI 'https://pvwa.example.com/PasswordVault' -UserName 'administrator' -LogonRequest $request + + .NOTES + Requires Windows 10 1903+ and the DSInternals.Win32.WebAuthn assembly + #> + [CmdletBinding()] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', 'LogonRequest', Justification = 'LogonRequest is used within scriptblock via closure')] + param( + [Parameter(Mandatory = $true)] + [string]$BaseURI, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string]$UserName, + + [Parameter(Mandatory = $false)] + [hashtable]$LogonRequest + ) + + begin { + # FIDO2 requires Windows 10+ + if ((Test-IsCoreCLR) -and -not $IsWindows) { + throw 'FIDO2 authentication is only supported on Windows platforms' + } + + # Load DSInternals.Win32.WebAuthn + try { + $assemblyPath = Join-Path $Script:ModuleRoot 'lib\DSInternals.Win32.WebAuthn.dll' + + if (Test-Path $assemblyPath) { + Add-Type -Path $assemblyPath -ErrorAction Stop + } else { + throw "DSInternals.Win32.WebAuthn assembly not found at: $assemblyPath" + } + } catch { + throw "Failed to load WebAuthn Interop Assembly: $($_.Exception.Message)" + } + } + + process { + try { + # Helper to add LogonRequest parameters to API call parameters + $addLogonParams = { + param($Target) + 'UseDefaultCredentials', 'SkipCertificateCheck', 'Certificate', 'CertificateThumbprint' | ForEach-Object { + if ($LogonRequest.ContainsKey($_)) { $Target[$_] = $LogonRequest[$_] } + } + } + + $challengeParams = @{ + Uri = "$BaseURI/api/auth/fido/logon" + Method = 'POST' + Body = (@{ username = $UserName; type = 'fido' } | ConvertTo-Json) + } + & $addLogonParams $challengeParams + + $challengeResponse = Invoke-PASRestMethod @challengeParams + + $assertionOptions = $challengeResponse.assertionOptions + + # Use FIDO2 device to generate assertion + $webAuthnApi = New-Object DSInternals.Win32.WebAuthn.WebAuthnApi + $challengeBytes = ConvertFrom-Base64UrlString -InputString $assertionOptions.challenge + + # Build allowed credentials list + $allowCredentials = New-Object 'System.Collections.Generic.List[DSInternals.Win32.WebAuthn.PublicKeyCredentialDescriptor]' + if ($assertionOptions.allowCredentials) { + $assertionOptions.allowCredentials | ForEach-Object { + $credId = ConvertFrom-Base64UrlString -InputString $_.id + $allowCredentials.Add((New-Object DSInternals.Win32.WebAuthn.PublicKeyCredentialDescriptor -ArgumentList @( + $credId, + [DSInternals.Win32.WebAuthn.AuthenticatorTransport]::NoRestrictions, + 'public-key' + ))) + } + } + + # Set user verification to required + $userVerification = [DSInternals.Win32.WebAuthn.UserVerificationRequirement]::Required + + # Create CollectedClientData with correct origin (bypasses DSInternals' UriBuilder :80/ issue) + $clientData = New-Object DSInternals.Win32.WebAuthn.FIDO.CollectedClientData + $clientData.Type = 'webauthn.get' + $clientData.Challenge = $challengeBytes + $clientData.Origin = "https://$($assertionOptions.rpId)" + $clientData.CrossOrigin = $false + + $assertion = $webAuthnApi.AuthenticatorGetAssertion( + $assertionOptions.rpId, $clientData, $userVerification, + [DSInternals.Win32.WebAuthn.AuthenticatorAttachment]::Any, 60000, $allowCredentials + ) + + # Build the response payload + $credentialIdBytes = $allowCredentials[0].Id + + $assertionResponse = [ordered]@{ + Id = [Convert]::ToBase64String($credentialIdBytes).Replace('+', '-').Replace('/', '_').TrimEnd('=') + RawId = [Convert]::ToBase64String($credentialIdBytes).Replace('+', '-').Replace('/', '_').TrimEnd('=') + Type = 'public-key' + Extensions = @{} + Response = [ordered]@{ + AuthenticatorData = [Convert]::ToBase64String($assertion.AuthenticatorData).Replace('+', '-').Replace('/', '_').TrimEnd('=') + ClientDataJson = [Convert]::ToBase64String($assertion.ClientDataJson).Replace('+', '-').Replace('/', '_').TrimEnd('=') + Signature = [Convert]::ToBase64String($assertion.Signature).Replace('+', '-').Replace('/', '_').TrimEnd('=') + UserHandle = $null + } + } + + if ($assertion.UserHandle -and $assertion.UserHandle.Length -gt 0) { + $assertionResponse.Response.UserHandle = [Convert]::ToBase64String($assertion.UserHandle).Replace('+', '-').Replace('/', '_').TrimEnd('=') + } + + $additionalInfo = $assertionResponse | ConvertTo-Json -Depth 10 -Compress + $additionalInfoBase64 = [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($additionalInfo)).Replace('+', '-').Replace('/', '_').TrimEnd('=') + + # Submit assertion to CyberArk API + $authParams = @{ + Uri = "$BaseURI/api/auth/fido/logon" + Method = 'POST' + Body = (@{ userName = $UserName; AdditionalInfo = $additionalInfoBase64 } | ConvertTo-Json) + SessionVariable = 'FIDOSession' + } + & $addLogonParams $authParams + + $authResponse = Invoke-PASRestMethod @authParams + + return $authResponse + + } catch { + throw "FIDO2 authentication failed: $($_.Exception.Message)" + } + } + + end {} +} diff --git a/psPAS/en-US/psPAS-help.xml b/psPAS/en-US/psPAS-help.xml index 1f7927cd..57563779 100644 --- a/psPAS/en-US/psPAS-help.xml +++ b/psPAS/en-US/psPAS-help.xml @@ -29332,10 +29332,11 @@ PS C:\> New-PASReportSchedule -version 1 -type 'Report' -subType 'CyberArk.Re When using the Gen2 API, specify the type of authentication to use. Valid values are: - CyberArk - LDAP - - Windows - - Minimum version required 10.4 - RADIUS + - Windows (Minimum version required 10.4) + - RADIUS - PKI - PKIPN + - FIDO2 (Minimum version required 14.4) String @@ -29459,6 +29460,18 @@ PS C:\> New-PASReportSchedule -version 1 -type 'Report' -subType 'CyberArk.Re False + + UserName + + The username for FIDO2 authentication. When using `-type FIDO2`, specify the username with this parameter (required). The username identifies the user and their registered security keys. + + String + + String + + + None + New-PASSession @@ -30024,10 +30037,11 @@ PS C:\> New-PASReportSchedule -version 1 -type 'Report' -subType 'CyberArk.Re When using the Gen2 API, specify the type of authentication to use. Valid values are: - CyberArk - LDAP - - Windows - - Minimum version required 10.4 - RADIUS + - Windows (Minimum version required 10.4) + - RADIUS - PKI - PKIPN + - FIDO2 (Minimum version required 14.4) String @@ -31163,10 +31177,11 @@ PS C:\> New-PASReportSchedule -version 1 -type 'Report' -subType 'CyberArk.Re When using the Gen2 API, specify the type of authentication to use. Valid values are: - CyberArk - LDAP - - Windows - - Minimum version required 10.4 - RADIUS + - Windows (Minimum version required 10.4) + - RADIUS - PKI - PKIPN + - FIDO2 (Minimum version required 14.4) String @@ -31452,6 +31467,18 @@ PS C:\> New-PASReportSchedule -version 1 -type 'Report' -subType 'CyberArk.Re False + + UserName + + The username for FIDO2 authentication. When using `-type FIDO2`, specify the username with this parameter (required). The username identifies the user and their registered security keys. + + String + + String + + + None + @@ -31713,6 +31740,13 @@ New-PASSession -BaseURI $url -type PKIPN -Certificate $Cert See: Get-Help IdentityCommand + + -------------------------- EXAMPLE 31 -------------------------- + New-PASSession -BaseURI https://pvwa.company.com -type FIDO2 -UserName administrator + + Authenticates to CyberArk using FIDO2/WebAuthn hardware security key authentication. + + diff --git a/psPAS/lib/DSInternals.LICENSE b/psPAS/lib/DSInternals.LICENSE new file mode 100644 index 00000000..699d5dfb --- /dev/null +++ b/psPAS/lib/DSInternals.LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2015-2025 Michael Grafnetter + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/psPAS/lib/DSInternals.Win32.WebAuthn.dll b/psPAS/lib/DSInternals.Win32.WebAuthn.dll new file mode 100644 index 00000000..7dfde562 Binary files /dev/null and b/psPAS/lib/DSInternals.Win32.WebAuthn.dll differ diff --git a/psPAS/psPAS.psm1 b/psPAS/psPAS.psm1 index 7c05cace..6a166a74 100644 --- a/psPAS/psPAS.psm1 +++ b/psPAS/psPAS.psm1 @@ -55,6 +55,9 @@ Get-ChildItem $PSScriptRoot\ -Recurse -Include '*.ps1' -Exclude '*.ps1xml' | } +# Store module root path for use in functions (like FIDO2) +$Script:ModuleRoot = $PSScriptRoot + # Script scope session object for session data $psPASSession = [ordered]@{ BaseURI = $null