Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ All notable changes to this project will be documented in this file.
### Changed
- N/A

### Fixed
- `New-IDSession`: Adds support for OOB IdP Authentication flows that require a PIN code.
- Tenants configured to display a PIN in the browser after external IdP login are now prompted for the PIN and completed via `AdvanceAuthentication`. Previously these tenants would hang in the `OobAuthStatus` polling loop with no way to enter the PIN.

## [0.3] - 2025-03-09

### Added
Expand Down
62 changes: 52 additions & 10 deletions IdentityCommand/Public/New-IDSession.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -96,20 +96,62 @@ $($IDSession.IdpRedirectShortUrl)
#Launches the user's default browser and navigates it to the external identity provider
Start-Process $IDSession.IdpRedirectShortUrl

$OobAuthStatusRequest = @{ }
$OobAuthStatusRequest['Method'] = 'POST'
#Undocumented endpoint for checking the IdpLoginSessionId's status. Sniffed out from the ark-sdk-python project
$OobAuthStatusRequest['Uri'] = "$tenant_url/Security/OobAuthStatus"
#We need the cookies the server provides in the same response it provides the IdpAuth information
$OobAuthStatusRequest['WebSession'] = $ISPSSSession.WebSession
$OobAuthStatusRequest['Body'] = @{SessionId = $IDSession.IdpLoginSessionId} | ConvertTo-Json
if ($IDSession.IdpOobAuthPinRequired) {

$IDSession = Invoke-IDRestMethod @OobAuthStatusRequest
#A PIN code is displayed in the browser after IdP login and must be entered here
#to complete authentication via AdvanceAuthentication.
$Pin = Read-Host -Prompt 'Enter the PIN code displayed after you logged in to your identity provider' -AsSecureString

while ($IDSession.State -ne 'Success') {
Start-Sleep 2
$OobPinAuthRequest = @{ }
$OobPinAuthRequest['Method'] = 'POST'
#Use the session tenant_url so that any PodFqdn redirect from Start-Authentication is honoured
$OobPinAuthRequest['Uri'] = "$($ISPSSSession.tenant_url)/Security/AdvanceAuthentication"
$OobPinAuthRequest['WebSession'] = $ISPSSSession.WebSession
$OobPinAuthRequest['Body'] = @{
SessionId = $IDSession.IdpLoginSessionId
MechanismId = 'OOBAUTHPIN'
Action = 'Answer'
Answer = Unprotect-Answer $Pin
} | ConvertTo-Json

try {

$IDSession = Invoke-IDRestMethod @OobPinAuthRequest

if ($IDSession.Summary -ne 'LoginSuccess' -or -not $IDSession.Token) {

throw 'Failed to complete OOB IdP authentication with PIN code'

}

} catch {

#Cleanup Authentication on any error at the PIN submission stage
Clear-AdvanceAuthentication

throw $PSItem

}

} else {

$OobAuthStatusRequest = @{ }
$OobAuthStatusRequest['Method'] = 'POST'
#Undocumented endpoint for checking the IdpLoginSessionId's status. Sniffed out from the ark-sdk-python project
#Use the session tenant_url so that any PodFqdn redirect from Start-Authentication is honoured
$OobAuthStatusRequest['Uri'] = "$($ISPSSSession.tenant_url)/Security/OobAuthStatus"
#We need the cookies the server provides in the same response it provides the IdpAuth information
$OobAuthStatusRequest['WebSession'] = $ISPSSSession.WebSession
$OobAuthStatusRequest['Body'] = @{SessionId = $IDSession.IdpLoginSessionId} | ConvertTo-Json

$IDSession = Invoke-IDRestMethod @OobAuthStatusRequest

while ($IDSession.State -ne 'Success') {
Start-Sleep 2

$IDSession = Invoke-IDRestMethod @OobAuthStatusRequest
}

}

break
Expand Down
1 change: 1 addition & 0 deletions IdentityCommand/en-US/IdentityCommand-help.xml
Original file line number Diff line number Diff line change
Expand Up @@ -1250,6 +1250,7 @@ LastCommandResults {"success":true,"Result":{"SomeResult"}}</dev:cod
<maml:para>Allows a user to provide authentication details, and satisfy any required MFA challenges.</maml:para>
<maml:para>Currently supports all Identity MFA authentication mechanisms except U2F &amp; DUO.</maml:para>
<maml:para>When you specify a username associated with a SAML or OIDC-based federation, then you will be redirected to the external identity provider to authenticate. Alternatively, you can provide a SamlAssertion from a configured external IDP.</maml:para>
<maml:para>If your tenant is configured to require a PIN code after external identity provider authentication, you will be prompted to enter the PIN code displayed in the browser to complete the sign-in.</maml:para>
</maml:description>
<command:syntax>
<command:syntaxItem>
Expand Down
215 changes: 215 additions & 0 deletions Tests/New-IDSession.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,221 @@ Describe $($PSCommandPath -Replace '.Tests.ps1') {

}

Context 'OOB IdP Authentication' {

BeforeEach {

Mock Start-Process -MockWith {}

#Downstream switch expects a WebSession on successful auth
Mock Invoke-IDRestMethod -MockWith {
$ISPSSSession.WebSession = New-Object Microsoft.PowerShell.Commands.WebRequestSession
[pscustomobject]@{
Summary = 'LoginSuccess'
Token = 'SomeToken'
State = 'Success'
}
}

}

Context 'PIN required' {

BeforeEach {

Mock Start-Authentication -MockWith {
[pscustomobject]@{
TenantId = 'SomeID'
SessionId = 'SomeSession'
IdpRedirectShortUrl = 'https://short.example/x'
IdpLoginSessionId = 'IDP-123'
IdpOobAuthPinRequired = $true
}
}

Mock Read-Host -MockWith {
ConvertTo-SecureString 'TEST-PIN' -AsPlainText -Force
}

}

It 'launches the browser to the IdpRedirectShortUrl' {
New-IDSession -tenant_url https://somedomain.id.cyberark.cloud -Credential $Creds
Assert-MockCalled -CommandName Start-Process -Times 1 -Exactly -Scope It -ParameterFilter {
$args[0] -eq 'https://short.example/x' -or $FilePath -eq 'https://short.example/x'
}
}

It 'prompts for the PIN as a secure string' {
New-IDSession -tenant_url https://somedomain.id.cyberark.cloud -Credential $Creds
Assert-MockCalled -CommandName Read-Host -Times 1 -Exactly -Scope It -ParameterFilter {
$AsSecureString -eq $true
}
}

It 'sends the PIN to AdvanceAuthentication with the OOBAUTHPIN mechanism' {
New-IDSession -tenant_url https://somedomain.id.cyberark.cloud -Credential $Creds
Assert-MockCalled -CommandName Invoke-IDRestMethod -Times 1 -Exactly -Scope It -ParameterFilter {
$Uri -eq 'https://somedomain.id.cyberark.cloud/Security/AdvanceAuthentication' -and
$Method -eq 'POST' -and
($Body | ConvertFrom-Json).MechanismId -eq 'OOBAUTHPIN' -and
($Body | ConvertFrom-Json).Action -eq 'Answer' -and
($Body | ConvertFrom-Json).Answer -eq 'TEST-PIN'
}
}

It 'sends the IdpLoginSessionId as the SessionId in the PIN request' {
New-IDSession -tenant_url https://somedomain.id.cyberark.cloud -Credential $Creds
Assert-MockCalled -CommandName Invoke-IDRestMethod -Times 1 -Exactly -Scope It -ParameterFilter {
$Uri -match 'AdvanceAuthentication$' -and
($Body | ConvertFrom-Json).SessionId -eq 'IDP-123'
}
}

It 'does not call OobAuthStatus when a PIN is required' {
New-IDSession -tenant_url https://somedomain.id.cyberark.cloud -Credential $Creds
Assert-MockCalled -CommandName Invoke-IDRestMethod -Times 0 -Exactly -Scope It -ParameterFilter {
$Uri -match 'OobAuthStatus$'
}
}

It 'does not invoke Start-AdvanceAuthentication (MFA challenge loop is bypassed)' {
New-IDSession -tenant_url https://somedomain.id.cyberark.cloud -Credential $Creds
Assert-MockCalled -CommandName Start-AdvanceAuthentication -Times 0 -Exactly -Scope It
}

It 'builds the PIN request URI from ISPSSSession.tenant_url after a PodFqdn redirect' {
Mock Start-Authentication -MockWith {
#Simulate the PodFqdn redirect performed by Start-Authentication
$ISPSSSession.tenant_url = 'https://pod.id.cyberark.cloud'
[pscustomobject]@{
TenantId = 'SomeID'
SessionId = 'SomeSession'
IdpRedirectShortUrl = 'https://short.example/x'
IdpLoginSessionId = 'IDP-123'
IdpOobAuthPinRequired = $true
}
}
New-IDSession -tenant_url https://somedomain.id.cyberark.cloud -Credential $Creds
Assert-MockCalled -CommandName Invoke-IDRestMethod -Times 1 -Exactly -Scope It -ParameterFilter {
$Uri -eq 'https://pod.id.cyberark.cloud/Security/AdvanceAuthentication'
}
}

}

Context 'PIN required - error handling' {

BeforeEach {

Mock Start-Authentication -MockWith {
[pscustomobject]@{
TenantId = 'SomeID'
SessionId = 'SomeSession'
IdpRedirectShortUrl = 'https://short.example/x'
IdpLoginSessionId = 'IDP-123'
IdpOobAuthPinRequired = $true
}
}

Mock Read-Host -MockWith {
ConvertTo-SecureString 'TEST-PIN' -AsPlainText -Force
}

Mock Clear-AdvanceAuthentication -MockWith {}

}

It 'invokes Clear-AdvanceAuthentication and re-throws on API error' {
Mock Invoke-IDRestMethod -MockWith { throw 'Wrong PIN' }
{ New-IDSession -tenant_url https://somedomain.id.cyberark.cloud -Credential $Creds } |
Should -Throw -ExpectedMessage 'Wrong PIN'
Assert-MockCalled -CommandName Clear-AdvanceAuthentication -Times 1 -Exactly -Scope It
}

It 'invokes Clear-AdvanceAuthentication and throws on non-LoginSuccess response' {
Mock Invoke-IDRestMethod -MockWith {
[pscustomobject]@{
Summary = 'SomeOtherSummary'
Token = 'SomeToken'
}
}
{ New-IDSession -tenant_url https://somedomain.id.cyberark.cloud -Credential $Creds } |
Should -Throw
Assert-MockCalled -CommandName Clear-AdvanceAuthentication -Times 1 -Exactly -Scope It
}

It 'invokes Clear-AdvanceAuthentication and throws when Token is missing' {
Mock Invoke-IDRestMethod -MockWith {
[pscustomobject]@{
Summary = 'LoginSuccess'
}
}
{ New-IDSession -tenant_url https://somedomain.id.cyberark.cloud -Credential $Creds } |
Should -Throw
Assert-MockCalled -CommandName Clear-AdvanceAuthentication -Times 1 -Exactly -Scope It
}

}

Context 'Polling (no PIN required)' {

BeforeEach {

Mock Start-Authentication -MockWith {
[pscustomobject]@{
TenantId = 'SomeID'
SessionId = 'SomeSession'
IdpRedirectShortUrl = 'https://short.example/x'
IdpLoginSessionId = 'IDP-123'
}
}

}

It 'polls OobAuthStatus with the IdpLoginSessionId' {
New-IDSession -tenant_url https://somedomain.id.cyberark.cloud -Credential $Creds
Assert-MockCalled -CommandName Invoke-IDRestMethod -Times 1 -Exactly -Scope It -ParameterFilter {
$Uri -eq 'https://somedomain.id.cyberark.cloud/Security/OobAuthStatus' -and
$Method -eq 'POST' -and
($Body | ConvertFrom-Json).SessionId -eq 'IDP-123'
}
}

It 'does not call AdvanceAuthentication when no PIN is required' {
New-IDSession -tenant_url https://somedomain.id.cyberark.cloud -Credential $Creds
Assert-MockCalled -CommandName Invoke-IDRestMethod -Times 0 -Exactly -Scope It -ParameterFilter {
$Uri -match 'AdvanceAuthentication$'
}
}

It 'does not prompt the user for a PIN' {
Mock Read-Host -MockWith { throw 'Read-Host should not be called in the polling path' }
{ New-IDSession -tenant_url https://somedomain.id.cyberark.cloud -Credential $Creds } |
Should -Not -Throw
}

It 'builds the polling request URI from ISPSSSession.tenant_url after a PodFqdn redirect' {
Mock Start-Authentication -MockWith {
#Simulate the PodFqdn redirect performed by Start-Authentication
$ISPSSSession.tenant_url = 'https://pod.id.cyberark.cloud'
[pscustomobject]@{
TenantId = 'SomeID'
SessionId = 'SomeSession'
IdpRedirectShortUrl = 'https://short.example/x'
IdpLoginSessionId = 'IDP-123'
}
}
New-IDSession -tenant_url https://somedomain.id.cyberark.cloud -Credential $Creds
Assert-MockCalled -CommandName Invoke-IDRestMethod -Times 1 -Exactly -Scope It -ParameterFilter {
$Uri -eq 'https://pod.id.cyberark.cloud/Security/OobAuthStatus'
}
}

}

}

Context 'Output' {

BeforeEach {
Expand Down
2 changes: 2 additions & 0 deletions docs/collections/_commands/New-IDSession.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ Currently supports all Identity MFA authentication mechanisms except U2F & DUO.

Supports federated authentication when providing a SamlAssertion from a configured external IDP.

If your tenant is configured to require a PIN code after external identity provider authentication, you will be prompted to enter the PIN code displayed in the browser to complete the sign-in.

## EXAMPLES

### Example 1
Expand Down