diff --git a/CHANGELOG.md b/CHANGELOG.md
index 26d33cd..1aee181 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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
diff --git a/IdentityCommand/Public/New-IDSession.ps1 b/IdentityCommand/Public/New-IDSession.ps1
index d02ae85..083f055 100644
--- a/IdentityCommand/Public/New-IDSession.ps1
+++ b/IdentityCommand/Public/New-IDSession.ps1
@@ -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
diff --git a/IdentityCommand/en-US/IdentityCommand-help.xml b/IdentityCommand/en-US/IdentityCommand-help.xml
index ac6db53..91febe2 100644
--- a/IdentityCommand/en-US/IdentityCommand-help.xml
+++ b/IdentityCommand/en-US/IdentityCommand-help.xml
@@ -1250,6 +1250,7 @@ LastCommandResults {"success":true,"Result":{"SomeResult"}}Allows a user to provide authentication details, and satisfy any required MFA challenges.
Currently supports all Identity MFA authentication mechanisms except U2F & DUO.
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.
+ 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.
diff --git a/Tests/New-IDSession.Tests.ps1 b/Tests/New-IDSession.Tests.ps1
index de6d68b..99979f6 100644
--- a/Tests/New-IDSession.Tests.ps1
+++ b/Tests/New-IDSession.Tests.ps1
@@ -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 {
diff --git a/docs/collections/_commands/New-IDSession.md b/docs/collections/_commands/New-IDSession.md
index 8793501..9e9c679 100644
--- a/docs/collections/_commands/New-IDSession.md
+++ b/docs/collections/_commands/New-IDSession.md
@@ -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