From c8e9f67e6347e9c137872d4579eefbdc9edd7bd8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 8 Feb 2026 19:40:48 +0000 Subject: [PATCH 01/13] Initial plan From d6d8cecb1b785dbdd825520cc58908103e7eed88 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 8 Feb 2026 19:47:04 +0000 Subject: [PATCH 02/13] core: implement per-entry AuthSessionType support in broker - Add Assert-IdleAuthSessionMatchesType validation function - Make -AuthSessionType optional, acts as default - Support typed SessionMap values: @{ AuthSessionType = 'X'; Session = Y } - Normalize SessionMap internally with Session + Type - Validate session types before returning from AcquireAuthSession - Update wrapper New-IdleAuthSession to pass through optional parameter - Add comprehensive tests for typed sessions, mixed types, validation - Maintain backward compatibility with untyped values + required type Co-authored-by: blindzero <13959569+blindzero@users.noreply.github.com> --- .../Assert-IdleAuthSessionMatchesType.ps1 | 79 ++++++ .../Public/New-IdleAuthSessionBroker.ps1 | 164 ++++++++++-- src/IdLE/Public/New-IdleAuthSession.ps1 | 16 +- tests/Core/New-IdleAuthSession.Tests.ps1 | 233 +++++++++++++++++- 4 files changed, 464 insertions(+), 28 deletions(-) create mode 100644 src/IdLE.Core/Private/Assert-IdleAuthSessionMatchesType.ps1 diff --git a/src/IdLE.Core/Private/Assert-IdleAuthSessionMatchesType.ps1 b/src/IdLE.Core/Private/Assert-IdleAuthSessionMatchesType.ps1 new file mode 100644 index 00000000..eef774fe --- /dev/null +++ b/src/IdLE.Core/Private/Assert-IdleAuthSessionMatchesType.ps1 @@ -0,0 +1,79 @@ +function Assert-IdleAuthSessionMatchesType { + <# + .SYNOPSIS + Validates that an auth session object matches the expected AuthSessionType. + + .DESCRIPTION + Validates that an auth session object's runtime type is compatible with the + declared AuthSessionType. This ensures that providers receive the expected + session format for authentication. + + .PARAMETER AuthSessionType + The expected authentication session type. + + .PARAMETER Session + The authentication session object to validate. + + .PARAMETER SessionName + The session name for error messages. + + .OUTPUTS + None. Throws if validation fails. + #> + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [ValidateSet('OAuth', 'PSRemoting', 'Credential')] + [string] $AuthSessionType, + + [Parameter(Mandatory)] + [AllowNull()] + [object] $Session, + + [Parameter()] + [string] $SessionName = '' + ) + + if ($null -eq $Session) { + throw "Auth session validation failed for '$SessionName': Session object is null." + } + + switch ($AuthSessionType) { + 'Credential' { + if ($Session -isnot [pscredential]) { + $actualType = $Session.GetType().FullName + throw "Auth session validation failed for '$SessionName': Expected AuthSessionType='Credential' requires a [PSCredential] object, but received [$actualType]." + } + } + + 'OAuth' { + # Accept string tokens as the primary OAuth session format + if ($Session -isnot [string]) { + $actualType = $Session.GetType().FullName + throw "Auth session validation failed for '$SessionName': Expected AuthSessionType='OAuth' requires a [string] token, but received [$actualType]." + } + } + + 'PSRemoting' { + # Accept PSSession objects or PSCredential for PSRemoting scenarios + $validTypes = @( + [System.Management.Automation.Runspaces.PSSession] + [pscredential] + ) + + $isValid = $false + foreach ($validType in $validTypes) { + if ($Session -is $validType) { + $isValid = $true + break + } + } + + if (-not $isValid) { + $actualType = $Session.GetType().FullName + $expectedTypes = ($validTypes | ForEach-Object { "[$($_.FullName)]" }) -join ' or ' + throw "Auth session validation failed for '$SessionName': Expected AuthSessionType='PSRemoting' requires $expectedTypes, but received [$actualType]." + } + } + } +} diff --git a/src/IdLE.Core/Public/New-IdleAuthSessionBroker.ps1 b/src/IdLE.Core/Public/New-IdleAuthSessionBroker.ps1 index 09410fdf..d5d42cc3 100644 --- a/src/IdLE.Core/Public/New-IdleAuthSessionBroker.ps1 +++ b/src/IdLE.Core/Public/New-IdleAuthSessionBroker.ps1 @@ -15,8 +15,15 @@ function New-IdleAuthSessionBroker { .PARAMETER SessionMap Optional hashtable that maps session configurations to auth sessions. Each key is a hashtable representing the AuthSessionOptions pattern, and each value is the auth session to return. - The value can be a PSCredential, token string, session object, or any object appropriate - for the AuthSessionType. + + Values can be specified in two formats: + + 1. Legacy/Untyped (requires -AuthSessionType): + - Direct session object: @{ Role = 'Admin' } = $credential + + 2. Typed (supports mixed types, -AuthSessionType acts as default): + - Hashtable: @{ Role = 'Admin' } = @{ AuthSessionType = 'Credential'; Session = $credential } + - PSCustomObject: @{ Role = 'Admin' } = [pscustomobject]@{ AuthSessionType = 'OAuth'; Session = $token } Keys can include AuthSessionName for name-based routing: - @{ AuthSessionName = 'AD'; Role = 'ADAdm' } -> $admAD (AuthSessionName + Role routing) @@ -30,13 +37,17 @@ function New-IdleAuthSessionBroker { .PARAMETER DefaultAuthSession Optional default auth session to return when no session options are provided or - when the options don't match any entry in SessionMap. Can be a PSCredential, token - string, session object, or any object appropriate for the AuthSessionType. + when the options don't match any entry in SessionMap. + + Can be specified in two formats: + 1. Legacy/Untyped (requires -AuthSessionType): $credential + 2. Typed: @{ AuthSessionType = 'Credential'; Session = $credential } At least one of SessionMap or DefaultAuthSession must be provided. .PARAMETER AuthSessionType - Specifies the type of authentication session. This determines validation rules, + Optional default authentication session type. Acts as the default for untyped + SessionMap entries and DefaultAuthSession. This determines validation rules, lifecycle management, and telemetry behavior. Valid values: @@ -44,6 +55,9 @@ function New-IdleAuthSessionBroker { - 'PSRemoting': PowerShell remoting execution context (e.g., Entra Connect) - 'Credential': Credential-based authentication (e.g., Active Directory, mock providers) + If not provided, all SessionMap values and DefaultAuthSession must be typed + (include AuthSessionType and Session properties). + .EXAMPLE # Simple single-credential broker (no SessionMap required) $broker = New-IdleAuthSessionBroker -DefaultAuthSession $admCred -AuthSessionType 'Credential' @@ -86,6 +100,20 @@ function New-IdleAuthSessionBroker { @{ Environment = 'Test' } = $testCred } -DefaultAuthSession $devCred -AuthSessionType 'Credential' + .EXAMPLE + # Mixed-type broker for AD (Credential) + EXO (OAuth) in single workflow + $broker = New-IdleAuthSessionBroker -SessionMap @{ + @{ AuthSessionName = 'AD' } = @{ AuthSessionType = 'Credential'; Session = $adCred } + @{ AuthSessionName = 'EXO' } = @{ AuthSessionType = 'OAuth'; Session = $exoToken } + } + + .EXAMPLE + # Mixed typed and untyped with default AuthSessionType + $broker = New-IdleAuthSessionBroker -SessionMap @{ + @{ AuthSessionName = 'AD' } = $adCred # Uses default 'Credential' + @{ AuthSessionName = 'EXO' } = @{ AuthSessionType = 'OAuth'; Session = $exoToken } + } -AuthSessionType 'Credential' + .OUTPUTS PSCustomObject with AcquireAuthSession method #> @@ -100,7 +128,7 @@ function New-IdleAuthSessionBroker { [AllowNull()] [object] $DefaultAuthSession, - [Parameter(Mandatory)] + [Parameter()] [ValidateSet('OAuth', 'PSRemoting', 'Credential')] [string] $AuthSessionType ) @@ -110,11 +138,90 @@ function New-IdleAuthSessionBroker { throw "SessionMap is empty or null. DefaultAuthSession must be provided when SessionMap is not used." } + # Helper function to detect if a value is a typed session descriptor + $isTypedSession = { + param($value) + + if ($null -eq $value) { + return $false + } + + # Check for hashtable with AuthSessionType and Session keys + if ($value -is [hashtable]) { + return ($value.ContainsKey('AuthSessionType') -and $value.ContainsKey('Session')) + } + + # Check for PSCustomObject with AuthSessionType and Session properties + if ($value -is [pscustomobject]) { + $properties = $value.PSObject.Properties.Name + return (($properties -contains 'AuthSessionType') -and ($properties -contains 'Session')) + } + + return $false + } + + # Helper function to normalize session value to internal format + $normalizeSessionValue = { + param($value, $defaultType, $context) + + if ($null -eq $value) { + return $null + } + + # Check if value is already typed + if (& $isTypedSession $value) { + $sessionType = $value.AuthSessionType + $session = $value.Session + + # Validate the provided AuthSessionType + if ($sessionType -notin @('OAuth', 'PSRemoting', 'Credential')) { + throw "Invalid AuthSessionType '$sessionType' in $context. Valid values: 'OAuth', 'PSRemoting', 'Credential'." + } + + return @{ + AuthSessionType = $sessionType + Session = $session + } + } + + # Untyped value - use default type + if ([string]::IsNullOrEmpty($defaultType)) { + throw "Untyped session value found in $context, but no default -AuthSessionType provided. Either provide -AuthSessionType or use typed session values: @{ AuthSessionType = ''; Session = }" + } + + return @{ + AuthSessionType = $defaultType + Session = $value + } + } + + # Normalize SessionMap entries + $normalizedSessionMap = @{} + if ($null -ne $SessionMap -and $SessionMap.Count -gt 0) { + foreach ($entry in $SessionMap.GetEnumerator()) { + $pattern = $entry.Key + $value = $entry.Value + + # Create a readable pattern description for error messages + $patternDesc = ($pattern.Keys | ForEach-Object { "$_=$($pattern[$_])" }) -join ', ' + $context = "SessionMap entry { $patternDesc }" + + $normalizedValue = & $normalizeSessionValue $value $AuthSessionType $context + $normalizedSessionMap[$pattern] = $normalizedValue + } + } + + # Normalize DefaultAuthSession + $normalizedDefaultAuthSession = $null + if ($null -ne $DefaultAuthSession) { + $normalizedDefaultAuthSession = & $normalizeSessionValue $DefaultAuthSession $AuthSessionType 'DefaultAuthSession' + } + $broker = [pscustomobject]@{ PSTypeName = 'IdLE.AuthSessionBroker' - SessionMap = $SessionMap - DefaultAuthSession = $DefaultAuthSession - AuthSessionType = $AuthSessionType + SessionMap = $normalizedSessionMap + DefaultAuthSession = $normalizedDefaultAuthSession + AuthSessionType = $AuthSessionType # Store for backward compatibility } $broker | Add-Member -MemberType ScriptMethod -Name AcquireAuthSession -Value { @@ -128,17 +235,16 @@ function New-IdleAuthSessionBroker { [hashtable] $Options ) - # TODO: Implement type-specific validation rules for AuthSessionType - # Current implementation allows all options for all session types - # Future enhancements may add: - # - OAuth: Validate token format, expiration, scopes - # - PSRemoting: Validate remote session state, connectivity - # - Credential: Validate credential format, domain membership - # Empty string signals default session request if ([string]::IsNullOrEmpty($Name)) { if ($null -ne $this.DefaultAuthSession) { - return $this.DefaultAuthSession + $normalized = $this.DefaultAuthSession + + # Validate type before returning + $validationScript = (Get-Command -Name 'Assert-IdleAuthSessionMatchesType' -ErrorAction Stop).ScriptBlock + & $validationScript -AuthSessionType $normalized.AuthSessionType -Session $normalized.Session -SessionName '' + + return $normalized.Session } throw "No default auth session configured." } @@ -146,7 +252,13 @@ function New-IdleAuthSessionBroker { # If SessionMap is null or empty, return default if ($null -eq $this.SessionMap -or $this.SessionMap.Count -eq 0) { if ($null -ne $this.DefaultAuthSession) { - return $this.DefaultAuthSession + $normalized = $this.DefaultAuthSession + + # Validate type before returning + $validationScript = (Get-Command -Name 'Assert-IdleAuthSessionMatchesType' -ErrorAction Stop).ScriptBlock + & $validationScript -AuthSessionType $normalized.AuthSessionType -Session $normalized.Session -SessionName $Name + + return $normalized.Session } throw "No SessionMap configured and no default auth session available." } @@ -228,7 +340,13 @@ function New-IdleAuthSessionBroker { # Return first match if exactly one found if ($matchingEntries.Count -eq 1) { - return $matchingEntries[0].Value + $normalized = $matchingEntries[0].Value + + # Validate type before returning + $validationScript = (Get-Command -Name 'Assert-IdleAuthSessionMatchesType' -ErrorAction Stop).ScriptBlock + & $validationScript -AuthSessionType $normalized.AuthSessionType -Session $normalized.Session -SessionName $Name + + return $normalized.Session } # If multiple matches, this is ambiguous - fail with clear error @@ -243,7 +361,13 @@ function New-IdleAuthSessionBroker { # No match found - fall back to default if ($null -ne $this.DefaultAuthSession) { - return $this.DefaultAuthSession + $normalized = $this.DefaultAuthSession + + # Validate type before returning + $validationScript = (Get-Command -Name 'Assert-IdleAuthSessionMatchesType' -ErrorAction Stop).ScriptBlock + & $validationScript -AuthSessionType $normalized.AuthSessionType -Session $normalized.Session -SessionName $Name + + return $normalized.Session } # No match and no default diff --git a/src/IdLE/Public/New-IdleAuthSession.ps1 b/src/IdLE/Public/New-IdleAuthSession.ps1 index 5f5b3fd8..66858518 100644 --- a/src/IdLE/Public/New-IdleAuthSession.ps1 +++ b/src/IdLE/Public/New-IdleAuthSession.ps1 @@ -25,14 +25,17 @@ function New-IdleAuthSession { Optional default auth session to return when no session options are provided. .PARAMETER AuthSessionType - Specifies the type of authentication session. This determines validation rules, - lifecycle management, and telemetry behavior. + Optional default authentication session type. Acts as the default for untyped + SessionMap entries and DefaultAuthSession. Valid values: - 'OAuth': Token-based authentication (e.g., Microsoft Graph, Exchange Online) - 'PSRemoting': PowerShell remoting execution context (e.g., Entra Connect) - 'Credential': Credential-based authentication (e.g., Active Directory, mock providers) + If not provided, all SessionMap values and DefaultAuthSession must be typed + (include AuthSessionType and Session properties). + .EXAMPLE $broker = New-IdleAuthSession -SessionMap @{ @{ Role = 'Tier0' } = $tier0Credential @@ -55,21 +58,22 @@ function New-IdleAuthSession { [AllowNull()] [object] $DefaultAuthSession, - [Parameter(Mandatory)] + [Parameter()] [ValidateSet('OAuth', 'PSRemoting', 'Credential')] [string] $AuthSessionType ) # Delegate to IdLE.Core implementation. - $params = @{ - AuthSessionType = $AuthSessionType - } + $params = @{} if ($PSBoundParameters.ContainsKey('SessionMap')) { $params['SessionMap'] = $SessionMap } if ($PSBoundParameters.ContainsKey('DefaultAuthSession')) { $params['DefaultAuthSession'] = $DefaultAuthSession } + if ($PSBoundParameters.ContainsKey('AuthSessionType')) { + $params['AuthSessionType'] = $AuthSessionType + } return IdLE.Core\New-IdleAuthSessionBroker @params } diff --git a/tests/Core/New-IdleAuthSession.Tests.ps1 b/tests/Core/New-IdleAuthSession.Tests.ps1 index 1518768b..ba6dbced 100644 --- a/tests/Core/New-IdleAuthSession.Tests.ps1 +++ b/tests/Core/New-IdleAuthSession.Tests.ps1 @@ -45,7 +45,9 @@ Describe 'New-IdleAuthSession' { } -DefaultAuthSession $testCred -AuthSessionType 'Credential' $broker.DefaultAuthSession | Should -Not -BeNullOrEmpty - $broker.DefaultAuthSession.UserName | Should -Be 'TestUser' + # Note: DefaultAuthSession is now normalized internally, test via AcquireAuthSession + $session = $broker.AcquireAuthSession('', $null) + $session.UserName | Should -Be 'TestUser' } It 'broker can acquire auth session with matching options' { @@ -140,12 +142,14 @@ Describe 'New-IdleAuthSession' { Context 'AuthSessionType validation during acquisition' { It 'OAuth broker can acquire sessions with appropriate options' { + $oauthToken = 'mock-oauth-token' $broker = New-IdleAuthSession -SessionMap @{ - @{ Role = 'Admin' } = $testCred + @{ Role = 'Admin' } = $oauthToken } -AuthSessionType 'OAuth' $session = $broker.AcquireAuthSession('MicrosoftGraph', @{ Role = 'Admin' }) $session | Should -Not -BeNullOrEmpty + $session | Should -BeOfType [string] } It 'PSRemoting broker can acquire sessions with appropriate options' { @@ -155,6 +159,7 @@ Describe 'New-IdleAuthSession' { $session = $broker.AcquireAuthSession('EntraConnect', @{ Server = 'AADConnect01' }) $session | Should -Not -BeNullOrEmpty + $session | Should -BeOfType [PSCredential] } It 'Credential broker can acquire sessions with appropriate options' { @@ -320,4 +325,228 @@ Describe 'New-IdleAuthSession' { $session.UserName | Should -Be 'TestUser' } } + + Context 'Per-entry AuthSessionType support' { + BeforeEach { + $password = ConvertTo-SecureString 'Password!' -AsPlainText -Force + $adCred = New-Object System.Management.Automation.PSCredential('ADUser', $password) + $exoToken = 'mock-oauth-token-12345' + } + + It 'supports typed SessionMap values with AuthSessionType property' { + $broker = New-IdleAuthSession -SessionMap @{ + @{ AuthSessionName = 'AD' } = @{ AuthSessionType = 'Credential'; Session = $adCred } + } + + $session = $broker.AcquireAuthSession('AD', $null) + + $session | Should -Not -BeNullOrEmpty + $session | Should -BeOfType [PSCredential] + $session.UserName | Should -Be 'ADUser' + } + + It 'supports mixed typed and untyped SessionMap values with default AuthSessionType' { + $broker = New-IdleAuthSession -SessionMap @{ + @{ AuthSessionName = 'AD' } = $adCred # Untyped, uses default + @{ AuthSessionName = 'EXO' } = @{ AuthSessionType = 'OAuth'; Session = $exoToken } # Typed + } -AuthSessionType 'Credential' + + $adSession = $broker.AcquireAuthSession('AD', $null) + $adSession | Should -BeOfType [PSCredential] + + $exoSession = $broker.AcquireAuthSession('EXO', $null) + $exoSession | Should -BeOfType [string] + $exoSession | Should -Be 'mock-oauth-token-12345' + } + + It 'supports all typed SessionMap values without -AuthSessionType' { + $broker = New-IdleAuthSession -SessionMap @{ + @{ AuthSessionName = 'AD' } = @{ AuthSessionType = 'Credential'; Session = $adCred } + @{ AuthSessionName = 'EXO' } = @{ AuthSessionType = 'OAuth'; Session = $exoToken } + } + + $adSession = $broker.AcquireAuthSession('AD', $null) + $adSession | Should -BeOfType [PSCredential] + + $exoSession = $broker.AcquireAuthSession('EXO', $null) + $exoSession | Should -BeOfType [string] + } + + It 'throws when untyped SessionMap value provided without -AuthSessionType' { + { + New-IdleAuthSession -SessionMap @{ + @{ AuthSessionName = 'AD' } = $adCred # Untyped + } + } | Should -Throw '*Untyped session value*' + } + + It 'throws when untyped DefaultAuthSession provided without -AuthSessionType' { + { + New-IdleAuthSession -SessionMap @{ + @{ AuthSessionName = 'AD' } = @{ AuthSessionType = 'Credential'; Session = $adCred } + } -DefaultAuthSession $adCred # Untyped default + } | Should -Throw '*Untyped session value*' + } + + It 'validates Credential type matches PSCredential' { + $broker = New-IdleAuthSession -SessionMap @{ + @{ AuthSessionName = 'AD' } = @{ AuthSessionType = 'Credential'; Session = $adCred } + } + + # Should succeed + $session = $broker.AcquireAuthSession('AD', $null) + $session | Should -BeOfType [PSCredential] + } + + It 'throws when Credential type receives non-PSCredential object' { + { + $broker = New-IdleAuthSession -SessionMap @{ + @{ AuthSessionName = 'AD' } = @{ AuthSessionType = 'Credential'; Session = 'not-a-credential' } + } + $broker.AcquireAuthSession('AD', $null) + } | Should -Throw '*Expected AuthSessionType=''Credential'' requires a*PSCredential*' + } + + It 'validates OAuth type matches string token' { + $broker = New-IdleAuthSession -SessionMap @{ + @{ AuthSessionName = 'EXO' } = @{ AuthSessionType = 'OAuth'; Session = $exoToken } + } + + # Should succeed + $session = $broker.AcquireAuthSession('EXO', $null) + $session | Should -BeOfType [string] + } + + It 'throws when OAuth type receives non-string object' { + { + $broker = New-IdleAuthSession -SessionMap @{ + @{ AuthSessionName = 'EXO' } = @{ AuthSessionType = 'OAuth'; Session = $adCred } + } + $broker.AcquireAuthSession('EXO', $null) + } | Should -Throw '*Expected AuthSessionType=''OAuth'' requires a*string*' + } + + It 'validates PSRemoting type matches PSCredential' { + $broker = New-IdleAuthSession -SessionMap @{ + @{ AuthSessionName = 'Remote' } = @{ AuthSessionType = 'PSRemoting'; Session = $adCred } + } + + # Should succeed (PSCredential is valid for PSRemoting) + $session = $broker.AcquireAuthSession('Remote', $null) + $session | Should -BeOfType [PSCredential] + } + + It 'throws when PSRemoting type receives invalid object' { + { + $broker = New-IdleAuthSession -SessionMap @{ + @{ AuthSessionName = 'Remote' } = @{ AuthSessionType = 'PSRemoting'; Session = 'not-valid' } + } + $broker.AcquireAuthSession('Remote', $null) + } | Should -Throw '*Expected AuthSessionType=''PSRemoting''*' + } + + It 'supports typed DefaultAuthSession' { + $broker = New-IdleAuthSession -SessionMap @{ + @{ AuthSessionName = 'AD' } = @{ AuthSessionType = 'Credential'; Session = $adCred } + } -DefaultAuthSession @{ AuthSessionType = 'OAuth'; Session = $exoToken } + + $defaultSession = $broker.AcquireAuthSession('Unknown', $null) + $defaultSession | Should -BeOfType [string] + $defaultSession | Should -Be 'mock-oauth-token-12345' + } + + It 'validates typed DefaultAuthSession' { + { + $broker = New-IdleAuthSession -SessionMap @{ + @{ AuthSessionName = 'AD' } = @{ AuthSessionType = 'Credential'; Session = $adCred } + } -DefaultAuthSession @{ AuthSessionType = 'Credential'; Session = 'not-a-credential' } + + $broker.AcquireAuthSession('Unknown', $null) + } | Should -Throw '*Expected AuthSessionType=''Credential''*' + } + + It 'supports PSCustomObject format for typed sessions' { + $typedSession = [pscustomobject]@{ + AuthSessionType = 'OAuth' + Session = $exoToken + } + + $broker = New-IdleAuthSession -SessionMap @{ + @{ AuthSessionName = 'EXO' } = $typedSession + } + + $session = $broker.AcquireAuthSession('EXO', $null) + $session | Should -BeOfType [string] + $session | Should -Be 'mock-oauth-token-12345' + } + + It 'multi-provider scenario: AD (Credential) + EXO (OAuth)' { + # Real-world scenario: mixed authentication types in single broker + $broker = New-IdleAuthSession -SessionMap @{ + @{ AuthSessionName = 'AD'; Role = 'Admin' } = @{ AuthSessionType = 'Credential'; Session = $adCred } + @{ AuthSessionName = 'EXO' } = @{ AuthSessionType = 'OAuth'; Session = $exoToken } + } + + # Acquire AD session + $adSession = $broker.AcquireAuthSession('AD', @{ Role = 'Admin' }) + $adSession | Should -BeOfType [PSCredential] + $adSession.UserName | Should -Be 'ADUser' + + # Acquire EXO session + $exoSession = $broker.AcquireAuthSession('EXO', $null) + $exoSession | Should -BeOfType [string] + $exoSession | Should -Be 'mock-oauth-token-12345' + } + + It 'throws with clear error including session name on type mismatch' { + { + $broker = New-IdleAuthSession -SessionMap @{ + @{ AuthSessionName = 'MyCustomSession' } = @{ AuthSessionType = 'Credential'; Session = 'wrong-type' } + } + $broker.AcquireAuthSession('MyCustomSession', $null) + } | Should -Throw '*MyCustomSession*' + } + + It 'throws when invalid AuthSessionType provided in typed value' { + { + New-IdleAuthSession -SessionMap @{ + @{ AuthSessionName = 'AD' } = @{ AuthSessionType = 'InvalidType'; Session = $adCred } + } + } | Should -Throw '*Invalid AuthSessionType*' + } + } + + Context 'Backward compatibility' { + BeforeEach { + $password = ConvertTo-SecureString 'Password!' -AsPlainText -Force + $cred = New-Object System.Management.Automation.PSCredential('User', $password) + } + + It 'legacy untyped SessionMap with -AuthSessionType still works' { + $broker = New-IdleAuthSession -SessionMap @{ + @{ Role = 'Admin' } = $cred + } -AuthSessionType 'Credential' + + $session = $broker.AcquireAuthSession('Test', @{ Role = 'Admin' }) + $session | Should -BeOfType [PSCredential] + } + + It 'legacy untyped DefaultAuthSession with -AuthSessionType still works' { + $broker = New-IdleAuthSession -SessionMap @{} -DefaultAuthSession $cred -AuthSessionType 'Credential' + + $session = $broker.AcquireAuthSession('Test', $null) + $session | Should -BeOfType [PSCredential] + } + + It 'existing tests should continue to work' { + # This mimics the original test pattern + $broker = New-IdleAuthSession -SessionMap @{ + @{ Role = 'Tier0' } = $cred + } -AuthSessionType 'Credential' + + $session = $broker.AcquireAuthSession('TestName', @{ Role = 'Tier0' }) + $session | Should -Not -BeNullOrEmpty + $session | Should -BeOfType [PSCredential] + } + } } From 2586a89692f88ebb664a4933f874345bc2a57cda Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 8 Feb 2026 19:49:10 +0000 Subject: [PATCH 03/13] docs: add mixed-type AuthSessionBroker example and guidance - Update docs/use/providers.md with per-entry AuthSessionType info - Add comprehensive example for AD + EXO mixed auth scenario - Document typed session value format - Add backward compatibility note - Note: cmdlet reference will be regenerated by CI Co-authored-by: blindzero <13959569+blindzero@users.noreply.github.com> --- docs/use/providers.md | 72 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 71 insertions(+), 1 deletion(-) diff --git a/docs/use/providers.md b/docs/use/providers.md index f9e446c0..8198bae6 100644 --- a/docs/use/providers.md +++ b/docs/use/providers.md @@ -153,12 +153,14 @@ host-supplied broker. Using the **AuthSessionBroker** is in particular helpful f ### AuthSessionType -Each `AuthSessionBroker` must specify an `AuthSessionType` that determines validation rules, lifecycle management, and telemetry behavior: +Each `AuthSessionBroker` can specify an `AuthSessionType` that determines validation rules, lifecycle management, and telemetry behavior: - **`OAuth`** - Token-based authentication (e.g., Microsoft Graph, Exchange Online) - **`PSRemoting`** - PowerShell remoting execution context (e.g., Entra Connect) - **`Credential`** - Credential-based authentication (e.g., Active Directory, mock providers) +Starting with version 0.3.0, you can use **per-entry session types** to support multiple authentication types in a single broker (e.g., AD with Credential + EXO with OAuth). + Each provider documents its required `AuthSessionType` in its reference documentation. ### Example: Active Directory with Credential Auth @@ -206,6 +208,74 @@ $plan = New-IdlePlan -WorkflowPath './workflow.psd1' -Request $request -Provider } ``` +### Example: Mixed Authentication Types (AD + EXO) + +For workflows that need multiple providers with different authentication types, you can use typed session values: + +```powershell +# Obtain credentials and tokens +$adCredential = Get-Credential -Message "Enter AD admin credentials" +$exoToken = (Connect-AzAccount | Get-AzAccessToken -ResourceUrl "https://outlook.office365.com").Token + +# Create providers +$adProvider = New-IdleADIdentityProvider +$exoProvider = New-IdleExchangeOnlineProvider + +# Create broker with mixed authentication types +$broker = New-IdleAuthSession -SessionMap @{ + # Active Directory uses Credential type + @{ AuthSessionName = 'AD' } = @{ AuthSessionType = 'Credential'; Session = $adCredential } + + # Exchange Online uses OAuth type + @{ AuthSessionName = 'EXO' } = @{ AuthSessionType = 'OAuth'; Session = $exoToken } +} + +# Use in plan +$plan = New-IdlePlan -WorkflowPath './workflow.psd1' -Request $request -Providers @{ + AD = $adProvider + EXO = $exoProvider + AuthSessionBroker = $broker +} +``` + +In the workflow, steps specify which authentication session to use: + +```powershell +# Step using AD (Credential) +@{ + Name = 'Create AD User' + Type = 'IdLE.Step.CreateIdentity' + With = @{ + Provider = 'AD' + AuthSessionName = 'AD' + # ... + } +} + +# Step using EXO (OAuth) +@{ + Name = 'Create Mailbox' + Type = 'IdLE.Step.CreateMailbox' + With = @{ + Provider = 'EXO' + AuthSessionName = 'EXO' + # ... + } +} +``` + +:::tip Backward Compatibility + +For simpler scenarios with a single authentication type, you can continue using the original syntax with `-AuthSessionType`: + +```powershell +$broker = New-IdleAuthSession -SessionMap @{ + @{ Role = 'Admin' } = $credential +} -AuthSessionType 'Credential' +``` + +::: + The different authentication sessions are used by the workflow definition by the steps via `AuthSessionOptions`. ```powershell With = @{ From b783c3f6db579d1125244e90e3de6364cfe35140 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 9 Feb 2026 18:13:52 +0000 Subject: [PATCH 04/13] Remove backward compatibility - typed sessions only - Remove -AuthSessionType parameter entirely - All SessionMap values must be typed: @{ AuthSessionType = 'X'; Session = Y } - DefaultAuthSession must be typed - Simplify normalization logic (no untyped support) - Update all examples to use typed format - Remove all backward compatibility tests - All 40 tests pass Co-authored-by: blindzero <13959569+blindzero@users.noreply.github.com> --- docs/use/providers.md | 109 ++--- .../Public/New-IdleAuthSessionBroker.ps1 | 134 ++---- src/IdLE/Public/New-IdleAuthSession.ps1 | 36 +- tests/Core/New-IdleAuthSession.Tests.ps1 | 427 +++++++----------- 4 files changed, 266 insertions(+), 440 deletions(-) diff --git a/docs/use/providers.md b/docs/use/providers.md index 8198bae6..c404d47e 100644 --- a/docs/use/providers.md +++ b/docs/use/providers.md @@ -153,36 +153,57 @@ host-supplied broker. Using the **AuthSessionBroker** is in particular helpful f ### AuthSessionType -Each `AuthSessionBroker` can specify an `AuthSessionType` that determines validation rules, lifecycle management, and telemetry behavior: +AuthSessionBroker session values must specify an `AuthSessionType` that determines validation rules, lifecycle management, and telemetry behavior: - **`OAuth`** - Token-based authentication (e.g., Microsoft Graph, Exchange Online) - **`PSRemoting`** - PowerShell remoting execution context (e.g., Entra Connect) - **`Credential`** - Credential-based authentication (e.g., Active Directory, mock providers) -Starting with version 0.3.0, you can use **per-entry session types** to support multiple authentication types in a single broker (e.g., AD with Credential + EXO with OAuth). - Each provider documents its required `AuthSessionType` in its reference documentation. -### Example: Active Directory with Credential Auth +### Example: Simple Single Credential + +For the simplest case with just one credential: + +```powershell +# Obtain credential (e.g., from a secure vault or credential manager) +$credential = Get-Credential -Message "Enter admin credentials" + +# Create broker with single credential +$broker = New-IdleAuthSession -DefaultAuthSession @{ + AuthSessionType = 'Credential' + Session = $credential +} + +# Use in plan +$plan = New-IdlePlan -WorkflowPath './workflow.psd1' -Request $request +``` + +### Example: Role-Based Credentials + +For scenarios with multiple credentials for different roles, use `AuthSessionOptions` in workflows to select the appropriate credential: ```powershell -# Assuming you have credentials available (e.g., from a secure vault or credential manager) +# Obtain credentials (e.g., from a secure vault or credential manager) $tier0Credential = Get-Credential -Message "Enter Tier0 admin credentials" $adminCredential = Get-Credential -Message "Enter regular admin credentials" -# Create provider -$provider = New-IdleADIdentityProvider - -# Create broker with role-based credential mapping and Credential session type +# Create broker with role-based credential mapping $broker = New-IdleAuthSession -SessionMap @{ - @{ Role = 'Tier0' } = $tier0Credential - @{ Role = 'Admin' } = $adminCredential -} -DefaultAuthSession $adminCredential -AuthSessionType 'Credential' - -# Use provider with broker -$plan = New-IdlePlan -WorkflowPath './workflow.psd1' -Request $request -Providers @{ - Identity = $provider - AuthSessionBroker = $broker + @{ Role = 'Tier0' } = @{ AuthSessionType = 'Credential'; Session = $tier0Credential } + @{ Role = 'Admin' } = @{ AuthSessionType = 'Credential'; Session = $adminCredential } +} -DefaultAuthSession @{ AuthSessionType = 'Credential'; Session = $adminCredential } + +# Use in plan +$plan = New-IdlePlan -WorkflowPath './workflow.psd1' -Request $request +``` + +In the workflow definition, steps specify which role to use via `AuthSessionOptions`: + +```powershell +With = @{ + ... + AuthSessionOptions = @{ Role = 'Tier0' } } ``` @@ -193,34 +214,25 @@ $plan = New-IdlePlan -WorkflowPath './workflow.psd1' -Request $request -Provider Connect-AzAccount $token = (Get-AzAccessToken -ResourceUrl "https://graph.microsoft.com").Token -# Create broker with OAuth session type (tokens can be passed directly) -$broker = New-IdleAuthSession -SessionMap @{ - @{} = $token -} -DefaultAuthSession $token -AuthSessionType 'OAuth' - -# Create provider -$provider = New-IdleEntraIDIdentityProvider +# Create broker with OAuth session type +$broker = New-IdleAuthSession -DefaultAuthSession @{ + AuthSessionType = 'OAuth' + Session = $token +} # Use in plan -$plan = New-IdlePlan -WorkflowPath './workflow.psd1' -Request $request -Providers @{ - Identity = $provider - AuthSessionBroker = $broker -} +$plan = New-IdlePlan -WorkflowPath './workflow.psd1' -Request $request ``` ### Example: Mixed Authentication Types (AD + EXO) -For workflows that need multiple providers with different authentication types, you can use typed session values: +For workflows that need multiple providers with different authentication types: ```powershell # Obtain credentials and tokens $adCredential = Get-Credential -Message "Enter AD admin credentials" $exoToken = (Connect-AzAccount | Get-AzAccessToken -ResourceUrl "https://outlook.office365.com").Token -# Create providers -$adProvider = New-IdleADIdentityProvider -$exoProvider = New-IdleExchangeOnlineProvider - # Create broker with mixed authentication types $broker = New-IdleAuthSession -SessionMap @{ # Active Directory uses Credential type @@ -231,14 +243,10 @@ $broker = New-IdleAuthSession -SessionMap @{ } # Use in plan -$plan = New-IdlePlan -WorkflowPath './workflow.psd1' -Request $request -Providers @{ - AD = $adProvider - EXO = $exoProvider - AuthSessionBroker = $broker -} +$plan = New-IdlePlan -WorkflowPath './workflow.psd1' -Request $request ``` -In the workflow, steps specify which authentication session to use: +In the workflow, steps specify which authentication session to use via `AuthSessionName`: ```powershell # Step using AD (Credential) @@ -246,7 +254,6 @@ In the workflow, steps specify which authentication session to use: Name = 'Create AD User' Type = 'IdLE.Step.CreateIdentity' With = @{ - Provider = 'AD' AuthSessionName = 'AD' # ... } @@ -257,34 +264,12 @@ In the workflow, steps specify which authentication session to use: Name = 'Create Mailbox' Type = 'IdLE.Step.CreateMailbox' With = @{ - Provider = 'EXO' AuthSessionName = 'EXO' # ... } } ``` -:::tip Backward Compatibility - -For simpler scenarios with a single authentication type, you can continue using the original syntax with `-AuthSessionType`: - -```powershell -$broker = New-IdleAuthSession -SessionMap @{ - @{ Role = 'Admin' } = $credential -} -AuthSessionType 'Credential' -``` - -::: - -The different authentication sessions are used by the workflow definition by the steps via `AuthSessionOptions`. -```powershell -With = @{ - ... - AuthSessionName = 'ActiveDirectory' - AuthSessionOptions = @{ Role = 'Tier0' } -} -``` - :::info Please see the detailed [Provider Reference](../reference/providers.md) documentation for authentication help. diff --git a/src/IdLE.Core/Public/New-IdleAuthSessionBroker.ps1 b/src/IdLE.Core/Public/New-IdleAuthSessionBroker.ps1 index d5d42cc3..a1206c0c 100644 --- a/src/IdLE.Core/Public/New-IdleAuthSessionBroker.ps1 +++ b/src/IdLE.Core/Public/New-IdleAuthSessionBroker.ps1 @@ -14,24 +14,18 @@ function New-IdleAuthSessionBroker { .PARAMETER SessionMap Optional hashtable that maps session configurations to auth sessions. Each key is a hashtable - representing the AuthSessionOptions pattern, and each value is the auth session to return. + representing the AuthSessionOptions pattern, and each value is a typed session descriptor. - Values can be specified in two formats: - - 1. Legacy/Untyped (requires -AuthSessionType): - - Direct session object: @{ Role = 'Admin' } = $credential - - 2. Typed (supports mixed types, -AuthSessionType acts as default): - - Hashtable: @{ Role = 'Admin' } = @{ AuthSessionType = 'Credential'; Session = $credential } - - PSCustomObject: @{ Role = 'Admin' } = [pscustomobject]@{ AuthSessionType = 'OAuth'; Session = $token } + Values must be typed session descriptors: + - Hashtable: @{ Role = 'Admin' } = @{ AuthSessionType = 'Credential'; Session = $credential } + - PSCustomObject: @{ Role = 'Admin' } = [pscustomobject]@{ AuthSessionType = 'OAuth'; Session = $token } Keys can include AuthSessionName for name-based routing: - - @{ AuthSessionName = 'AD'; Role = 'ADAdm' } -> $admAD (AuthSessionName + Role routing) - - @{ AuthSessionName = 'EXO' } -> $exoToken (AuthSessionName-only routing) - - @{ Role = 'Tier0' } -> $tier0Credential (Options-only routing, legacy support) - - @{ Server = 'AADConnect01' } -> $remoteSession (for PSRemoting scenarios) - - @{ Domain = 'SourceAD' } -> $sourceCred (for multi-forest scenarios) - - @{ Environment = 'Production' } -> $prodCred (for environment-specific routing) + - @{ AuthSessionName = 'AD'; Role = 'ADAdm' } -> @{ AuthSessionType = 'Credential'; Session = $admAD } + - @{ AuthSessionName = 'EXO' } -> @{ AuthSessionType = 'OAuth'; Session = $exoToken } + - @{ Server = 'AADConnect01' } -> @{ AuthSessionType = 'PSRemoting'; Session = $remoteSession } + - @{ Domain = 'SourceAD' } -> @{ AuthSessionType = 'Credential'; Session = $sourceCred } + - @{ Environment = 'Production' } -> @{ AuthSessionType = 'Credential'; Session = $prodCred } SessionMap is optional if DefaultAuthSession is provided. @@ -39,81 +33,58 @@ function New-IdleAuthSessionBroker { Optional default auth session to return when no session options are provided or when the options don't match any entry in SessionMap. - Can be specified in two formats: - 1. Legacy/Untyped (requires -AuthSessionType): $credential - 2. Typed: @{ AuthSessionType = 'Credential'; Session = $credential } + Must be a typed session descriptor: + @{ AuthSessionType = 'Credential'; Session = $credential } At least one of SessionMap or DefaultAuthSession must be provided. - .PARAMETER AuthSessionType - Optional default authentication session type. Acts as the default for untyped - SessionMap entries and DefaultAuthSession. This determines validation rules, - lifecycle management, and telemetry behavior. - - Valid values: - - 'OAuth': Token-based authentication (e.g., Microsoft Graph, Exchange Online) - - 'PSRemoting': PowerShell remoting execution context (e.g., Entra Connect) - - 'Credential': Credential-based authentication (e.g., Active Directory, mock providers) - - If not provided, all SessionMap values and DefaultAuthSession must be typed - (include AuthSessionType and Session properties). - .EXAMPLE - # Simple single-credential broker (no SessionMap required) - $broker = New-IdleAuthSessionBroker -DefaultAuthSession $admCred -AuthSessionType 'Credential' - - $plan = New-IdlePlan -WorkflowPath './workflow.psd1' -Request $request -Providers @{ - Identity = New-IdleADIdentityProvider - AuthSessionBroker = $broker + # Simple single-credential broker + $broker = New-IdleAuthSessionBroker -DefaultAuthSession @{ + AuthSessionType = 'Credential' + Session = $admCred } .EXAMPLE # AuthSessionName-based routing with roles $broker = New-IdleAuthSessionBroker -SessionMap @{ - @{ AuthSessionName = 'AD'; Role = 'ADAdm' } = $tier0Credential - @{ AuthSessionName = 'AD'; Role = 'ADRead' } = $readOnlyCredential - } -DefaultAuthSession $adminCredential -AuthSessionType 'Credential' + @{ AuthSessionName = 'AD'; Role = 'ADAdm' } = @{ AuthSessionType = 'Credential'; Session = $tier0Credential } + @{ AuthSessionName = 'AD'; Role = 'ADRead' } = @{ AuthSessionType = 'Credential'; Session = $readOnlyCredential } + } -DefaultAuthSession @{ AuthSessionType = 'Credential'; Session = $adminCredential } .EXAMPLE # OAuth broker with token strings $broker = New-IdleAuthSessionBroker -SessionMap @{ - @{ Role = 'Admin' } = $graphToken - } -DefaultAuthSession $graphToken -AuthSessionType 'OAuth' + @{ Role = 'Admin' } = @{ AuthSessionType = 'OAuth'; Session = $graphToken } + } -DefaultAuthSession @{ AuthSessionType = 'OAuth'; Session = $graphToken } .EXAMPLE - # Domain-based broker for multi-forest scenarios with Credential session type + # Domain-based broker for multi-forest scenarios $broker = New-IdleAuthSessionBroker -SessionMap @{ - @{ Domain = 'SourceAD' } = $sourceCred - @{ Domain = 'TargetAD' } = $targetCred - } -AuthSessionType 'Credential' + @{ Domain = 'SourceAD' } = @{ AuthSessionType = 'Credential'; Session = $sourceCred } + @{ Domain = 'TargetAD' } = @{ AuthSessionType = 'Credential'; Session = $targetCred } + } .EXAMPLE # PSRemoting broker for Entra Connect directory sync $broker = New-IdleAuthSessionBroker -SessionMap @{ - @{ Server = 'AADConnect01' } = $remoteSessionCred - } -AuthSessionType 'PSRemoting' + @{ Server = 'AADConnect01' } = @{ AuthSessionType = 'PSRemoting'; Session = $remoteSessionCred } + } .EXAMPLE - # Environment-based routing for multi-environment scenarios + # Environment-based routing $broker = New-IdleAuthSessionBroker -SessionMap @{ - @{ Environment = 'Production' } = $prodCred - @{ Environment = 'Test' } = $testCred - } -DefaultAuthSession $devCred -AuthSessionType 'Credential' + @{ Environment = 'Production' } = @{ AuthSessionType = 'Credential'; Session = $prodCred } + @{ Environment = 'Test' } = @{ AuthSessionType = 'Credential'; Session = $testCred } + } -DefaultAuthSession @{ AuthSessionType = 'Credential'; Session = $devCred } .EXAMPLE - # Mixed-type broker for AD (Credential) + EXO (OAuth) in single workflow + # Mixed-type broker for AD (Credential) + EXO (OAuth) $broker = New-IdleAuthSessionBroker -SessionMap @{ @{ AuthSessionName = 'AD' } = @{ AuthSessionType = 'Credential'; Session = $adCred } @{ AuthSessionName = 'EXO' } = @{ AuthSessionType = 'OAuth'; Session = $exoToken } } - .EXAMPLE - # Mixed typed and untyped with default AuthSessionType - $broker = New-IdleAuthSessionBroker -SessionMap @{ - @{ AuthSessionName = 'AD' } = $adCred # Uses default 'Credential' - @{ AuthSessionName = 'EXO' } = @{ AuthSessionType = 'OAuth'; Session = $exoToken } - } -AuthSessionType 'Credential' - .OUTPUTS PSCustomObject with AcquireAuthSession method #> @@ -126,11 +97,7 @@ function New-IdleAuthSessionBroker { [Parameter()] [AllowNull()] - [object] $DefaultAuthSession, - - [Parameter()] - [ValidateSet('OAuth', 'PSRemoting', 'Credential')] - [string] $AuthSessionType + [object] $DefaultAuthSession ) # Validate: If SessionMap is empty/null, DefaultAuthSession must be provided @@ -162,36 +129,28 @@ function New-IdleAuthSessionBroker { # Helper function to normalize session value to internal format $normalizeSessionValue = { - param($value, $defaultType, $context) + param($value, $context) if ($null -eq $value) { return $null } - # Check if value is already typed - if (& $isTypedSession $value) { - $sessionType = $value.AuthSessionType - $session = $value.Session - - # Validate the provided AuthSessionType - if ($sessionType -notin @('OAuth', 'PSRemoting', 'Credential')) { - throw "Invalid AuthSessionType '$sessionType' in $context. Valid values: 'OAuth', 'PSRemoting', 'Credential'." - } - - return @{ - AuthSessionType = $sessionType - Session = $session - } + # Value must be typed + if (-not (& $isTypedSession $value)) { + throw "Session value in $context must be a typed session descriptor with 'AuthSessionType' and 'Session' properties. Example: @{ AuthSessionType = 'Credential'; Session = `$credential }" } - # Untyped value - use default type - if ([string]::IsNullOrEmpty($defaultType)) { - throw "Untyped session value found in $context, but no default -AuthSessionType provided. Either provide -AuthSessionType or use typed session values: @{ AuthSessionType = ''; Session = }" + $sessionType = $value.AuthSessionType + $session = $value.Session + + # Validate the provided AuthSessionType + if ($sessionType -notin @('OAuth', 'PSRemoting', 'Credential')) { + throw "Invalid AuthSessionType '$sessionType' in $context. Valid values: 'OAuth', 'PSRemoting', 'Credential'." } return @{ - AuthSessionType = $defaultType - Session = $value + AuthSessionType = $sessionType + Session = $session } } @@ -206,7 +165,7 @@ function New-IdleAuthSessionBroker { $patternDesc = ($pattern.Keys | ForEach-Object { "$_=$($pattern[$_])" }) -join ', ' $context = "SessionMap entry { $patternDesc }" - $normalizedValue = & $normalizeSessionValue $value $AuthSessionType $context + $normalizedValue = & $normalizeSessionValue $value $context $normalizedSessionMap[$pattern] = $normalizedValue } } @@ -214,14 +173,13 @@ function New-IdleAuthSessionBroker { # Normalize DefaultAuthSession $normalizedDefaultAuthSession = $null if ($null -ne $DefaultAuthSession) { - $normalizedDefaultAuthSession = & $normalizeSessionValue $DefaultAuthSession $AuthSessionType 'DefaultAuthSession' + $normalizedDefaultAuthSession = & $normalizeSessionValue $DefaultAuthSession 'DefaultAuthSession' } $broker = [pscustomobject]@{ PSTypeName = 'IdLE.AuthSessionBroker' SessionMap = $normalizedSessionMap DefaultAuthSession = $normalizedDefaultAuthSession - AuthSessionType = $AuthSessionType # Store for backward compatibility } $broker | Add-Member -MemberType ScriptMethod -Name AcquireAuthSession -Value { diff --git a/src/IdLE/Public/New-IdleAuthSession.ps1 b/src/IdLE/Public/New-IdleAuthSession.ps1 index 66858518..72a523b3 100644 --- a/src/IdLE/Public/New-IdleAuthSession.ps1 +++ b/src/IdLE/Public/New-IdleAuthSession.ps1 @@ -19,27 +19,24 @@ function New-IdleAuthSession { This is a thin wrapper that delegates to IdLE.Core\New-IdleAuthSessionBroker. .PARAMETER SessionMap - A hashtable that maps session configurations to auth sessions. + A hashtable that maps session configurations to typed auth sessions. .PARAMETER DefaultAuthSession - Optional default auth session to return when no session options are provided. + Optional default typed auth session to return when no session options are provided. - .PARAMETER AuthSessionType - Optional default authentication session type. Acts as the default for untyped - SessionMap entries and DefaultAuthSession. - - Valid values: - - 'OAuth': Token-based authentication (e.g., Microsoft Graph, Exchange Online) - - 'PSRemoting': PowerShell remoting execution context (e.g., Entra Connect) - - 'Credential': Credential-based authentication (e.g., Active Directory, mock providers) - - If not provided, all SessionMap values and DefaultAuthSession must be typed - (include AuthSessionType and Session properties). + .EXAMPLE + # Simple broker with single credential + $broker = New-IdleAuthSession -DefaultAuthSession @{ + AuthSessionType = 'Credential' + Session = $credential + } .EXAMPLE + # Mixed-type broker for AD + EXO $broker = New-IdleAuthSession -SessionMap @{ - @{ Role = 'Tier0' } = $tier0Credential - } -AuthSessionType 'Credential' + @{ AuthSessionName = 'AD' } = @{ AuthSessionType = 'Credential'; Session = $adCred } + @{ AuthSessionName = 'EXO' } = @{ AuthSessionType = 'OAuth'; Session = $token } + } .OUTPUTS PSCustomObject with AcquireAuthSession method @@ -56,11 +53,7 @@ function New-IdleAuthSession { [Parameter()] [AllowNull()] - [object] $DefaultAuthSession, - - [Parameter()] - [ValidateSet('OAuth', 'PSRemoting', 'Credential')] - [string] $AuthSessionType + [object] $DefaultAuthSession ) # Delegate to IdLE.Core implementation. @@ -71,9 +64,6 @@ function New-IdleAuthSession { if ($PSBoundParameters.ContainsKey('DefaultAuthSession')) { $params['DefaultAuthSession'] = $DefaultAuthSession } - if ($PSBoundParameters.ContainsKey('AuthSessionType')) { - $params['AuthSessionType'] = $AuthSessionType - } return IdLE.Core\New-IdleAuthSessionBroker @params } diff --git a/tests/Core/New-IdleAuthSession.Tests.ps1 b/tests/Core/New-IdleAuthSession.Tests.ps1 index ba6dbced..10ff89c1 100644 --- a/tests/Core/New-IdleAuthSession.Tests.ps1 +++ b/tests/Core/New-IdleAuthSession.Tests.ps1 @@ -5,15 +5,16 @@ BeforeAll { Describe 'New-IdleAuthSession' { BeforeEach { - # Create a test credential for use in tests + # Create test credentials and tokens for use in tests $password = ConvertTo-SecureString 'TestPassword123!' -AsPlainText -Force $testCred = New-Object System.Management.Automation.PSCredential('TestUser', $password) + $testToken = 'mock-oauth-token-12345' } It 'creates an auth session broker with the expected type' { $broker = New-IdleAuthSession -SessionMap @{ - @{ Role = 'AD' } = $testCred - } -AuthSessionType 'Credential' + @{ Role = 'AD' } = @{ AuthSessionType = 'Credential'; Session = $testCred } + } $broker | Should -Not -BeNullOrEmpty $broker.PSTypeNames | Should -Contain 'IdLE.AuthSessionBroker' @@ -21,19 +22,19 @@ Describe 'New-IdleAuthSession' { It 'creates broker with AcquireAuthSession method' { $broker = New-IdleAuthSession -SessionMap @{ - @{ Role = 'AD' } = $testCred - } -AuthSessionType 'Credential' + @{ Role = 'AD' } = @{ AuthSessionType = 'Credential'; Session = $testCred } + } $broker.PSObject.Methods['AcquireAuthSession'] | Should -Not -BeNullOrEmpty } - It 'accepts SessionMap parameter' { + It 'accepts SessionMap parameter with typed values' { $sessionMap = @{ - @{ Role = 'Tier0' } = $testCred - @{ Role = 'Admin' } = $testCred + @{ Role = 'Tier0' } = @{ AuthSessionType = 'Credential'; Session = $testCred } + @{ Role = 'Admin' } = @{ AuthSessionType = 'Credential'; Session = $testCred } } - $broker = New-IdleAuthSession -SessionMap $sessionMap -AuthSessionType 'Credential' + $broker = New-IdleAuthSession -SessionMap $sessionMap $broker.SessionMap | Should -Not -BeNullOrEmpty $broker.SessionMap.Count | Should -Be 2 @@ -41,19 +42,19 @@ Describe 'New-IdleAuthSession' { It 'accepts optional DefaultAuthSession parameter' { $broker = New-IdleAuthSession -SessionMap @{ - @{ Role = 'AD' } = $testCred - } -DefaultAuthSession $testCred -AuthSessionType 'Credential' + @{ Role = 'AD' } = @{ AuthSessionType = 'Credential'; Session = $testCred } + } -DefaultAuthSession @{ AuthSessionType = 'Credential'; Session = $testCred } $broker.DefaultAuthSession | Should -Not -BeNullOrEmpty - # Note: DefaultAuthSession is now normalized internally, test via AcquireAuthSession + # Test via AcquireAuthSession (empty name signals default) $session = $broker.AcquireAuthSession('', $null) $session.UserName | Should -Be 'TestUser' } It 'broker can acquire auth session with matching options' { $broker = New-IdleAuthSession -SessionMap @{ - @{ Role = 'Tier0' } = $testCred - } -AuthSessionType 'Credential' + @{ Role = 'Tier0' } = @{ AuthSessionType = 'Credential'; Session = $testCred } + } $acquiredSession = $broker.AcquireAuthSession('TestName', @{ Role = 'Tier0' }) @@ -62,13 +63,13 @@ Describe 'New-IdleAuthSession' { $acquiredSession.UserName | Should -Be 'TestUser' } - It 'broker returns default auth session when no options provided' { + It 'broker returns default auth session when no match found' { $defaultPassword = ConvertTo-SecureString 'DefaultPassword!' -AsPlainText -Force $defaultCred = New-Object System.Management.Automation.PSCredential('DefaultUser', $defaultPassword) $broker = New-IdleAuthSession -SessionMap @{ - @{ Role = 'Tier0' } = $testCred - } -DefaultAuthSession $defaultCred -AuthSessionType 'Credential' + @{ Role = 'Tier0' } = @{ AuthSessionType = 'Credential'; Session = $testCred } + } -DefaultAuthSession @{ AuthSessionType = 'Credential'; Session = $defaultCred } $acquiredSession = $broker.AcquireAuthSession('TestName', $null) @@ -78,15 +79,14 @@ Describe 'New-IdleAuthSession' { It 'throws when no matching auth session found and no default provided' { $broker = New-IdleAuthSession -SessionMap @{ - @{ Role = 'Tier0' } = $testCred - } -AuthSessionType 'Credential' + @{ Role = 'Tier0' } = @{ AuthSessionType = 'Credential'; Session = $testCred } + } { $broker.AcquireAuthSession('TestName', @{ Role = 'NonExistent' }) } | Should -Throw '*No matching auth session found*' } It 'is available as exported command from IdLE module' { - # This test ensures the command is properly exported and accessible $command = Get-Command -Name New-IdleAuthSession -ErrorAction SilentlyContinue $command | Should -Not -BeNullOrEmpty @@ -95,86 +95,138 @@ Describe 'New-IdleAuthSession' { } It 'delegates to IdLE.Core\New-IdleAuthSessionBroker correctly' { - # This test ensures the underlying Core function is available and working - # by verifying that New-IdleAuthSession can complete without errors { $broker = New-IdleAuthSession -SessionMap @{ - @{ Role = 'AD' } = $testCred - } -AuthSessionType 'Credential' -ErrorAction Stop + @{ Role = 'AD' } = @{ AuthSessionType = 'Credential'; Session = $testCred } + } -ErrorAction Stop $broker | Should -Not -BeNullOrEmpty } | Should -Not -Throw } - Context 'AuthSessionType parameter' { + Context 'Typed session validation' { + It 'accepts Credential session type' { + $broker = New-IdleAuthSession -SessionMap @{ + @{ Domain = 'corp.example.com' } = @{ AuthSessionType = 'Credential'; Session = $testCred } + } + + $session = $broker.AcquireAuthSession('AD', @{ Domain = 'corp.example.com' }) + $session | Should -BeOfType [PSCredential] + } + It 'accepts OAuth session type' { $broker = New-IdleAuthSession -SessionMap @{ - @{ Role = 'Admin' } = $testCred - } -AuthSessionType 'OAuth' + @{ Role = 'Admin' } = @{ AuthSessionType = 'OAuth'; Session = $testToken } + } - $broker.AuthSessionType | Should -Be 'OAuth' + $session = $broker.AcquireAuthSession('Graph', @{ Role = 'Admin' }) + $session | Should -BeOfType [string] + $session | Should -Be 'mock-oauth-token-12345' } It 'accepts PSRemoting session type' { $broker = New-IdleAuthSession -SessionMap @{ - @{ Server = 'AADConnect01' } = $testCred - } -AuthSessionType 'PSRemoting' + @{ Server = 'AADConnect01' } = @{ AuthSessionType = 'PSRemoting'; Session = $testCred } + } - $broker.AuthSessionType | Should -Be 'PSRemoting' + $session = $broker.AcquireAuthSession('Remote', @{ Server = 'AADConnect01' }) + $session | Should -BeOfType [PSCredential] } - It 'accepts Credential session type' { + It 'validates Credential type matches PSCredential' { $broker = New-IdleAuthSession -SessionMap @{ - @{ Domain = 'corp.example.com' } = $testCred - } -AuthSessionType 'Credential' - - $broker.AuthSessionType | Should -Be 'Credential' + @{ AuthSessionName = 'AD' } = @{ AuthSessionType = 'Credential'; Session = $testCred } + } + + # Should succeed + $session = $broker.AcquireAuthSession('AD', $null) + $session | Should -BeOfType [PSCredential] } - It 'throws on invalid session type' { - { - New-IdleAuthSession -SessionMap @{ - @{ Role = 'AD' } = $testCred - } -AuthSessionType 'InvalidType' - } | Should -Throw + It 'throws when Credential type receives non-PSCredential object' { + { + $broker = New-IdleAuthSession -SessionMap @{ + @{ AuthSessionName = 'AD' } = @{ AuthSessionType = 'Credential'; Session = 'not-a-credential' } + } + $broker.AcquireAuthSession('AD', $null) + } | Should -Throw '*Expected AuthSessionType=''Credential'' requires a*PSCredential*' } - } - Context 'AuthSessionType validation during acquisition' { - It 'OAuth broker can acquire sessions with appropriate options' { - $oauthToken = 'mock-oauth-token' + It 'validates OAuth type matches string token' { $broker = New-IdleAuthSession -SessionMap @{ - @{ Role = 'Admin' } = $oauthToken - } -AuthSessionType 'OAuth' - - $session = $broker.AcquireAuthSession('MicrosoftGraph', @{ Role = 'Admin' }) - $session | Should -Not -BeNullOrEmpty + @{ AuthSessionName = 'EXO' } = @{ AuthSessionType = 'OAuth'; Session = $testToken } + } + + # Should succeed + $session = $broker.AcquireAuthSession('EXO', $null) $session | Should -BeOfType [string] } - It 'PSRemoting broker can acquire sessions with appropriate options' { + It 'throws when OAuth type receives non-string object' { + { + $broker = New-IdleAuthSession -SessionMap @{ + @{ AuthSessionName = 'EXO' } = @{ AuthSessionType = 'OAuth'; Session = $testCred } + } + $broker.AcquireAuthSession('EXO', $null) + } | Should -Throw '*Expected AuthSessionType=''OAuth'' requires a*string*' + } + + It 'validates PSRemoting type matches PSCredential' { $broker = New-IdleAuthSession -SessionMap @{ - @{ Server = 'AADConnect01' } = $testCred - } -AuthSessionType 'PSRemoting' - - $session = $broker.AcquireAuthSession('EntraConnect', @{ Server = 'AADConnect01' }) - $session | Should -Not -BeNullOrEmpty + @{ AuthSessionName = 'Remote' } = @{ AuthSessionType = 'PSRemoting'; Session = $testCred } + } + + # Should succeed (PSCredential is valid for PSRemoting) + $session = $broker.AcquireAuthSession('Remote', $null) $session | Should -BeOfType [PSCredential] } - It 'Credential broker can acquire sessions with appropriate options' { - $broker = New-IdleAuthSession -SessionMap @{ - @{ Domain = 'corp.example.com' } = $testCred - } -AuthSessionType 'Credential' - - $session = $broker.AcquireAuthSession('ActiveDirectory', @{ Domain = 'corp.example.com' }) - $session | Should -Not -BeNullOrEmpty + It 'throws when PSRemoting type receives invalid object' { + { + $broker = New-IdleAuthSession -SessionMap @{ + @{ AuthSessionName = 'Remote' } = @{ AuthSessionType = 'PSRemoting'; Session = 'not-valid' } + } + $broker.AcquireAuthSession('Remote', $null) + } | Should -Throw '*Expected AuthSessionType=''PSRemoting''*' + } + + It 'throws when invalid AuthSessionType provided' { + { + New-IdleAuthSession -SessionMap @{ + @{ AuthSessionName = 'AD' } = @{ AuthSessionType = 'InvalidType'; Session = $testCred } + } + } | Should -Throw '*Invalid AuthSessionType*' + } + + It 'throws when untyped session value provided' { + { + New-IdleAuthSession -SessionMap @{ + @{ AuthSessionName = 'AD' } = $testCred # Untyped + } + } | Should -Throw '*must be a typed session descriptor*' + } + + It 'throws when untyped DefaultAuthSession provided' { + { + New-IdleAuthSession -SessionMap @{ + @{ AuthSessionName = 'AD' } = @{ AuthSessionType = 'Credential'; Session = $testCred } + } -DefaultAuthSession $testCred # Untyped default + } | Should -Throw '*must be a typed session descriptor*' + } + + It 'throws with clear error including session name on type mismatch' { + { + $broker = New-IdleAuthSession -SessionMap @{ + @{ AuthSessionName = 'MyCustomSession' } = @{ AuthSessionType = 'Credential'; Session = 'wrong-type' } + } + $broker.AcquireAuthSession('MyCustomSession', $null) + } | Should -Throw '*MyCustomSession*' } } Context 'Optional SessionMap' { It 'creates broker with only DefaultAuthSession (no SessionMap)' { - $broker = New-IdleAuthSession -DefaultAuthSession $testCred -AuthSessionType 'Credential' + $broker = New-IdleAuthSession -DefaultAuthSession @{ AuthSessionType = 'Credential'; Session = $testCred } $broker | Should -Not -BeNullOrEmpty $broker.DefaultAuthSession | Should -Not -BeNullOrEmpty @@ -182,7 +234,7 @@ Describe 'New-IdleAuthSession' { } It 'returns DefaultAuthSession when SessionMap is null' { - $broker = New-IdleAuthSession -DefaultAuthSession $testCred -AuthSessionType 'Credential' + $broker = New-IdleAuthSession -DefaultAuthSession @{ AuthSessionType = 'Credential'; Session = $testCred } $session = $broker.AcquireAuthSession('AnyName', $null) @@ -191,7 +243,7 @@ Describe 'New-IdleAuthSession' { } It 'returns DefaultAuthSession when SessionMap is empty' { - $broker = New-IdleAuthSession -SessionMap @{} -DefaultAuthSession $testCred -AuthSessionType 'Credential' + $broker = New-IdleAuthSession -SessionMap @{} -DefaultAuthSession @{ AuthSessionType = 'Credential'; Session = $testCred } $session = $broker.AcquireAuthSession('AnyName', $null) @@ -201,13 +253,13 @@ Describe 'New-IdleAuthSession' { It 'throws when SessionMap is null and DefaultAuthSession is not provided' { { - New-IdleAuthSession -SessionMap $null -AuthSessionType 'Credential' + New-IdleAuthSession -SessionMap $null } | Should -Throw '*DefaultAuthSession must be provided*' } It 'throws when SessionMap is empty and DefaultAuthSession is not provided' { { - New-IdleAuthSession -SessionMap @{} -AuthSessionType 'Credential' + New-IdleAuthSession -SessionMap @{} } | Should -Throw '*DefaultAuthSession must be provided*' } } @@ -226,9 +278,9 @@ Describe 'New-IdleAuthSession' { It 'matches AuthSessionName without options' { $broker = New-IdleAuthSession -SessionMap @{ - @{ AuthSessionName = 'AD' } = $cred1 - @{ AuthSessionName = 'EXO' } = $cred2 - } -AuthSessionType 'Credential' + @{ AuthSessionName = 'AD' } = @{ AuthSessionType = 'Credential'; Session = $cred1 } + @{ AuthSessionName = 'EXO' } = @{ AuthSessionType = 'Credential'; Session = $cred2 } + } $session = $broker.AcquireAuthSession('AD', $null) @@ -238,9 +290,9 @@ Describe 'New-IdleAuthSession' { It 'matches AuthSessionName with matching options' { $broker = New-IdleAuthSession -SessionMap @{ - @{ AuthSessionName = 'AD'; Role = 'ADAdm' } = $cred1 - @{ AuthSessionName = 'AD'; Role = 'ADRead' } = $cred3 - } -AuthSessionType 'Credential' + @{ AuthSessionName = 'AD'; Role = 'ADAdm' } = @{ AuthSessionType = 'Credential'; Session = $cred1 } + @{ AuthSessionName = 'AD'; Role = 'ADRead' } = @{ AuthSessionType = 'Credential'; Session = $cred3 } + } $session = $broker.AcquireAuthSession('AD', @{ Role = 'ADRead' }) @@ -250,8 +302,8 @@ Describe 'New-IdleAuthSession' { It 'falls back to default when AuthSessionName does not match' { $broker = New-IdleAuthSession -SessionMap @{ - @{ AuthSessionName = 'AD' } = $cred1 - } -DefaultAuthSession $testCred -AuthSessionType 'Credential' + @{ AuthSessionName = 'AD' } = @{ AuthSessionType = 'Credential'; Session = $cred1 } + } -DefaultAuthSession @{ AuthSessionType = 'Credential'; Session = $testCred } $session = $broker.AcquireAuthSession('EXO', $null) @@ -261,19 +313,19 @@ Describe 'New-IdleAuthSession' { It 'throws when AuthSessionName matches multiple entries (ambiguous)' { $broker = New-IdleAuthSession -SessionMap @{ - @{ AuthSessionName = 'AD' } = $cred1 - @{ AuthSessionName = 'AD' } = $cred3 - } -AuthSessionType 'Credential' + @{ AuthSessionName = 'AD' } = @{ AuthSessionType = 'Credential'; Session = $cred1 } + @{ AuthSessionName = 'AD' } = @{ AuthSessionType = 'Credential'; Session = $cred3 } + } { $broker.AcquireAuthSession('AD', $null) } | Should -Throw '*Ambiguous*' } - It 'prefers AuthSessionName match over legacy Options-only match' { + It 'prefers AuthSessionName match over Options-only match' { $broker = New-IdleAuthSession -SessionMap @{ - @{ Role = 'Admin' } = $testCred - @{ AuthSessionName = 'AD'; Role = 'Admin' } = $cred1 - } -AuthSessionType 'Credential' + @{ Role = 'Admin' } = @{ AuthSessionType = 'Credential'; Session = $testCred } + @{ AuthSessionName = 'AD'; Role = 'Admin' } = @{ AuthSessionType = 'Credential'; Session = $cred1 } + } $session = $broker.AcquireAuthSession('AD', @{ Role = 'Admin' }) @@ -281,11 +333,11 @@ Describe 'New-IdleAuthSession' { $session.UserName | Should -Be 'ADAdm' } - It 'supports legacy Options-only routing when AuthSessionName is not in pattern' { + It 'supports Options-only routing when AuthSessionName is not in pattern' { $broker = New-IdleAuthSession -SessionMap @{ - @{ Role = 'Tier0' } = $cred1 - @{ Role = 'Admin' } = $cred2 - } -AuthSessionType 'Credential' + @{ Role = 'Tier0' } = @{ AuthSessionType = 'Credential'; Session = $cred1 } + @{ Role = 'Admin' } = @{ AuthSessionType = 'Credential'; Session = $cred2 } + } $session = $broker.AcquireAuthSession('AnyName', @{ Role = 'Admin' }) @@ -295,8 +347,8 @@ Describe 'New-IdleAuthSession' { It 'throws when AuthSessionName does not match and no default provided' { $broker = New-IdleAuthSession -SessionMap @{ - @{ AuthSessionName = 'AD' } = $cred1 - } -AuthSessionType 'Credential' + @{ AuthSessionName = 'AD' } = @{ AuthSessionType = 'Credential'; Session = $cred1 } + } { $broker.AcquireAuthSession('EXO', $null) } | Should -Throw '*No matching auth session found*' @@ -304,8 +356,8 @@ Describe 'New-IdleAuthSession' { It 'matches complex pattern: AuthSessionName + multiple options' { $broker = New-IdleAuthSession -SessionMap @{ - @{ AuthSessionName = 'AD'; Role = 'Admin'; Environment = 'Prod' } = $cred1 - } -AuthSessionType 'Credential' + @{ AuthSessionName = 'AD'; Role = 'Admin'; Environment = 'Prod' } = @{ AuthSessionType = 'Credential'; Session = $cred1 } + } $session = $broker.AcquireAuthSession('AD', @{ Role = 'Admin'; Environment = 'Prod' }) @@ -315,8 +367,8 @@ Describe 'New-IdleAuthSession' { It 'does not match when partial options provided' { $broker = New-IdleAuthSession -SessionMap @{ - @{ AuthSessionName = 'AD'; Role = 'Admin'; Environment = 'Prod' } = $cred1 - } -DefaultAuthSession $testCred -AuthSessionType 'Credential' + @{ AuthSessionName = 'AD'; Role = 'Admin'; Environment = 'Prod' } = @{ AuthSessionType = 'Credential'; Session = $cred1 } + } -DefaultAuthSession @{ AuthSessionType = 'Credential'; Session = $testCred } # Only providing Role, not Environment - should fall back to default $session = $broker.AcquireAuthSession('AD', @{ Role = 'Admin' }) @@ -326,40 +378,14 @@ Describe 'New-IdleAuthSession' { } } - Context 'Per-entry AuthSessionType support' { + Context 'Mixed authentication types' { BeforeEach { $password = ConvertTo-SecureString 'Password!' -AsPlainText -Force $adCred = New-Object System.Management.Automation.PSCredential('ADUser', $password) $exoToken = 'mock-oauth-token-12345' } - It 'supports typed SessionMap values with AuthSessionType property' { - $broker = New-IdleAuthSession -SessionMap @{ - @{ AuthSessionName = 'AD' } = @{ AuthSessionType = 'Credential'; Session = $adCred } - } - - $session = $broker.AcquireAuthSession('AD', $null) - - $session | Should -Not -BeNullOrEmpty - $session | Should -BeOfType [PSCredential] - $session.UserName | Should -Be 'ADUser' - } - - It 'supports mixed typed and untyped SessionMap values with default AuthSessionType' { - $broker = New-IdleAuthSession -SessionMap @{ - @{ AuthSessionName = 'AD' } = $adCred # Untyped, uses default - @{ AuthSessionName = 'EXO' } = @{ AuthSessionType = 'OAuth'; Session = $exoToken } # Typed - } -AuthSessionType 'Credential' - - $adSession = $broker.AcquireAuthSession('AD', $null) - $adSession | Should -BeOfType [PSCredential] - - $exoSession = $broker.AcquireAuthSession('EXO', $null) - $exoSession | Should -BeOfType [string] - $exoSession | Should -Be 'mock-oauth-token-12345' - } - - It 'supports all typed SessionMap values without -AuthSessionType' { + It 'supports mixed types in single SessionMap' { $broker = New-IdleAuthSession -SessionMap @{ @{ AuthSessionName = 'AD' } = @{ AuthSessionType = 'Credential'; Session = $adCred } @{ AuthSessionName = 'EXO' } = @{ AuthSessionType = 'OAuth'; Session = $exoToken } @@ -370,82 +396,10 @@ Describe 'New-IdleAuthSession' { $exoSession = $broker.AcquireAuthSession('EXO', $null) $exoSession | Should -BeOfType [string] + $exoSession | Should -Be 'mock-oauth-token-12345' } - It 'throws when untyped SessionMap value provided without -AuthSessionType' { - { - New-IdleAuthSession -SessionMap @{ - @{ AuthSessionName = 'AD' } = $adCred # Untyped - } - } | Should -Throw '*Untyped session value*' - } - - It 'throws when untyped DefaultAuthSession provided without -AuthSessionType' { - { - New-IdleAuthSession -SessionMap @{ - @{ AuthSessionName = 'AD' } = @{ AuthSessionType = 'Credential'; Session = $adCred } - } -DefaultAuthSession $adCred # Untyped default - } | Should -Throw '*Untyped session value*' - } - - It 'validates Credential type matches PSCredential' { - $broker = New-IdleAuthSession -SessionMap @{ - @{ AuthSessionName = 'AD' } = @{ AuthSessionType = 'Credential'; Session = $adCred } - } - - # Should succeed - $session = $broker.AcquireAuthSession('AD', $null) - $session | Should -BeOfType [PSCredential] - } - - It 'throws when Credential type receives non-PSCredential object' { - { - $broker = New-IdleAuthSession -SessionMap @{ - @{ AuthSessionName = 'AD' } = @{ AuthSessionType = 'Credential'; Session = 'not-a-credential' } - } - $broker.AcquireAuthSession('AD', $null) - } | Should -Throw '*Expected AuthSessionType=''Credential'' requires a*PSCredential*' - } - - It 'validates OAuth type matches string token' { - $broker = New-IdleAuthSession -SessionMap @{ - @{ AuthSessionName = 'EXO' } = @{ AuthSessionType = 'OAuth'; Session = $exoToken } - } - - # Should succeed - $session = $broker.AcquireAuthSession('EXO', $null) - $session | Should -BeOfType [string] - } - - It 'throws when OAuth type receives non-string object' { - { - $broker = New-IdleAuthSession -SessionMap @{ - @{ AuthSessionName = 'EXO' } = @{ AuthSessionType = 'OAuth'; Session = $adCred } - } - $broker.AcquireAuthSession('EXO', $null) - } | Should -Throw '*Expected AuthSessionType=''OAuth'' requires a*string*' - } - - It 'validates PSRemoting type matches PSCredential' { - $broker = New-IdleAuthSession -SessionMap @{ - @{ AuthSessionName = 'Remote' } = @{ AuthSessionType = 'PSRemoting'; Session = $adCred } - } - - # Should succeed (PSCredential is valid for PSRemoting) - $session = $broker.AcquireAuthSession('Remote', $null) - $session | Should -BeOfType [PSCredential] - } - - It 'throws when PSRemoting type receives invalid object' { - { - $broker = New-IdleAuthSession -SessionMap @{ - @{ AuthSessionName = 'Remote' } = @{ AuthSessionType = 'PSRemoting'; Session = 'not-valid' } - } - $broker.AcquireAuthSession('Remote', $null) - } | Should -Throw '*Expected AuthSessionType=''PSRemoting''*' - } - - It 'supports typed DefaultAuthSession' { + It 'validates typed DefaultAuthSession with different type than SessionMap' { $broker = New-IdleAuthSession -SessionMap @{ @{ AuthSessionName = 'AD' } = @{ AuthSessionType = 'Credential'; Session = $adCred } } -DefaultAuthSession @{ AuthSessionType = 'OAuth'; Session = $exoToken } @@ -455,31 +409,6 @@ Describe 'New-IdleAuthSession' { $defaultSession | Should -Be 'mock-oauth-token-12345' } - It 'validates typed DefaultAuthSession' { - { - $broker = New-IdleAuthSession -SessionMap @{ - @{ AuthSessionName = 'AD' } = @{ AuthSessionType = 'Credential'; Session = $adCred } - } -DefaultAuthSession @{ AuthSessionType = 'Credential'; Session = 'not-a-credential' } - - $broker.AcquireAuthSession('Unknown', $null) - } | Should -Throw '*Expected AuthSessionType=''Credential''*' - } - - It 'supports PSCustomObject format for typed sessions' { - $typedSession = [pscustomobject]@{ - AuthSessionType = 'OAuth' - Session = $exoToken - } - - $broker = New-IdleAuthSession -SessionMap @{ - @{ AuthSessionName = 'EXO' } = $typedSession - } - - $session = $broker.AcquireAuthSession('EXO', $null) - $session | Should -BeOfType [string] - $session | Should -Be 'mock-oauth-token-12345' - } - It 'multi-provider scenario: AD (Credential) + EXO (OAuth)' { # Real-world scenario: mixed authentication types in single broker $broker = New-IdleAuthSession -SessionMap @{ @@ -498,55 +427,19 @@ Describe 'New-IdleAuthSession' { $exoSession | Should -Be 'mock-oauth-token-12345' } - It 'throws with clear error including session name on type mismatch' { - { - $broker = New-IdleAuthSession -SessionMap @{ - @{ AuthSessionName = 'MyCustomSession' } = @{ AuthSessionType = 'Credential'; Session = 'wrong-type' } - } - $broker.AcquireAuthSession('MyCustomSession', $null) - } | Should -Throw '*MyCustomSession*' - } - - It 'throws when invalid AuthSessionType provided in typed value' { - { - New-IdleAuthSession -SessionMap @{ - @{ AuthSessionName = 'AD' } = @{ AuthSessionType = 'InvalidType'; Session = $adCred } - } - } | Should -Throw '*Invalid AuthSessionType*' - } - } - - Context 'Backward compatibility' { - BeforeEach { - $password = ConvertTo-SecureString 'Password!' -AsPlainText -Force - $cred = New-Object System.Management.Automation.PSCredential('User', $password) - } - - It 'legacy untyped SessionMap with -AuthSessionType still works' { - $broker = New-IdleAuthSession -SessionMap @{ - @{ Role = 'Admin' } = $cred - } -AuthSessionType 'Credential' - - $session = $broker.AcquireAuthSession('Test', @{ Role = 'Admin' }) - $session | Should -BeOfType [PSCredential] - } - - It 'legacy untyped DefaultAuthSession with -AuthSessionType still works' { - $broker = New-IdleAuthSession -SessionMap @{} -DefaultAuthSession $cred -AuthSessionType 'Credential' - - $session = $broker.AcquireAuthSession('Test', $null) - $session | Should -BeOfType [PSCredential] - } + It 'supports PSCustomObject format for typed sessions' { + $typedSession = [pscustomobject]@{ + AuthSessionType = 'OAuth' + Session = $exoToken + } - It 'existing tests should continue to work' { - # This mimics the original test pattern $broker = New-IdleAuthSession -SessionMap @{ - @{ Role = 'Tier0' } = $cred - } -AuthSessionType 'Credential' + @{ AuthSessionName = 'EXO' } = $typedSession + } - $session = $broker.AcquireAuthSession('TestName', @{ Role = 'Tier0' }) - $session | Should -Not -BeNullOrEmpty - $session | Should -BeOfType [PSCredential] + $session = $broker.AcquireAuthSession('EXO', $null) + $session | Should -BeOfType [string] + $session | Should -Be 'mock-oauth-token-12345' } } } From f919aac506483bea362ccfdbf32dca0d8227e71f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 9 Feb 2026 19:35:16 +0000 Subject: [PATCH 05/13] Re-add -AuthSessionType for simpler syntax, rename Session to Credential - Re-add -AuthSessionType parameter (optional) - When provided, allows simple untyped values: $credential instead of @{ AuthSessionType = 'X'; Credential = Y } - When not provided, typed descriptors required for mixed types - Renamed Session property to Credential for clarity - Removed PSCustomObject support (only hashtable) - Re-added provider setup and -Providers in documentation examples - Updated all examples to show simple syntax with -AuthSessionType - Mixed-type example uses typed syntax with Credential property - Updated tests: 21 tests covering simple and typed syntax - All tests pass Co-authored-by: blindzero <13959569+blindzero@users.noreply.github.com> --- docs/use/providers.md | 54 ++- .../Public/New-IdleAuthSessionBroker.ps1 | 150 +++--- src/IdLE/Public/New-IdleAuthSession.ps1 | 31 +- tests/Core/New-IdleAuthSession.Tests.ps1 | 440 +++++------------- 4 files changed, 250 insertions(+), 425 deletions(-) diff --git a/docs/use/providers.md b/docs/use/providers.md index c404d47e..315c8036 100644 --- a/docs/use/providers.md +++ b/docs/use/providers.md @@ -169,14 +169,17 @@ For the simplest case with just one credential: # Obtain credential (e.g., from a secure vault or credential manager) $credential = Get-Credential -Message "Enter admin credentials" +# Create provider +$provider = New-IdleADIdentityProvider + # Create broker with single credential -$broker = New-IdleAuthSession -DefaultAuthSession @{ - AuthSessionType = 'Credential' - Session = $credential -} +$broker = New-IdleAuthSession -DefaultAuthSession $credential -AuthSessionType 'Credential' # Use in plan -$plan = New-IdlePlan -WorkflowPath './workflow.psd1' -Request $request +$plan = New-IdlePlan -WorkflowPath './workflow.psd1' -Request $request -Providers @{ + Identity = $provider + AuthSessionBroker = $broker +} ``` ### Example: Role-Based Credentials @@ -188,14 +191,20 @@ For scenarios with multiple credentials for different roles, use `AuthSessionOpt $tier0Credential = Get-Credential -Message "Enter Tier0 admin credentials" $adminCredential = Get-Credential -Message "Enter regular admin credentials" +# Create provider +$provider = New-IdleADIdentityProvider + # Create broker with role-based credential mapping $broker = New-IdleAuthSession -SessionMap @{ - @{ Role = 'Tier0' } = @{ AuthSessionType = 'Credential'; Session = $tier0Credential } - @{ Role = 'Admin' } = @{ AuthSessionType = 'Credential'; Session = $adminCredential } -} -DefaultAuthSession @{ AuthSessionType = 'Credential'; Session = $adminCredential } + @{ Role = 'Tier0' } = $tier0Credential + @{ Role = 'Admin' } = $adminCredential +} -DefaultAuthSession $adminCredential -AuthSessionType 'Credential' # Use in plan -$plan = New-IdlePlan -WorkflowPath './workflow.psd1' -Request $request +$plan = New-IdlePlan -WorkflowPath './workflow.psd1' -Request $request -Providers @{ + Identity = $provider + AuthSessionBroker = $broker +} ``` In the workflow definition, steps specify which role to use via `AuthSessionOptions`: @@ -214,14 +223,17 @@ With = @{ Connect-AzAccount $token = (Get-AzAccessToken -ResourceUrl "https://graph.microsoft.com").Token +# Create provider +$provider = New-IdleEntraIDIdentityProvider + # Create broker with OAuth session type -$broker = New-IdleAuthSession -DefaultAuthSession @{ - AuthSessionType = 'OAuth' - Session = $token -} +$broker = New-IdleAuthSession -DefaultAuthSession $token -AuthSessionType 'OAuth' # Use in plan -$plan = New-IdlePlan -WorkflowPath './workflow.psd1' -Request $request +$plan = New-IdlePlan -WorkflowPath './workflow.psd1' -Request $request -Providers @{ + Identity = $provider + AuthSessionBroker = $broker +} ``` ### Example: Mixed Authentication Types (AD + EXO) @@ -233,17 +245,25 @@ For workflows that need multiple providers with different authentication types: $adCredential = Get-Credential -Message "Enter AD admin credentials" $exoToken = (Connect-AzAccount | Get-AzAccessToken -ResourceUrl "https://outlook.office365.com").Token +# Create providers +$adProvider = New-IdleADIdentityProvider +$exoProvider = New-IdleExchangeOnlineProvider + # Create broker with mixed authentication types $broker = New-IdleAuthSession -SessionMap @{ # Active Directory uses Credential type - @{ AuthSessionName = 'AD' } = @{ AuthSessionType = 'Credential'; Session = $adCredential } + @{ AuthSessionName = 'AD' } = @{ AuthSessionType = 'Credential'; Credential = $adCredential } # Exchange Online uses OAuth type - @{ AuthSessionName = 'EXO' } = @{ AuthSessionType = 'OAuth'; Session = $exoToken } + @{ AuthSessionName = 'EXO' } = @{ AuthSessionType = 'OAuth'; Credential = $exoToken } } # Use in plan -$plan = New-IdlePlan -WorkflowPath './workflow.psd1' -Request $request +$plan = New-IdlePlan -WorkflowPath './workflow.psd1' -Request $request -Providers @{ + AD = $adProvider + EXO = $exoProvider + AuthSessionBroker = $broker +} ``` In the workflow, steps specify which authentication session to use via `AuthSessionName`: diff --git a/src/IdLE.Core/Public/New-IdleAuthSessionBroker.ps1 b/src/IdLE.Core/Public/New-IdleAuthSessionBroker.ps1 index a1206c0c..9100b1fb 100644 --- a/src/IdLE.Core/Public/New-IdleAuthSessionBroker.ps1 +++ b/src/IdLE.Core/Public/New-IdleAuthSessionBroker.ps1 @@ -14,18 +14,17 @@ function New-IdleAuthSessionBroker { .PARAMETER SessionMap Optional hashtable that maps session configurations to auth sessions. Each key is a hashtable - representing the AuthSessionOptions pattern, and each value is a typed session descriptor. - - Values must be typed session descriptors: - - Hashtable: @{ Role = 'Admin' } = @{ AuthSessionType = 'Credential'; Session = $credential } - - PSCustomObject: @{ Role = 'Admin' } = [pscustomobject]@{ AuthSessionType = 'OAuth'; Session = $token } + representing the AuthSessionOptions pattern, and each value is either: + + - A direct credential/token (when -AuthSessionType is provided) + - A typed descriptor: @{ AuthSessionType = 'Credential'; Credential = $credential } Keys can include AuthSessionName for name-based routing: - - @{ AuthSessionName = 'AD'; Role = 'ADAdm' } -> @{ AuthSessionType = 'Credential'; Session = $admAD } - - @{ AuthSessionName = 'EXO' } -> @{ AuthSessionType = 'OAuth'; Session = $exoToken } - - @{ Server = 'AADConnect01' } -> @{ AuthSessionType = 'PSRemoting'; Session = $remoteSession } - - @{ Domain = 'SourceAD' } -> @{ AuthSessionType = 'Credential'; Session = $sourceCred } - - @{ Environment = 'Production' } -> @{ AuthSessionType = 'Credential'; Session = $prodCred } + - @{ AuthSessionName = 'AD'; Role = 'ADAdm' } -> $admCred or @{ AuthSessionType = 'Credential'; Credential = $admAD } + - @{ AuthSessionName = 'EXO' } -> $exoToken or @{ AuthSessionType = 'OAuth'; Credential = $exoToken } + - @{ Server = 'AADConnect01' } -> $remoteSession + - @{ Domain = 'SourceAD' } -> $sourceCred + - @{ Environment = 'Production' } -> $prodCred SessionMap is optional if DefaultAuthSession is provided. @@ -33,56 +32,62 @@ function New-IdleAuthSessionBroker { Optional default auth session to return when no session options are provided or when the options don't match any entry in SessionMap. - Must be a typed session descriptor: - @{ AuthSessionType = 'Credential'; Session = $credential } + Can be a direct credential/token (when -AuthSessionType is provided) or a typed descriptor. At least one of SessionMap or DefaultAuthSession must be provided. + .PARAMETER AuthSessionType + Optional default authentication session type. When provided, allows simple (untyped) + session values in SessionMap and DefaultAuthSession. When not provided, all values + must be typed descriptors. + + Valid values: + - 'OAuth': Token-based authentication (e.g., Microsoft Graph, Exchange Online) + - 'PSRemoting': PowerShell remoting execution context (e.g., Entra Connect) + - 'Credential': Credential-based authentication (e.g., Active Directory, mock providers) + .EXAMPLE - # Simple single-credential broker - $broker = New-IdleAuthSessionBroker -DefaultAuthSession @{ - AuthSessionType = 'Credential' - Session = $admCred - } + # Simple single-credential broker (with AuthSessionType) + $broker = New-IdleAuthSessionBroker -DefaultAuthSession $admCred -AuthSessionType 'Credential' .EXAMPLE - # AuthSessionName-based routing with roles + # AuthSessionName-based routing with roles (with AuthSessionType) $broker = New-IdleAuthSessionBroker -SessionMap @{ - @{ AuthSessionName = 'AD'; Role = 'ADAdm' } = @{ AuthSessionType = 'Credential'; Session = $tier0Credential } - @{ AuthSessionName = 'AD'; Role = 'ADRead' } = @{ AuthSessionType = 'Credential'; Session = $readOnlyCredential } - } -DefaultAuthSession @{ AuthSessionType = 'Credential'; Session = $adminCredential } + @{ AuthSessionName = 'AD'; Role = 'ADAdm' } = $tier0Credential + @{ AuthSessionName = 'AD'; Role = 'ADRead' } = $readOnlyCredential + } -DefaultAuthSession $adminCredential -AuthSessionType 'Credential' .EXAMPLE - # OAuth broker with token strings + # OAuth broker with token strings (with AuthSessionType) $broker = New-IdleAuthSessionBroker -SessionMap @{ - @{ Role = 'Admin' } = @{ AuthSessionType = 'OAuth'; Session = $graphToken } - } -DefaultAuthSession @{ AuthSessionType = 'OAuth'; Session = $graphToken } + @{ Role = 'Admin' } = $graphToken + } -DefaultAuthSession $graphToken -AuthSessionType 'OAuth' .EXAMPLE - # Domain-based broker for multi-forest scenarios + # Domain-based broker for multi-forest scenarios (with AuthSessionType) $broker = New-IdleAuthSessionBroker -SessionMap @{ - @{ Domain = 'SourceAD' } = @{ AuthSessionType = 'Credential'; Session = $sourceCred } - @{ Domain = 'TargetAD' } = @{ AuthSessionType = 'Credential'; Session = $targetCred } - } + @{ Domain = 'SourceAD' } = $sourceCred + @{ Domain = 'TargetAD' } = $targetCred + } -AuthSessionType 'Credential' .EXAMPLE - # PSRemoting broker for Entra Connect directory sync + # PSRemoting broker for Entra Connect directory sync (with AuthSessionType) $broker = New-IdleAuthSessionBroker -SessionMap @{ - @{ Server = 'AADConnect01' } = @{ AuthSessionType = 'PSRemoting'; Session = $remoteSessionCred } - } + @{ Server = 'AADConnect01' } = $remoteSessionCred + } -AuthSessionType 'PSRemoting' .EXAMPLE - # Environment-based routing + # Environment-based routing (with AuthSessionType) $broker = New-IdleAuthSessionBroker -SessionMap @{ - @{ Environment = 'Production' } = @{ AuthSessionType = 'Credential'; Session = $prodCred } - @{ Environment = 'Test' } = @{ AuthSessionType = 'Credential'; Session = $testCred } - } -DefaultAuthSession @{ AuthSessionType = 'Credential'; Session = $devCred } + @{ Environment = 'Production' } = $prodCred + @{ Environment = 'Test' } = $testCred + } -DefaultAuthSession $devCred -AuthSessionType 'Credential' .EXAMPLE - # Mixed-type broker for AD (Credential) + EXO (OAuth) + # Mixed-type broker for AD (Credential) + EXO (OAuth) - typed descriptors $broker = New-IdleAuthSessionBroker -SessionMap @{ - @{ AuthSessionName = 'AD' } = @{ AuthSessionType = 'Credential'; Session = $adCred } - @{ AuthSessionName = 'EXO' } = @{ AuthSessionType = 'OAuth'; Session = $exoToken } + @{ AuthSessionName = 'AD' } = @{ AuthSessionType = 'Credential'; Credential = $adCred } + @{ AuthSessionName = 'EXO' } = @{ AuthSessionType = 'OAuth'; Credential = $exoToken } } .OUTPUTS @@ -97,7 +102,11 @@ function New-IdleAuthSessionBroker { [Parameter()] [AllowNull()] - [object] $DefaultAuthSession + [object] $DefaultAuthSession, + + [Parameter()] + [ValidateSet('OAuth', 'PSRemoting', 'Credential')] + [string] $AuthSessionType ) # Validate: If SessionMap is empty/null, DefaultAuthSession must be provided @@ -113,15 +122,9 @@ function New-IdleAuthSessionBroker { return $false } - # Check for hashtable with AuthSessionType and Session keys + # Only support hashtable format (not PSCustomObject) if ($value -is [hashtable]) { - return ($value.ContainsKey('AuthSessionType') -and $value.ContainsKey('Session')) - } - - # Check for PSCustomObject with AuthSessionType and Session properties - if ($value -is [pscustomobject]) { - $properties = $value.PSObject.Properties.Name - return (($properties -contains 'AuthSessionType') -and ($properties -contains 'Session')) + return ($value.ContainsKey('AuthSessionType') -and $value.ContainsKey('Credential')) } return $false @@ -129,28 +132,36 @@ function New-IdleAuthSessionBroker { # Helper function to normalize session value to internal format $normalizeSessionValue = { - param($value, $context) + param($value, $defaultType, $context) if ($null -eq $value) { return $null } - # Value must be typed - if (-not (& $isTypedSession $value)) { - throw "Session value in $context must be a typed session descriptor with 'AuthSessionType' and 'Session' properties. Example: @{ AuthSessionType = 'Credential'; Session = `$credential }" - } + # Check if value is typed + if (& $isTypedSession $value) { + $sessionType = $value.AuthSessionType + $credential = $value.Credential - $sessionType = $value.AuthSessionType - $session = $value.Session + # Validate the provided AuthSessionType + if ($sessionType -notin @('OAuth', 'PSRemoting', 'Credential')) { + throw "Invalid AuthSessionType '$sessionType' in $context. Valid values: 'OAuth', 'PSRemoting', 'Credential'." + } + + return @{ + AuthSessionType = $sessionType + Credential = $credential + } + } - # Validate the provided AuthSessionType - if ($sessionType -notin @('OAuth', 'PSRemoting', 'Credential')) { - throw "Invalid AuthSessionType '$sessionType' in $context. Valid values: 'OAuth', 'PSRemoting', 'Credential'." + # Untyped value - use default type + if ([string]::IsNullOrEmpty($defaultType)) { + throw "Untyped session value found in $context. Either provide -AuthSessionType or use typed format: @{ AuthSessionType = ''; Credential = `$value }" } return @{ - AuthSessionType = $sessionType - Session = $session + AuthSessionType = $defaultType + Credential = $value } } @@ -165,7 +176,7 @@ function New-IdleAuthSessionBroker { $patternDesc = ($pattern.Keys | ForEach-Object { "$_=$($pattern[$_])" }) -join ', ' $context = "SessionMap entry { $patternDesc }" - $normalizedValue = & $normalizeSessionValue $value $context + $normalizedValue = & $normalizeSessionValue $value $AuthSessionType $context $normalizedSessionMap[$pattern] = $normalizedValue } } @@ -173,13 +184,14 @@ function New-IdleAuthSessionBroker { # Normalize DefaultAuthSession $normalizedDefaultAuthSession = $null if ($null -ne $DefaultAuthSession) { - $normalizedDefaultAuthSession = & $normalizeSessionValue $DefaultAuthSession 'DefaultAuthSession' + $normalizedDefaultAuthSession = & $normalizeSessionValue $DefaultAuthSession $AuthSessionType 'DefaultAuthSession' } $broker = [pscustomobject]@{ PSTypeName = 'IdLE.AuthSessionBroker' SessionMap = $normalizedSessionMap DefaultAuthSession = $normalizedDefaultAuthSession + AuthSessionType = $AuthSessionType } $broker | Add-Member -MemberType ScriptMethod -Name AcquireAuthSession -Value { @@ -200,9 +212,9 @@ function New-IdleAuthSessionBroker { # Validate type before returning $validationScript = (Get-Command -Name 'Assert-IdleAuthSessionMatchesType' -ErrorAction Stop).ScriptBlock - & $validationScript -AuthSessionType $normalized.AuthSessionType -Session $normalized.Session -SessionName '' + & $validationScript -AuthSessionType $normalized.AuthSessionType -Session $normalized.Credential -SessionName '' - return $normalized.Session + return $normalized.Credential } throw "No default auth session configured." } @@ -214,9 +226,9 @@ function New-IdleAuthSessionBroker { # Validate type before returning $validationScript = (Get-Command -Name 'Assert-IdleAuthSessionMatchesType' -ErrorAction Stop).ScriptBlock - & $validationScript -AuthSessionType $normalized.AuthSessionType -Session $normalized.Session -SessionName $Name + & $validationScript -AuthSessionType $normalized.AuthSessionType -Session $normalized.Credential -SessionName $Name - return $normalized.Session + return $normalized.Credential } throw "No SessionMap configured and no default auth session available." } @@ -302,9 +314,9 @@ function New-IdleAuthSessionBroker { # Validate type before returning $validationScript = (Get-Command -Name 'Assert-IdleAuthSessionMatchesType' -ErrorAction Stop).ScriptBlock - & $validationScript -AuthSessionType $normalized.AuthSessionType -Session $normalized.Session -SessionName $Name + & $validationScript -AuthSessionType $normalized.AuthSessionType -Session $normalized.Credential -SessionName $Name - return $normalized.Session + return $normalized.Credential } # If multiple matches, this is ambiguous - fail with clear error @@ -323,9 +335,9 @@ function New-IdleAuthSessionBroker { # Validate type before returning $validationScript = (Get-Command -Name 'Assert-IdleAuthSessionMatchesType' -ErrorAction Stop).ScriptBlock - & $validationScript -AuthSessionType $normalized.AuthSessionType -Session $normalized.Session -SessionName $Name + & $validationScript -AuthSessionType $normalized.AuthSessionType -Session $normalized.Credential -SessionName $Name - return $normalized.Session + return $normalized.Credential } # No match and no default diff --git a/src/IdLE/Public/New-IdleAuthSession.ps1 b/src/IdLE/Public/New-IdleAuthSession.ps1 index 72a523b3..f708d9e5 100644 --- a/src/IdLE/Public/New-IdleAuthSession.ps1 +++ b/src/IdLE/Public/New-IdleAuthSession.ps1 @@ -19,23 +19,29 @@ function New-IdleAuthSession { This is a thin wrapper that delegates to IdLE.Core\New-IdleAuthSessionBroker. .PARAMETER SessionMap - A hashtable that maps session configurations to typed auth sessions. + A hashtable that maps session configurations to auth sessions. .PARAMETER DefaultAuthSession - Optional default typed auth session to return when no session options are provided. + Optional default auth session to return when no session options are provided. + + .PARAMETER AuthSessionType + Optional default authentication session type. When provided, allows simple (untyped) + session values. When not provided, values must be typed descriptors. + + Valid values: + - 'OAuth': Token-based authentication (e.g., Microsoft Graph, Exchange Online) + - 'PSRemoting': PowerShell remoting execution context (e.g., Entra Connect) + - 'Credential': Credential-based authentication (e.g., Active Directory, mock providers) .EXAMPLE # Simple broker with single credential - $broker = New-IdleAuthSession -DefaultAuthSession @{ - AuthSessionType = 'Credential' - Session = $credential - } + $broker = New-IdleAuthSession -DefaultAuthSession $credential -AuthSessionType 'Credential' .EXAMPLE # Mixed-type broker for AD + EXO $broker = New-IdleAuthSession -SessionMap @{ - @{ AuthSessionName = 'AD' } = @{ AuthSessionType = 'Credential'; Session = $adCred } - @{ AuthSessionName = 'EXO' } = @{ AuthSessionType = 'OAuth'; Session = $token } + @{ AuthSessionName = 'AD' } = @{ AuthSessionType = 'Credential'; Credential = $adCred } + @{ AuthSessionName = 'EXO' } = @{ AuthSessionType = 'OAuth'; Credential = $token } } .OUTPUTS @@ -53,7 +59,11 @@ function New-IdleAuthSession { [Parameter()] [AllowNull()] - [object] $DefaultAuthSession + [object] $DefaultAuthSession, + + [Parameter()] + [ValidateSet('OAuth', 'PSRemoting', 'Credential')] + [string] $AuthSessionType ) # Delegate to IdLE.Core implementation. @@ -64,6 +74,9 @@ function New-IdleAuthSession { if ($PSBoundParameters.ContainsKey('DefaultAuthSession')) { $params['DefaultAuthSession'] = $DefaultAuthSession } + if ($PSBoundParameters.ContainsKey('AuthSessionType')) { + $params['AuthSessionType'] = $AuthSessionType + } return IdLE.Core\New-IdleAuthSessionBroker @params } diff --git a/tests/Core/New-IdleAuthSession.Tests.ps1 b/tests/Core/New-IdleAuthSession.Tests.ps1 index 10ff89c1..ba98a0b0 100644 --- a/tests/Core/New-IdleAuthSession.Tests.ps1 +++ b/tests/Core/New-IdleAuthSession.Tests.ps1 @@ -11,113 +11,63 @@ Describe 'New-IdleAuthSession' { $testToken = 'mock-oauth-token-12345' } - It 'creates an auth session broker with the expected type' { - $broker = New-IdleAuthSession -SessionMap @{ - @{ Role = 'AD' } = @{ AuthSessionType = 'Credential'; Session = $testCred } - } - - $broker | Should -Not -BeNullOrEmpty - $broker.PSTypeNames | Should -Contain 'IdLE.AuthSessionBroker' - } - - It 'creates broker with AcquireAuthSession method' { - $broker = New-IdleAuthSession -SessionMap @{ - @{ Role = 'AD' } = @{ AuthSessionType = 'Credential'; Session = $testCred } - } - - $broker.PSObject.Methods['AcquireAuthSession'] | Should -Not -BeNullOrEmpty - } - - It 'accepts SessionMap parameter with typed values' { - $sessionMap = @{ - @{ Role = 'Tier0' } = @{ AuthSessionType = 'Credential'; Session = $testCred } - @{ Role = 'Admin' } = @{ AuthSessionType = 'Credential'; Session = $testCred } + Context 'Simple syntax with AuthSessionType' { + It 'creates broker with single credential' { + $broker = New-IdleAuthSession -DefaultAuthSession $testCred -AuthSessionType 'Credential' + + $broker | Should -Not -BeNullOrEmpty + $broker.PSTypeNames | Should -Contain 'IdLE.AuthSessionBroker' } - - $broker = New-IdleAuthSession -SessionMap $sessionMap - - $broker.SessionMap | Should -Not -BeNullOrEmpty - $broker.SessionMap.Count | Should -Be 2 - } - - It 'accepts optional DefaultAuthSession parameter' { - $broker = New-IdleAuthSession -SessionMap @{ - @{ Role = 'AD' } = @{ AuthSessionType = 'Credential'; Session = $testCred } - } -DefaultAuthSession @{ AuthSessionType = 'Credential'; Session = $testCred } - - $broker.DefaultAuthSession | Should -Not -BeNullOrEmpty - # Test via AcquireAuthSession (empty name signals default) - $session = $broker.AcquireAuthSession('', $null) - $session.UserName | Should -Be 'TestUser' - } - It 'broker can acquire auth session with matching options' { - $broker = New-IdleAuthSession -SessionMap @{ - @{ Role = 'Tier0' } = @{ AuthSessionType = 'Credential'; Session = $testCred } + It 'accepts SessionMap with untyped values when AuthSessionType provided' { + $broker = New-IdleAuthSession -SessionMap @{ + @{ Role = 'Tier0' } = $testCred + @{ Role = 'Admin' } = $testCred + } -AuthSessionType 'Credential' + + $broker.SessionMap | Should -Not -BeNullOrEmpty + $broker.SessionMap.Count | Should -Be 2 } - - $acquiredSession = $broker.AcquireAuthSession('TestName', @{ Role = 'Tier0' }) - - $acquiredSession | Should -Not -BeNullOrEmpty - $acquiredSession | Should -BeOfType [PSCredential] - $acquiredSession.UserName | Should -Be 'TestUser' - } - It 'broker returns default auth session when no match found' { - $defaultPassword = ConvertTo-SecureString 'DefaultPassword!' -AsPlainText -Force - $defaultCred = New-Object System.Management.Automation.PSCredential('DefaultUser', $defaultPassword) - - $broker = New-IdleAuthSession -SessionMap @{ - @{ Role = 'Tier0' } = @{ AuthSessionType = 'Credential'; Session = $testCred } - } -DefaultAuthSession @{ AuthSessionType = 'Credential'; Session = $defaultCred } - - $acquiredSession = $broker.AcquireAuthSession('TestName', $null) - - $acquiredSession | Should -Not -BeNullOrEmpty - $acquiredSession.UserName | Should -Be 'DefaultUser' - } - - It 'throws when no matching auth session found and no default provided' { - $broker = New-IdleAuthSession -SessionMap @{ - @{ Role = 'Tier0' } = @{ AuthSessionType = 'Credential'; Session = $testCred } + It 'broker can acquire auth session with matching options' { + $broker = New-IdleAuthSession -SessionMap @{ + @{ Role = 'Tier0' } = $testCred + } -AuthSessionType 'Credential' + + $acquiredSession = $broker.AcquireAuthSession('TestName', @{ Role = 'Tier0' }) + + $acquiredSession | Should -Not -BeNullOrEmpty + $acquiredSession | Should -BeOfType [PSCredential] + $acquiredSession.UserName | Should -Be 'TestUser' } - - { $broker.AcquireAuthSession('TestName', @{ Role = 'NonExistent' }) } | - Should -Throw '*No matching auth session found*' - } - - It 'is available as exported command from IdLE module' { - $command = Get-Command -Name New-IdleAuthSession -ErrorAction SilentlyContinue - - $command | Should -Not -BeNullOrEmpty - $command.Name | Should -Be 'New-IdleAuthSession' - $command.Module.Name | Should -Be 'IdLE' - } - It 'delegates to IdLE.Core\New-IdleAuthSessionBroker correctly' { - { + It 'broker returns default auth session when no match found' { + $defaultPassword = ConvertTo-SecureString 'DefaultPassword!' -AsPlainText -Force + $defaultCred = New-Object System.Management.Automation.PSCredential('DefaultUser', $defaultPassword) + $broker = New-IdleAuthSession -SessionMap @{ - @{ Role = 'AD' } = @{ AuthSessionType = 'Credential'; Session = $testCred } - } -ErrorAction Stop + @{ Role = 'Tier0' } = $testCred + } -DefaultAuthSession $defaultCred -AuthSessionType 'Credential' - $broker | Should -Not -BeNullOrEmpty - } | Should -Not -Throw - } + $acquiredSession = $broker.AcquireAuthSession('TestName', $null) + + $acquiredSession | Should -Not -BeNullOrEmpty + $acquiredSession.UserName | Should -Be 'DefaultUser' + } - Context 'Typed session validation' { - It 'accepts Credential session type' { + It 'throws when no matching auth session found and no default provided' { $broker = New-IdleAuthSession -SessionMap @{ - @{ Domain = 'corp.example.com' } = @{ AuthSessionType = 'Credential'; Session = $testCred } - } + @{ Role = 'Tier0' } = $testCred + } -AuthSessionType 'Credential' - $session = $broker.AcquireAuthSession('AD', @{ Domain = 'corp.example.com' }) - $session | Should -BeOfType [PSCredential] + { $broker.AcquireAuthSession('TestName', @{ Role = 'NonExistent' }) } | + Should -Throw '*No matching auth session found*' } It 'accepts OAuth session type' { $broker = New-IdleAuthSession -SessionMap @{ - @{ Role = 'Admin' } = @{ AuthSessionType = 'OAuth'; Session = $testToken } - } + @{ Role = 'Admin' } = $testToken + } -AuthSessionType 'OAuth' $session = $broker.AcquireAuthSession('Graph', @{ Role = 'Admin' }) $session | Should -BeOfType [string] @@ -126,141 +76,84 @@ Describe 'New-IdleAuthSession' { It 'accepts PSRemoting session type' { $broker = New-IdleAuthSession -SessionMap @{ - @{ Server = 'AADConnect01' } = @{ AuthSessionType = 'PSRemoting'; Session = $testCred } - } + @{ Server = 'AADConnect01' } = $testCred + } -AuthSessionType 'PSRemoting' $session = $broker.AcquireAuthSession('Remote', @{ Server = 'AADConnect01' }) $session | Should -BeOfType [PSCredential] } + } - It 'validates Credential type matches PSCredential' { + Context 'Typed syntax for mixed types' { + It 'supports typed SessionMap values with AuthSessionType property' { $broker = New-IdleAuthSession -SessionMap @{ - @{ AuthSessionName = 'AD' } = @{ AuthSessionType = 'Credential'; Session = $testCred } + @{ AuthSessionName = 'AD' } = @{ AuthSessionType = 'Credential'; Credential = $testCred } } - # Should succeed $session = $broker.AcquireAuthSession('AD', $null) $session | Should -BeOfType [PSCredential] } - It 'throws when Credential type receives non-PSCredential object' { - { - $broker = New-IdleAuthSession -SessionMap @{ - @{ AuthSessionName = 'AD' } = @{ AuthSessionType = 'Credential'; Session = 'not-a-credential' } - } - $broker.AcquireAuthSession('AD', $null) - } | Should -Throw '*Expected AuthSessionType=''Credential'' requires a*PSCredential*' - } - - It 'validates OAuth type matches string token' { + It 'supports mixed types in single SessionMap' { $broker = New-IdleAuthSession -SessionMap @{ - @{ AuthSessionName = 'EXO' } = @{ AuthSessionType = 'OAuth'; Session = $testToken } + @{ AuthSessionName = 'AD' } = @{ AuthSessionType = 'Credential'; Credential = $testCred } + @{ AuthSessionName = 'EXO' } = @{ AuthSessionType = 'OAuth'; Credential = $testToken } } - # Should succeed - $session = $broker.AcquireAuthSession('EXO', $null) - $session | Should -BeOfType [string] - } - - It 'throws when OAuth type receives non-string object' { - { - $broker = New-IdleAuthSession -SessionMap @{ - @{ AuthSessionName = 'EXO' } = @{ AuthSessionType = 'OAuth'; Session = $testCred } - } - $broker.AcquireAuthSession('EXO', $null) - } | Should -Throw '*Expected AuthSessionType=''OAuth'' requires a*string*' - } - - It 'validates PSRemoting type matches PSCredential' { - $broker = New-IdleAuthSession -SessionMap @{ - @{ AuthSessionName = 'Remote' } = @{ AuthSessionType = 'PSRemoting'; Session = $testCred } - } + $adSession = $broker.AcquireAuthSession('AD', $null) + $adSession | Should -BeOfType [PSCredential] - # Should succeed (PSCredential is valid for PSRemoting) - $session = $broker.AcquireAuthSession('Remote', $null) - $session | Should -BeOfType [PSCredential] + $exoSession = $broker.AcquireAuthSession('EXO', $null) + $exoSession | Should -BeOfType [string] + $exoSession | Should -Be 'mock-oauth-token-12345' } - It 'throws when PSRemoting type receives invalid object' { + It 'throws when untyped value provided without AuthSessionType' { { - $broker = New-IdleAuthSession -SessionMap @{ - @{ AuthSessionName = 'Remote' } = @{ AuthSessionType = 'PSRemoting'; Session = 'not-valid' } + New-IdleAuthSession -SessionMap @{ + @{ AuthSessionName = 'AD' } = $testCred # Untyped } - $broker.AcquireAuthSession('Remote', $null) - } | Should -Throw '*Expected AuthSessionType=''PSRemoting''*' + } | Should -Throw '*Untyped session value*' } It 'throws when invalid AuthSessionType provided' { { New-IdleAuthSession -SessionMap @{ - @{ AuthSessionName = 'AD' } = @{ AuthSessionType = 'InvalidType'; Session = $testCred } + @{ AuthSessionName = 'AD' } = @{ AuthSessionType = 'InvalidType'; Credential = $testCred } } } | Should -Throw '*Invalid AuthSessionType*' } + } - It 'throws when untyped session value provided' { - { - New-IdleAuthSession -SessionMap @{ - @{ AuthSessionName = 'AD' } = $testCred # Untyped - } - } | Should -Throw '*must be a typed session descriptor*' - } - - It 'throws when untyped DefaultAuthSession provided' { - { - New-IdleAuthSession -SessionMap @{ - @{ AuthSessionName = 'AD' } = @{ AuthSessionType = 'Credential'; Session = $testCred } - } -DefaultAuthSession $testCred # Untyped default - } | Should -Throw '*must be a typed session descriptor*' + Context 'Type validation' { + It 'validates Credential type matches PSCredential' { + $broker = New-IdleAuthSession -DefaultAuthSession $testCred -AuthSessionType 'Credential' + $session = $broker.AcquireAuthSession('', $null) + $session | Should -BeOfType [PSCredential] } - It 'throws with clear error including session name on type mismatch' { + It 'throws when Credential type receives non-PSCredential object' { { $broker = New-IdleAuthSession -SessionMap @{ - @{ AuthSessionName = 'MyCustomSession' } = @{ AuthSessionType = 'Credential'; Session = 'wrong-type' } + @{ AuthSessionName = 'AD' } = @{ AuthSessionType = 'Credential'; Credential = 'not-a-credential' } } - $broker.AcquireAuthSession('MyCustomSession', $null) - } | Should -Throw '*MyCustomSession*' - } - } - - Context 'Optional SessionMap' { - It 'creates broker with only DefaultAuthSession (no SessionMap)' { - $broker = New-IdleAuthSession -DefaultAuthSession @{ AuthSessionType = 'Credential'; Session = $testCred } - - $broker | Should -Not -BeNullOrEmpty - $broker.DefaultAuthSession | Should -Not -BeNullOrEmpty - $broker.SessionMap | Should -BeNullOrEmpty - } - - It 'returns DefaultAuthSession when SessionMap is null' { - $broker = New-IdleAuthSession -DefaultAuthSession @{ AuthSessionType = 'Credential'; Session = $testCred } - - $session = $broker.AcquireAuthSession('AnyName', $null) - - $session | Should -Not -BeNullOrEmpty - $session.UserName | Should -Be 'TestUser' - } - - It 'returns DefaultAuthSession when SessionMap is empty' { - $broker = New-IdleAuthSession -SessionMap @{} -DefaultAuthSession @{ AuthSessionType = 'Credential'; Session = $testCred } - - $session = $broker.AcquireAuthSession('AnyName', $null) - - $session | Should -Not -BeNullOrEmpty - $session.UserName | Should -Be 'TestUser' + $broker.AcquireAuthSession('AD', $null) + } | Should -Throw '*Expected AuthSessionType=''Credential'' requires a*PSCredential*' } - It 'throws when SessionMap is null and DefaultAuthSession is not provided' { - { - New-IdleAuthSession -SessionMap $null - } | Should -Throw '*DefaultAuthSession must be provided*' + It 'validates OAuth type matches string token' { + $broker = New-IdleAuthSession -DefaultAuthSession $testToken -AuthSessionType 'OAuth' + $session = $broker.AcquireAuthSession('', $null) + $session | Should -BeOfType [string] } - It 'throws when SessionMap is empty and DefaultAuthSession is not provided' { - { - New-IdleAuthSession -SessionMap @{} - } | Should -Throw '*DefaultAuthSession must be provided*' + It 'throws when OAuth type receives non-string object' { + { + $broker = New-IdleAuthSession -SessionMap @{ + @{ AuthSessionName = 'EXO' } = @{ AuthSessionType = 'OAuth'; Credential = $testCred } + } + $broker.AcquireAuthSession('EXO', $null) + } | Should -Throw '*Expected AuthSessionType=''OAuth'' requires a*string*' } } @@ -271,175 +164,62 @@ Describe 'New-IdleAuthSession' { $password2 = ConvertTo-SecureString 'Password2!' -AsPlainText -Force $cred2 = New-Object System.Management.Automation.PSCredential('EXOAdm', $password2) - - $password3 = ConvertTo-SecureString 'Password3!' -AsPlainText -Force - $cred3 = New-Object System.Management.Automation.PSCredential('ADRead', $password3) } It 'matches AuthSessionName without options' { $broker = New-IdleAuthSession -SessionMap @{ - @{ AuthSessionName = 'AD' } = @{ AuthSessionType = 'Credential'; Session = $cred1 } - @{ AuthSessionName = 'EXO' } = @{ AuthSessionType = 'Credential'; Session = $cred2 } - } + @{ AuthSessionName = 'AD' } = $cred1 + @{ AuthSessionName = 'EXO' } = $cred2 + } -AuthSessionType 'Credential' $session = $broker.AcquireAuthSession('AD', $null) - - $session | Should -Not -BeNullOrEmpty $session.UserName | Should -Be 'ADAdm' } It 'matches AuthSessionName with matching options' { + $password3 = ConvertTo-SecureString 'Password3!' -AsPlainText -Force + $cred3 = New-Object System.Management.Automation.PSCredential('ADRead', $password3) + $broker = New-IdleAuthSession -SessionMap @{ - @{ AuthSessionName = 'AD'; Role = 'ADAdm' } = @{ AuthSessionType = 'Credential'; Session = $cred1 } - @{ AuthSessionName = 'AD'; Role = 'ADRead' } = @{ AuthSessionType = 'Credential'; Session = $cred3 } - } + @{ AuthSessionName = 'AD'; Role = 'ADAdm' } = $cred1 + @{ AuthSessionName = 'AD'; Role = 'ADRead' } = $cred3 + } -AuthSessionType 'Credential' $session = $broker.AcquireAuthSession('AD', @{ Role = 'ADRead' }) - - $session | Should -Not -BeNullOrEmpty $session.UserName | Should -Be 'ADRead' } - It 'falls back to default when AuthSessionName does not match' { - $broker = New-IdleAuthSession -SessionMap @{ - @{ AuthSessionName = 'AD' } = @{ AuthSessionType = 'Credential'; Session = $cred1 } - } -DefaultAuthSession @{ AuthSessionType = 'Credential'; Session = $testCred } - - $session = $broker.AcquireAuthSession('EXO', $null) - - $session | Should -Not -BeNullOrEmpty - $session.UserName | Should -Be 'TestUser' - } - - It 'throws when AuthSessionName matches multiple entries (ambiguous)' { - $broker = New-IdleAuthSession -SessionMap @{ - @{ AuthSessionName = 'AD' } = @{ AuthSessionType = 'Credential'; Session = $cred1 } - @{ AuthSessionName = 'AD' } = @{ AuthSessionType = 'Credential'; Session = $cred3 } - } - - { $broker.AcquireAuthSession('AD', $null) } | - Should -Throw '*Ambiguous*' - } - - It 'prefers AuthSessionName match over Options-only match' { - $broker = New-IdleAuthSession -SessionMap @{ - @{ Role = 'Admin' } = @{ AuthSessionType = 'Credential'; Session = $testCred } - @{ AuthSessionName = 'AD'; Role = 'Admin' } = @{ AuthSessionType = 'Credential'; Session = $cred1 } - } - - $session = $broker.AcquireAuthSession('AD', @{ Role = 'Admin' }) - - $session | Should -Not -BeNullOrEmpty - $session.UserName | Should -Be 'ADAdm' - } - It 'supports Options-only routing when AuthSessionName is not in pattern' { $broker = New-IdleAuthSession -SessionMap @{ - @{ Role = 'Tier0' } = @{ AuthSessionType = 'Credential'; Session = $cred1 } - @{ Role = 'Admin' } = @{ AuthSessionType = 'Credential'; Session = $cred2 } - } + @{ Role = 'Tier0' } = $cred1 + @{ Role = 'Admin' } = $cred2 + } -AuthSessionType 'Credential' $session = $broker.AcquireAuthSession('AnyName', @{ Role = 'Admin' }) - - $session | Should -Not -BeNullOrEmpty $session.UserName | Should -Be 'EXOAdm' } + } - It 'throws when AuthSessionName does not match and no default provided' { - $broker = New-IdleAuthSession -SessionMap @{ - @{ AuthSessionName = 'AD' } = @{ AuthSessionType = 'Credential'; Session = $cred1 } - } - - { $broker.AcquireAuthSession('EXO', $null) } | - Should -Throw '*No matching auth session found*' - } - - It 'matches complex pattern: AuthSessionName + multiple options' { - $broker = New-IdleAuthSession -SessionMap @{ - @{ AuthSessionName = 'AD'; Role = 'Admin'; Environment = 'Prod' } = @{ AuthSessionType = 'Credential'; Session = $cred1 } - } - - $session = $broker.AcquireAuthSession('AD', @{ Role = 'Admin'; Environment = 'Prod' }) + Context 'Optional SessionMap' { + It 'creates broker with only DefaultAuthSession' { + $broker = New-IdleAuthSession -DefaultAuthSession $testCred -AuthSessionType 'Credential' - $session | Should -Not -BeNullOrEmpty - $session.UserName | Should -Be 'ADAdm' + $broker | Should -Not -BeNullOrEmpty + $broker.DefaultAuthSession | Should -Not -BeNullOrEmpty } - It 'does not match when partial options provided' { - $broker = New-IdleAuthSession -SessionMap @{ - @{ AuthSessionName = 'AD'; Role = 'Admin'; Environment = 'Prod' } = @{ AuthSessionType = 'Credential'; Session = $cred1 } - } -DefaultAuthSession @{ AuthSessionType = 'Credential'; Session = $testCred } - - # Only providing Role, not Environment - should fall back to default - $session = $broker.AcquireAuthSession('AD', @{ Role = 'Admin' }) - - $session | Should -Not -BeNullOrEmpty - $session.UserName | Should -Be 'TestUser' + It 'throws when SessionMap is null and DefaultAuthSession is not provided' { + { + New-IdleAuthSession -SessionMap $null -AuthSessionType 'Credential' + } | Should -Throw '*DefaultAuthSession must be provided*' } } - Context 'Mixed authentication types' { - BeforeEach { - $password = ConvertTo-SecureString 'Password!' -AsPlainText -Force - $adCred = New-Object System.Management.Automation.PSCredential('ADUser', $password) - $exoToken = 'mock-oauth-token-12345' - } - - It 'supports mixed types in single SessionMap' { - $broker = New-IdleAuthSession -SessionMap @{ - @{ AuthSessionName = 'AD' } = @{ AuthSessionType = 'Credential'; Session = $adCred } - @{ AuthSessionName = 'EXO' } = @{ AuthSessionType = 'OAuth'; Session = $exoToken } - } - - $adSession = $broker.AcquireAuthSession('AD', $null) - $adSession | Should -BeOfType [PSCredential] - - $exoSession = $broker.AcquireAuthSession('EXO', $null) - $exoSession | Should -BeOfType [string] - $exoSession | Should -Be 'mock-oauth-token-12345' - } - - It 'validates typed DefaultAuthSession with different type than SessionMap' { - $broker = New-IdleAuthSession -SessionMap @{ - @{ AuthSessionName = 'AD' } = @{ AuthSessionType = 'Credential'; Session = $adCred } - } -DefaultAuthSession @{ AuthSessionType = 'OAuth'; Session = $exoToken } - - $defaultSession = $broker.AcquireAuthSession('Unknown', $null) - $defaultSession | Should -BeOfType [string] - $defaultSession | Should -Be 'mock-oauth-token-12345' - } - - It 'multi-provider scenario: AD (Credential) + EXO (OAuth)' { - # Real-world scenario: mixed authentication types in single broker - $broker = New-IdleAuthSession -SessionMap @{ - @{ AuthSessionName = 'AD'; Role = 'Admin' } = @{ AuthSessionType = 'Credential'; Session = $adCred } - @{ AuthSessionName = 'EXO' } = @{ AuthSessionType = 'OAuth'; Session = $exoToken } - } - - # Acquire AD session - $adSession = $broker.AcquireAuthSession('AD', @{ Role = 'Admin' }) - $adSession | Should -BeOfType [PSCredential] - $adSession.UserName | Should -Be 'ADUser' - - # Acquire EXO session - $exoSession = $broker.AcquireAuthSession('EXO', $null) - $exoSession | Should -BeOfType [string] - $exoSession | Should -Be 'mock-oauth-token-12345' - } - - It 'supports PSCustomObject format for typed sessions' { - $typedSession = [pscustomobject]@{ - AuthSessionType = 'OAuth' - Session = $exoToken - } - - $broker = New-IdleAuthSession -SessionMap @{ - @{ AuthSessionName = 'EXO' } = $typedSession - } - - $session = $broker.AcquireAuthSession('EXO', $null) - $session | Should -BeOfType [string] - $session | Should -Be 'mock-oauth-token-12345' - } + It 'is available as exported command from IdLE module' { + $command = Get-Command -Name New-IdleAuthSession -ErrorAction SilentlyContinue + + $command | Should -Not -BeNullOrEmpty + $command.Name | Should -Be 'New-IdleAuthSession' + $command.Module.Name | Should -Be 'IdLE' } } From e59269b55bfb54c606820a7d8166adf2221b3cd0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 9 Feb 2026 20:19:58 +0000 Subject: [PATCH 06/13] docs: update EntraID provider reference for new AuthSession syntax - Remove deprecated empty @{} pattern - Use simple -DefaultAuthSession syntax for single token - Keep -AuthSessionType for all examples (simple syntax) - Consistent with main provider usage documentation Co-authored-by: blindzero <13959569+blindzero@users.noreply.github.com> --- docs/reference/providers/provider-entraID.md | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/docs/reference/providers/provider-entraID.md b/docs/reference/providers/provider-entraID.md index bc633a6b..a98331f0 100644 --- a/docs/reference/providers/provider-entraID.md +++ b/docs/reference/providers/provider-entraID.md @@ -86,10 +86,8 @@ The provider accepts authentication sessions in these formats: Connect-AzAccount $token = (Get-AzAccessToken -ResourceUrl "https://graph.microsoft.com").Token -# Create broker with OAuth session type (tokens can be passed directly) -$broker = New-IdleAuthSession -SessionMap @{ - @{} = $token -} -DefaultAuthSession $token -AuthSessionType 'OAuth' +# Create broker with OAuth session type +$broker = New-IdleAuthSession -DefaultAuthSession $token -AuthSessionType 'OAuth' # Create provider $provider = New-IdleEntraIDIdentityProvider @@ -112,10 +110,8 @@ $tenantId = "your-tenant-id" # Obtain token (pseudo-code - use your preferred auth library) $token = Get-GraphAppOnlyToken -ClientId $clientId -ClientSecret $clientSecret -TenantId $tenantId -# Create broker with OAuth session type (tokens can be passed directly) -$broker = New-IdleAuthSession -SessionMap @{ - @{} = $token -} -DefaultAuthSession $token -AuthSessionType 'OAuth' +# Create broker with OAuth session type +$broker = New-IdleAuthSession -DefaultAuthSession $token -AuthSessionType 'OAuth' # Rest is identical to delegated flow ``` @@ -126,7 +122,7 @@ $broker = New-IdleAuthSession -SessionMap @{ $tier0Token = Get-GraphToken -Role 'Tier0' $adminToken = Get-GraphToken -Role 'Admin' -# Create broker with OAuth session type (tokens can be passed directly) +# Create broker with role-based routing $broker = New-IdleAuthSession -SessionMap @{ @{ Role = 'Tier0' } = $tier0Token @{ Role = 'Admin' } = $adminToken From a1b45600dbaadea5226ea76fb1f244faa1027e26 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 9 Feb 2026 20:47:09 +0000 Subject: [PATCH 07/13] docs: add Device Code Flow example for EntraID provider - Add complete Device Code Flow example with MSAL.PS - Document prerequisites (MSAL.PS module, app registration) - Show how to configure app for public client flows - Include full example with broker setup - Addresses MFA-enabled environments use case Co-authored-by: blindzero <13959569+blindzero@users.noreply.github.com> --- docs/reference/providers/provider-entraID.md | 38 ++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/docs/reference/providers/provider-entraID.md b/docs/reference/providers/provider-entraID.md index a98331f0..1f89ac30 100644 --- a/docs/reference/providers/provider-entraID.md +++ b/docs/reference/providers/provider-entraID.md @@ -116,6 +116,44 @@ $broker = New-IdleAuthSession -DefaultAuthSession $token -AuthSessionType 'OAuth # Rest is identical to delegated flow ``` +### Example: Device Code Flow (MFA-enabled environments) + +For environments requiring MFA, use Device Code Flow with an app registration and MSAL.PS. + +**Prerequisites:** +- Install MSAL.PS module: `Install-Module MSAL.PS -Scope CurrentUser` +- App registration with delegated permissions (e.g., `User.ReadWrite.All`, `Group.ReadWrite.All`) +- App must allow public client flows (Authentication > Advanced settings > Allow public client flows: Yes) + +```powershell +# Import MSAL.PS +Import-Module MSAL.PS + +# Obtain token via Device Code Flow +$clientId = "your-app-id" # Application (client) ID from app registration +$tenantId = "your-tenant-id" + +$token = Get-MsalToken ` + -ClientId $clientId ` + -TenantId $tenantId ` + -Scopes "https://graph.microsoft.com/.default" ` + -DeviceCode + +# Create broker with OAuth session type +$broker = New-IdleAuthSession -DefaultAuthSession $token.AccessToken -AuthSessionType 'OAuth' + +# Create provider +$provider = New-IdleEntraIDIdentityProvider + +# Use in plan +$plan = New-IdlePlan -WorkflowPath './workflow.psd1' -Request $request -Providers @{ + Identity = $provider + AuthSessionBroker = $broker +} +``` + +The Device Code Flow will display a code and URL for the user to authenticate in a browser, supporting MFA and conditional access policies. + ### Example: Multi-Role Scenario ```powershell From 5efdb562cadcea8254213f56f8c347425b1a03c1 Mon Sep 17 00:00:00 2001 From: Matthias <13959569+blindzero@users.noreply.github.com> Date: Mon, 9 Feb 2026 22:03:23 +0100 Subject: [PATCH 08/13] Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- docs/use/providers.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/use/providers.md b/docs/use/providers.md index 315c8036..0479f557 100644 --- a/docs/use/providers.md +++ b/docs/use/providers.md @@ -243,7 +243,8 @@ For workflows that need multiple providers with different authentication types: ```powershell # Obtain credentials and tokens $adCredential = Get-Credential -Message "Enter AD admin credentials" -$exoToken = (Connect-AzAccount | Get-AzAccessToken -ResourceUrl "https://outlook.office365.com").Token +Connect-AzAccount +$exoToken = (Get-AzAccessToken -ResourceUrl "https://outlook.office365.com").Token # Create providers $adProvider = New-IdleADIdentityProvider From 0e261a1eb5b43c3e2fcb24702cd13d9d5d04e70d Mon Sep 17 00:00:00 2001 From: Matthias <13959569+blindzero@users.noreply.github.com> Date: Mon, 9 Feb 2026 22:06:16 +0100 Subject: [PATCH 09/13] Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/IdLE.Core/Public/New-IdleAuthSessionBroker.ps1 | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/IdLE.Core/Public/New-IdleAuthSessionBroker.ps1 b/src/IdLE.Core/Public/New-IdleAuthSessionBroker.ps1 index 9100b1fb..8045f8c0 100644 --- a/src/IdLE.Core/Public/New-IdleAuthSessionBroker.ps1 +++ b/src/IdLE.Core/Public/New-IdleAuthSessionBroker.ps1 @@ -172,6 +172,14 @@ function New-IdleAuthSessionBroker { $pattern = $entry.Key $value = $entry.Value + # Validate SessionMap key type before using hashtable members + if ($null -eq $pattern -or -not ($pattern -is [hashtable])) { + $patternType = if ($null -eq $pattern) { 'null' } else { $pattern.GetType().FullName } + throw [System.ArgumentException]::new( + "Invalid SessionMap key type '$patternType'. SessionMap keys must be hashtables representing the AuthSessionOptions pattern.", + 'SessionMap' + ) + } # Create a readable pattern description for error messages $patternDesc = ($pattern.Keys | ForEach-Object { "$_=$($pattern[$_])" }) -join ', ' $context = "SessionMap entry { $patternDesc }" From 6bfe706d75c2b85c3729a9255f6dc1d6cab73017 Mon Sep 17 00:00:00 2001 From: Matthias <13959569+blindzero@users.noreply.github.com> Date: Mon, 9 Feb 2026 22:07:31 +0100 Subject: [PATCH 10/13] Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../Assert-IdleAuthSessionMatchesType.ps1 | 41 +++++++++++++++++-- 1 file changed, 38 insertions(+), 3 deletions(-) diff --git a/src/IdLE.Core/Private/Assert-IdleAuthSessionMatchesType.ps1 b/src/IdLE.Core/Private/Assert-IdleAuthSessionMatchesType.ps1 index eef774fe..42984f1d 100644 --- a/src/IdLE.Core/Private/Assert-IdleAuthSessionMatchesType.ps1 +++ b/src/IdLE.Core/Private/Assert-IdleAuthSessionMatchesType.ps1 @@ -47,10 +47,45 @@ function Assert-IdleAuthSessionMatchesType { } 'OAuth' { - # Accept string tokens as the primary OAuth session format - if ($Session -isnot [string]) { + # Accept multiple OAuth session shapes: + # - [string] raw bearer token + # - [PSCredential] with token in the Password field + # - object with an AccessToken property + # - object with a GetAccessToken() method + $isValid = $false + + if ($Session -is [string]) { + $isValid = $true + } + elseif ($Session -is [pscredential]) { + $isValid = $true + } + elseif ($null -ne $Session) { + $psObj = [System.Management.Automation.PSObject]::AsPSObject($Session) + + # Check for AccessToken property + if ($psObj.Properties['AccessToken']) { + $isValid = $true + } + else { + # Check for GetAccessToken() method + $getTokenMethod = $psObj | Get-Member -Name GetAccessToken -MemberType Method -ErrorAction SilentlyContinue + if ($null -ne $getTokenMethod) { + $isValid = $true + } + } + } + + if (-not $isValid) { $actualType = $Session.GetType().FullName - throw "Auth session validation failed for '$SessionName': Expected AuthSessionType='OAuth' requires a [string] token, but received [$actualType]." + throw @" +Auth session validation failed for '$SessionName': Expected AuthSessionType='OAuth' requires one of: +- [string] raw access token +- [PSCredential] with the access token in the Password field +- object with an AccessToken property +- object with a GetAccessToken() method +but received [$actualType]. +"@ } } From 6e59a07433d81ca3a612016ac87c6acb0c989c0f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 9 Feb 2026 21:13:45 +0000 Subject: [PATCH 11/13] fix: address code review feedback - Fix PSRemoting validation to accept InvokeCommand method pattern (DirectorySync provider) - Re-add missing routing behavior tests (fallback, ambiguity, precedence) - Optimize validation function lookup (cache scriptblock on broker, avoid repeated Get-Command) - Fix OAuth validation to safely check for AccessToken property/method - Update test to use invalid type for OAuth validation All 24 tests pass. Cmdlet reference will be regenerated by CI. Co-authored-by: blindzero <13959569+blindzero@users.noreply.github.com> --- .../Assert-IdleAuthSessionMatchesType.ps1 | 41 ++++++++++++------- .../Public/New-IdleAuthSessionBroker.ps1 | 16 ++++---- tests/Core/New-IdleAuthSession.Tests.ps1 | 40 +++++++++++++++--- 3 files changed, 68 insertions(+), 29 deletions(-) diff --git a/src/IdLE.Core/Private/Assert-IdleAuthSessionMatchesType.ps1 b/src/IdLE.Core/Private/Assert-IdleAuthSessionMatchesType.ps1 index 42984f1d..f9bcaa35 100644 --- a/src/IdLE.Core/Private/Assert-IdleAuthSessionMatchesType.ps1 +++ b/src/IdLE.Core/Private/Assert-IdleAuthSessionMatchesType.ps1 @@ -61,15 +61,14 @@ function Assert-IdleAuthSessionMatchesType { $isValid = $true } elseif ($null -ne $Session) { - $psObj = [System.Management.Automation.PSObject]::AsPSObject($Session) - # Check for AccessToken property - if ($psObj.Properties['AccessToken']) { + $accessTokenProp = Get-Member -InputObject $Session -Name AccessToken -MemberType Properties -ErrorAction SilentlyContinue + if ($null -ne $accessTokenProp) { $isValid = $true } else { # Check for GetAccessToken() method - $getTokenMethod = $psObj | Get-Member -Name GetAccessToken -MemberType Method -ErrorAction SilentlyContinue + $getTokenMethod = Get-Member -InputObject $Session -Name GetAccessToken -MemberType Method -ErrorAction SilentlyContinue if ($null -ne $getTokenMethod) { $isValid = $true } @@ -90,24 +89,36 @@ but received [$actualType]. } 'PSRemoting' { - # Accept PSSession objects or PSCredential for PSRemoting scenarios - $validTypes = @( - [System.Management.Automation.Runspaces.PSSession] - [pscredential] - ) - + # Accept multiple PSRemoting session shapes: + # - [PSSession] PowerShell remoting session + # - [PSCredential] credential for establishing remote connection + # - object with InvokeCommand(CommandName, Parameters) method (DirectorySync provider pattern) $isValid = $false - foreach ($validType in $validTypes) { - if ($Session -is $validType) { + + if ($Session -is [System.Management.Automation.Runspaces.PSSession]) { + $isValid = $true + } + elseif ($Session -is [pscredential]) { + $isValid = $true + } + elseif ($null -ne $Session) { + # Check for InvokeCommand method (remote execution handle pattern) + $psObj = [System.Management.Automation.PSObject]::AsPSObject($Session) + $invokeMethod = $psObj.Methods['InvokeCommand'] + if ($null -ne $invokeMethod) { $isValid = $true - break } } if (-not $isValid) { $actualType = $Session.GetType().FullName - $expectedTypes = ($validTypes | ForEach-Object { "[$($_.FullName)]" }) -join ' or ' - throw "Auth session validation failed for '$SessionName': Expected AuthSessionType='PSRemoting' requires $expectedTypes, but received [$actualType]." + throw @" +Auth session validation failed for '$SessionName': Expected AuthSessionType='PSRemoting' requires one of: +- [System.Management.Automation.Runspaces.PSSession] +- [PSCredential] +- object with InvokeCommand(CommandName, Parameters) method +but received [$actualType]. +"@ } } } diff --git a/src/IdLE.Core/Public/New-IdleAuthSessionBroker.ps1 b/src/IdLE.Core/Public/New-IdleAuthSessionBroker.ps1 index 8045f8c0..b5f63cab 100644 --- a/src/IdLE.Core/Public/New-IdleAuthSessionBroker.ps1 +++ b/src/IdLE.Core/Public/New-IdleAuthSessionBroker.ps1 @@ -195,11 +195,15 @@ function New-IdleAuthSessionBroker { $normalizedDefaultAuthSession = & $normalizeSessionValue $DefaultAuthSession $AuthSessionType 'DefaultAuthSession' } + # Cache the validation function for performance (avoid repeated Get-Command calls per AcquireAuthSession invocation) + $validationScriptBlock = (Get-Command -Name 'Assert-IdleAuthSessionMatchesType' -ErrorAction Stop).ScriptBlock + $broker = [pscustomobject]@{ PSTypeName = 'IdLE.AuthSessionBroker' SessionMap = $normalizedSessionMap DefaultAuthSession = $normalizedDefaultAuthSession AuthSessionType = $AuthSessionType + ValidateAuthSession = $validationScriptBlock } $broker | Add-Member -MemberType ScriptMethod -Name AcquireAuthSession -Value { @@ -219,8 +223,7 @@ function New-IdleAuthSessionBroker { $normalized = $this.DefaultAuthSession # Validate type before returning - $validationScript = (Get-Command -Name 'Assert-IdleAuthSessionMatchesType' -ErrorAction Stop).ScriptBlock - & $validationScript -AuthSessionType $normalized.AuthSessionType -Session $normalized.Credential -SessionName '' + & $this.ValidateAuthSession -AuthSessionType $normalized.AuthSessionType -Session $normalized.Credential -SessionName '' return $normalized.Credential } @@ -233,8 +236,7 @@ function New-IdleAuthSessionBroker { $normalized = $this.DefaultAuthSession # Validate type before returning - $validationScript = (Get-Command -Name 'Assert-IdleAuthSessionMatchesType' -ErrorAction Stop).ScriptBlock - & $validationScript -AuthSessionType $normalized.AuthSessionType -Session $normalized.Credential -SessionName $Name + & $this.ValidateAuthSession -AuthSessionType $normalized.AuthSessionType -Session $normalized.Credential -SessionName $Name return $normalized.Credential } @@ -321,8 +323,7 @@ function New-IdleAuthSessionBroker { $normalized = $matchingEntries[0].Value # Validate type before returning - $validationScript = (Get-Command -Name 'Assert-IdleAuthSessionMatchesType' -ErrorAction Stop).ScriptBlock - & $validationScript -AuthSessionType $normalized.AuthSessionType -Session $normalized.Credential -SessionName $Name + & $this.ValidateAuthSession -AuthSessionType $normalized.AuthSessionType -Session $normalized.Credential -SessionName $Name return $normalized.Credential } @@ -342,8 +343,7 @@ function New-IdleAuthSessionBroker { $normalized = $this.DefaultAuthSession # Validate type before returning - $validationScript = (Get-Command -Name 'Assert-IdleAuthSessionMatchesType' -ErrorAction Stop).ScriptBlock - & $validationScript -AuthSessionType $normalized.AuthSessionType -Session $normalized.Credential -SessionName $Name + & $this.ValidateAuthSession -AuthSessionType $normalized.AuthSessionType -Session $normalized.Credential -SessionName $Name return $normalized.Credential } diff --git a/tests/Core/New-IdleAuthSession.Tests.ps1 b/tests/Core/New-IdleAuthSession.Tests.ps1 index ba98a0b0..7a2d0643 100644 --- a/tests/Core/New-IdleAuthSession.Tests.ps1 +++ b/tests/Core/New-IdleAuthSession.Tests.ps1 @@ -147,13 +147,13 @@ Describe 'New-IdleAuthSession' { $session | Should -BeOfType [string] } - It 'throws when OAuth type receives non-string object' { + It 'throws when OAuth type receives invalid object type' { { $broker = New-IdleAuthSession -SessionMap @{ - @{ AuthSessionName = 'EXO' } = @{ AuthSessionType = 'OAuth'; Credential = $testCred } + @{ AuthSessionName = 'EXO' } = @{ AuthSessionType = 'OAuth'; Credential = [datetime]::Now } } $broker.AcquireAuthSession('EXO', $null) - } | Should -Throw '*Expected AuthSessionType=''OAuth'' requires a*string*' + } | Should -Throw '*Expected AuthSessionType=''OAuth''*' } } @@ -164,6 +164,9 @@ Describe 'New-IdleAuthSession' { $password2 = ConvertTo-SecureString 'Password2!' -AsPlainText -Force $cred2 = New-Object System.Management.Automation.PSCredential('EXOAdm', $password2) + + $password3 = ConvertTo-SecureString 'Password3!' -AsPlainText -Force + $cred3 = New-Object System.Management.Automation.PSCredential('ADRead', $password3) } It 'matches AuthSessionName without options' { @@ -177,9 +180,6 @@ Describe 'New-IdleAuthSession' { } It 'matches AuthSessionName with matching options' { - $password3 = ConvertTo-SecureString 'Password3!' -AsPlainText -Force - $cred3 = New-Object System.Management.Automation.PSCredential('ADRead', $password3) - $broker = New-IdleAuthSession -SessionMap @{ @{ AuthSessionName = 'AD'; Role = 'ADAdm' } = $cred1 @{ AuthSessionName = 'AD'; Role = 'ADRead' } = $cred3 @@ -189,6 +189,34 @@ Describe 'New-IdleAuthSession' { $session.UserName | Should -Be 'ADRead' } + It 'falls back to default when AuthSessionName does not match' { + $broker = New-IdleAuthSession -SessionMap @{ + @{ AuthSessionName = 'AD' } = $cred1 + } -DefaultAuthSession $testCred -AuthSessionType 'Credential' + + $session = $broker.AcquireAuthSession('EXO', $null) + $session.UserName | Should -Be 'TestUser' + } + + It 'throws when AuthSessionName matches multiple entries (ambiguous)' { + $broker = New-IdleAuthSession -SessionMap @{ + @{ AuthSessionName = 'AD' } = $cred1 + @{ AuthSessionName = 'AD' } = $cred3 + } -AuthSessionType 'Credential' + + { $broker.AcquireAuthSession('AD', $null) } | Should -Throw '*Ambiguous*' + } + + It 'prefers AuthSessionName match over Options-only match' { + $broker = New-IdleAuthSession -SessionMap @{ + @{ Role = 'Admin' } = $testCred + @{ AuthSessionName = 'AD'; Role = 'Admin' } = $cred1 + } -AuthSessionType 'Credential' + + $session = $broker.AcquireAuthSession('AD', @{ Role = 'Admin' }) + $session.UserName | Should -Be 'ADAdm' + } + It 'supports Options-only routing when AuthSessionName is not in pattern' { $broker = New-IdleAuthSession -SessionMap @{ @{ Role = 'Tier0' } = $cred1 From 8e39965f10bac34937f15258c1533f4c878e34ed Mon Sep 17 00:00:00 2001 From: Matthias Fleschuetz <13959569+blindzero@users.noreply.github.com> Date: Mon, 9 Feb 2026 22:15:58 +0100 Subject: [PATCH 12/13] updated steps and cmdlet reference --- docs/reference/cmdlets/New-IdleAuthSession.md | 23 +++++++++++++------ 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/docs/reference/cmdlets/New-IdleAuthSession.md b/docs/reference/cmdlets/New-IdleAuthSession.md index f331e309..c8a04cbe 100644 --- a/docs/reference/cmdlets/New-IdleAuthSession.md +++ b/docs/reference/cmdlets/New-IdleAuthSession.md @@ -13,7 +13,7 @@ Creates a simple AuthSessionBroker for use with IdLE providers. ## SYNTAX ``` -New-IdleAuthSession [[-SessionMap] <Hashtable>] [[-DefaultAuthSession] <Object>] [-AuthSessionType] <String> +New-IdleAuthSession [[-SessionMap] <Hashtable>] [[-DefaultAuthSession] <Object>] [[-AuthSessionType] <String>] [-ProgressAction <ActionPreference>] [<CommonParameters>] ``` @@ -28,9 +28,17 @@ This is a thin wrapper that delegates to IdLE.Core\New-IdleAuthSessionBroker. ### EXAMPLE 1 ``` +# Simple broker with single credential +$broker = New-IdleAuthSession -DefaultAuthSession $credential -AuthSessionType 'Credential' +``` + +### EXAMPLE 2 +``` +# Mixed-type broker for AD + EXO $broker = New-IdleAuthSession -SessionMap @{ - @{ Role = 'Tier0' } = $tier0Credential -} -AuthSessionType 'Credential' + @{ AuthSessionName = 'AD' } = @{ AuthSessionType = 'Credential'; Credential = $adCred } + @{ AuthSessionName = 'EXO' } = @{ AuthSessionType = 'OAuth'; Credential = $token } +} ``` ## PARAMETERS @@ -66,9 +74,10 @@ Accept wildcard characters: False ``` ### -AuthSessionType -Specifies the type of authentication session. -This determines validation rules, -lifecycle management, and telemetry behavior. +Optional default authentication session type. +When provided, allows simple (untyped) +session values. +When not provided, values must be typed descriptors. Valid values: - 'OAuth': Token-based authentication (e.g., Microsoft Graph, Exchange Online) @@ -80,7 +89,7 @@ Type: String Parameter Sets: (All) Aliases: -Required: True +Required: False Position: 3 Default value: None Accept pipeline input: False From 16f57ba3e911658c596541ca7ffa45f4f66a22a3 Mon Sep 17 00:00:00 2001 From: Matthias <13959569+blindzero@users.noreply.github.com> Date: Mon, 9 Feb 2026 22:32:08 +0100 Subject: [PATCH 13/13] Update src/IdLE.Core/Public/New-IdleAuthSessionBroker.ps1 Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/IdLE.Core/Public/New-IdleAuthSessionBroker.ps1 | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/IdLE.Core/Public/New-IdleAuthSessionBroker.ps1 b/src/IdLE.Core/Public/New-IdleAuthSessionBroker.ps1 index b5f63cab..5679405a 100644 --- a/src/IdLE.Core/Public/New-IdleAuthSessionBroker.ps1 +++ b/src/IdLE.Core/Public/New-IdleAuthSessionBroker.ps1 @@ -196,7 +196,8 @@ function New-IdleAuthSessionBroker { } # Cache the validation function for performance (avoid repeated Get-Command calls per AcquireAuthSession invocation) - $validationScriptBlock = (Get-Command -Name 'Assert-IdleAuthSessionMatchesType' -ErrorAction Stop).ScriptBlock + $validationCommand = Get-Command -Name 'Assert-IdleAuthSessionMatchesType' -CommandType Function -Module $MyInvocation.MyCommand.Module -ErrorAction Stop + $validationScriptBlock = $validationCommand.ScriptBlock $broker = [pscustomobject]@{ PSTypeName = 'IdLE.AuthSessionBroker'