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 diff --git a/docs/reference/providers/provider-entraID.md b/docs/reference/providers/provider-entraID.md index bc633a6b..1f89ac30 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,21 +110,57 @@ $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 ``` +### 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 $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 diff --git a/docs/use/providers.md b/docs/use/providers.md index f9e446c0..0479f557 100644 --- a/docs/use/providers.md +++ b/docs/use/providers.md @@ -153,7 +153,7 @@ 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: +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) @@ -161,29 +161,61 @@ Each `AuthSessionBroker` must specify an `AuthSessionType` that determines valid 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 provider +$provider = New-IdleADIdentityProvider + +# Create broker with single credential +$broker = New-IdleAuthSession -DefaultAuthSession $credential -AuthSessionType 'Credential' + +# Use in plan +$plan = New-IdlePlan -WorkflowPath './workflow.psd1' -Request $request -Providers @{ + Identity = $provider + AuthSessionBroker = $broker +} +``` + +### 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 +# Use in plan $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`: + +```powershell +With = @{ + ... + AuthSessionOptions = @{ Role = 'Tier0' } +} +``` + ### Example: Entra ID with OAuth ```powershell @@ -191,14 +223,12 @@ $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 $token -AuthSessionType 'OAuth' + # Use in plan $plan = New-IdlePlan -WorkflowPath './workflow.psd1' -Request $request -Providers @{ Identity = $provider @@ -206,12 +236,58 @@ $plan = New-IdlePlan -WorkflowPath './workflow.psd1' -Request $request -Provider } ``` -The different authentication sessions are used by the workflow definition by the steps via `AuthSessionOptions`. +### Example: Mixed Authentication Types (AD + EXO) + +For workflows that need multiple providers with different authentication types: + ```powershell -With = @{ - ... - AuthSessionName = 'ActiveDirectory' - AuthSessionOptions = @{ Role = 'Tier0' } +# Obtain credentials and tokens +$adCredential = Get-Credential -Message "Enter AD admin credentials" +Connect-AzAccount +$exoToken = (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'; Credential = $adCredential } + + # Exchange Online uses OAuth type + @{ AuthSessionName = 'EXO' } = @{ AuthSessionType = 'OAuth'; Credential = $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 via `AuthSessionName`: + +```powershell +# Step using AD (Credential) +@{ + Name = 'Create AD User' + Type = 'IdLE.Step.CreateIdentity' + With = @{ + AuthSessionName = 'AD' + # ... + } +} + +# Step using EXO (OAuth) +@{ + Name = 'Create Mailbox' + Type = 'IdLE.Step.CreateMailbox' + With = @{ + AuthSessionName = 'EXO' + # ... + } } ``` diff --git a/src/IdLE.Core/Private/Assert-IdleAuthSessionMatchesType.ps1 b/src/IdLE.Core/Private/Assert-IdleAuthSessionMatchesType.ps1 new file mode 100644 index 00000000..f9bcaa35 --- /dev/null +++ b/src/IdLE.Core/Private/Assert-IdleAuthSessionMatchesType.ps1 @@ -0,0 +1,125 @@ +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 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) { + # Check for AccessToken property + $accessTokenProp = Get-Member -InputObject $Session -Name AccessToken -MemberType Properties -ErrorAction SilentlyContinue + if ($null -ne $accessTokenProp) { + $isValid = $true + } + else { + # Check for GetAccessToken() method + $getTokenMethod = Get-Member -InputObject $Session -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 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]. +"@ + } + } + + 'PSRemoting' { + # 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 + + 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 + } + } + + if (-not $isValid) { + $actualType = $Session.GetType().FullName + 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 09410fdf..5679405a 100644 --- a/src/IdLE.Core/Public/New-IdleAuthSessionBroker.ps1 +++ b/src/IdLE.Core/Public/New-IdleAuthSessionBroker.ps1 @@ -14,30 +14,32 @@ 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. + 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' } -> $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' } -> $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. .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 a direct credential/token (when -AuthSessionType is provided) or a typed descriptor. At least one of SessionMap or DefaultAuthSession must be provided. .PARAMETER 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 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) @@ -45,47 +47,49 @@ function New-IdleAuthSessionBroker { - 'Credential': Credential-based authentication (e.g., Active Directory, mock providers) .EXAMPLE - # Simple single-credential broker (no SessionMap required) + # Simple single-credential broker (with AuthSessionType) $broker = New-IdleAuthSessionBroker -DefaultAuthSession $admCred -AuthSessionType 'Credential' - $plan = New-IdlePlan -WorkflowPath './workflow.psd1' -Request $request -Providers @{ - Identity = New-IdleADIdentityProvider - AuthSessionBroker = $broker - } - .EXAMPLE - # AuthSessionName-based routing with roles + # AuthSessionName-based routing with roles (with AuthSessionType) $broker = New-IdleAuthSessionBroker -SessionMap @{ @{ 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' } = $graphToken } -DefaultAuthSession $graphToken -AuthSessionType 'OAuth' .EXAMPLE - # Domain-based broker for multi-forest scenarios with Credential session type + # Domain-based broker for multi-forest scenarios (with AuthSessionType) $broker = New-IdleAuthSessionBroker -SessionMap @{ @{ 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' } = $remoteSessionCred } -AuthSessionType 'PSRemoting' .EXAMPLE - # Environment-based routing for multi-environment scenarios + # Environment-based routing (with AuthSessionType) $broker = New-IdleAuthSessionBroker -SessionMap @{ @{ Environment = 'Production' } = $prodCred @{ Environment = 'Test' } = $testCred } -DefaultAuthSession $devCred -AuthSessionType 'Credential' + .EXAMPLE + # Mixed-type broker for AD (Credential) + EXO (OAuth) - typed descriptors + $broker = New-IdleAuthSessionBroker -SessionMap @{ + @{ AuthSessionName = 'AD' } = @{ AuthSessionType = 'Credential'; Credential = $adCred } + @{ AuthSessionName = 'EXO' } = @{ AuthSessionType = 'OAuth'; Credential = $exoToken } + } + .OUTPUTS PSCustomObject with AcquireAuthSession method #> @@ -100,7 +104,7 @@ function New-IdleAuthSessionBroker { [AllowNull()] [object] $DefaultAuthSession, - [Parameter(Mandatory)] + [Parameter()] [ValidateSet('OAuth', 'PSRemoting', 'Credential')] [string] $AuthSessionType ) @@ -110,11 +114,97 @@ 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 + } + + # Only support hashtable format (not PSCustomObject) + if ($value -is [hashtable]) { + return ($value.ContainsKey('AuthSessionType') -and $value.ContainsKey('Credential')) + } + + 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 typed + if (& $isTypedSession $value) { + $sessionType = $value.AuthSessionType + $credential = $value.Credential + + # 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 + } + } + + # 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 = $defaultType + Credential = $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 + + # 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 }" + + $normalizedValue = & $normalizeSessionValue $value $AuthSessionType $context + $normalizedSessionMap[$pattern] = $normalizedValue + } + } + + # Normalize DefaultAuthSession + $normalizedDefaultAuthSession = $null + if ($null -ne $DefaultAuthSession) { + $normalizedDefaultAuthSession = & $normalizeSessionValue $DefaultAuthSession $AuthSessionType 'DefaultAuthSession' + } + + # Cache the validation function for performance (avoid repeated Get-Command calls per AcquireAuthSession invocation) + $validationCommand = Get-Command -Name 'Assert-IdleAuthSessionMatchesType' -CommandType Function -Module $MyInvocation.MyCommand.Module -ErrorAction Stop + $validationScriptBlock = $validationCommand.ScriptBlock + $broker = [pscustomobject]@{ PSTypeName = 'IdLE.AuthSessionBroker' - SessionMap = $SessionMap - DefaultAuthSession = $DefaultAuthSession + SessionMap = $normalizedSessionMap + DefaultAuthSession = $normalizedDefaultAuthSession AuthSessionType = $AuthSessionType + ValidateAuthSession = $validationScriptBlock } $broker | Add-Member -MemberType ScriptMethod -Name AcquireAuthSession -Value { @@ -128,17 +218,15 @@ 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 + & $this.ValidateAuthSession -AuthSessionType $normalized.AuthSessionType -Session $normalized.Credential -SessionName '' + + return $normalized.Credential } throw "No default auth session configured." } @@ -146,7 +234,12 @@ 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 + & $this.ValidateAuthSession -AuthSessionType $normalized.AuthSessionType -Session $normalized.Credential -SessionName $Name + + return $normalized.Credential } throw "No SessionMap configured and no default auth session available." } @@ -228,7 +321,12 @@ 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 + & $this.ValidateAuthSession -AuthSessionType $normalized.AuthSessionType -Session $normalized.Credential -SessionName $Name + + return $normalized.Credential } # If multiple matches, this is ambiguous - fail with clear error @@ -243,7 +341,12 @@ 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 + & $this.ValidateAuthSession -AuthSessionType $normalized.AuthSessionType -Session $normalized.Credential -SessionName $Name + + 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 5f5b3fd8..f708d9e5 100644 --- a/src/IdLE/Public/New-IdleAuthSession.ps1 +++ b/src/IdLE/Public/New-IdleAuthSession.ps1 @@ -25,8 +25,8 @@ 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. 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) @@ -34,9 +34,15 @@ function New-IdleAuthSession { - 'Credential': Credential-based authentication (e.g., Active Directory, mock providers) .EXAMPLE + # Simple broker with single credential + $broker = New-IdleAuthSession -DefaultAuthSession $credential -AuthSessionType 'Credential' + + .EXAMPLE + # 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 } + } .OUTPUTS PSCustomObject with AcquireAuthSession method @@ -55,21 +61,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..7a2d0643 100644 --- a/tests/Core/New-IdleAuthSession.Tests.ps1 +++ b/tests/Core/New-IdleAuthSession.Tests.ps1 @@ -5,205 +5,155 @@ 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' - - $broker | Should -Not -BeNullOrEmpty - $broker.PSTypeNames | Should -Contain 'IdLE.AuthSessionBroker' - } - - It 'creates broker with AcquireAuthSession method' { - $broker = New-IdleAuthSession -SessionMap @{ - @{ Role = 'AD' } = $testCred - } -AuthSessionType 'Credential' - - $broker.PSObject.Methods['AcquireAuthSession'] | Should -Not -BeNullOrEmpty - } - - It 'accepts SessionMap parameter' { - $sessionMap = @{ - @{ Role = 'Tier0' } = $testCred - @{ Role = 'Admin' } = $testCred - } - - $broker = New-IdleAuthSession -SessionMap $sessionMap -AuthSessionType 'Credential' - - $broker.SessionMap | Should -Not -BeNullOrEmpty - $broker.SessionMap.Count | Should -Be 2 - } - - It 'accepts optional DefaultAuthSession parameter' { - $broker = New-IdleAuthSession -SessionMap @{ - @{ Role = 'AD' } = $testCred - } -DefaultAuthSession $testCred -AuthSessionType 'Credential' - - $broker.DefaultAuthSession | Should -Not -BeNullOrEmpty - $broker.DefaultAuthSession.UserName | Should -Be 'TestUser' - } - - 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' - } - - It 'broker returns default auth session when no options provided' { - $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' - - $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' } = $testCred - } -AuthSessionType 'Credential' - - { $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 - $command.Name | Should -Be 'New-IdleAuthSession' - $command.Module.Name | Should -Be 'IdLE' - } - - 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 + Context 'Simple syntax with AuthSessionType' { + It 'creates broker with single credential' { + $broker = New-IdleAuthSession -DefaultAuthSession $testCred -AuthSessionType 'Credential' $broker | Should -Not -BeNullOrEmpty - } | Should -Not -Throw - } + $broker.PSTypeNames | Should -Contain 'IdLE.AuthSessionBroker' + } - Context 'AuthSessionType parameter' { - It 'accepts OAuth session type' { + It 'accepts SessionMap with untyped values when AuthSessionType provided' { $broker = New-IdleAuthSession -SessionMap @{ + @{ Role = 'Tier0' } = $testCred @{ Role = 'Admin' } = $testCred - } -AuthSessionType 'OAuth' + } -AuthSessionType 'Credential' - $broker.AuthSessionType | Should -Be 'OAuth' + $broker.SessionMap | Should -Not -BeNullOrEmpty + $broker.SessionMap.Count | Should -Be 2 } - It 'accepts PSRemoting session type' { + It 'broker can acquire auth session with matching options' { $broker = New-IdleAuthSession -SessionMap @{ - @{ Server = 'AADConnect01' } = $testCred - } -AuthSessionType 'PSRemoting' + @{ Role = 'Tier0' } = $testCred + } -AuthSessionType 'Credential' - $broker.AuthSessionType | Should -Be 'PSRemoting' + $acquiredSession = $broker.AcquireAuthSession('TestName', @{ Role = 'Tier0' }) + + $acquiredSession | Should -Not -BeNullOrEmpty + $acquiredSession | Should -BeOfType [PSCredential] + $acquiredSession.UserName | Should -Be 'TestUser' } - It 'accepts Credential session type' { + 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 @{ - @{ Domain = 'corp.example.com' } = $testCred - } -AuthSessionType 'Credential' + @{ Role = 'Tier0' } = $testCred + } -DefaultAuthSession $defaultCred -AuthSessionType 'Credential' + + $acquiredSession = $broker.AcquireAuthSession('TestName', $null) - $broker.AuthSessionType | Should -Be 'Credential' + $acquiredSession | Should -Not -BeNullOrEmpty + $acquiredSession.UserName | Should -Be 'DefaultUser' } - It 'throws on invalid session type' { - { - New-IdleAuthSession -SessionMap @{ - @{ Role = 'AD' } = $testCred - } -AuthSessionType 'InvalidType' - } | Should -Throw + It 'throws when no matching auth session found and no default provided' { + $broker = New-IdleAuthSession -SessionMap @{ + @{ Role = 'Tier0' } = $testCred + } -AuthSessionType 'Credential' + + { $broker.AcquireAuthSession('TestName', @{ Role = 'NonExistent' }) } | + Should -Throw '*No matching auth session found*' } - } - Context 'AuthSessionType validation during acquisition' { - It 'OAuth broker can acquire sessions with appropriate options' { + It 'accepts OAuth session type' { $broker = New-IdleAuthSession -SessionMap @{ - @{ Role = 'Admin' } = $testCred + @{ Role = 'Admin' } = $testToken } -AuthSessionType 'OAuth' - $session = $broker.AcquireAuthSession('MicrosoftGraph', @{ Role = 'Admin' }) - $session | Should -Not -BeNullOrEmpty + $session = $broker.AcquireAuthSession('Graph', @{ Role = 'Admin' }) + $session | Should -BeOfType [string] + $session | Should -Be 'mock-oauth-token-12345' } - It 'PSRemoting broker can acquire sessions with appropriate options' { + It 'accepts PSRemoting session type' { $broker = New-IdleAuthSession -SessionMap @{ @{ Server = 'AADConnect01' } = $testCred } -AuthSessionType 'PSRemoting' - $session = $broker.AcquireAuthSession('EntraConnect', @{ Server = 'AADConnect01' }) - $session | Should -Not -BeNullOrEmpty + $session = $broker.AcquireAuthSession('Remote', @{ Server = 'AADConnect01' }) + $session | Should -BeOfType [PSCredential] } + } - It 'Credential broker can acquire sessions with appropriate options' { + Context 'Typed syntax for mixed types' { + It 'supports typed SessionMap values with AuthSessionType property' { $broker = New-IdleAuthSession -SessionMap @{ - @{ Domain = 'corp.example.com' } = $testCred - } -AuthSessionType 'Credential' - - $session = $broker.AcquireAuthSession('ActiveDirectory', @{ Domain = 'corp.example.com' }) - $session | Should -Not -BeNullOrEmpty + @{ AuthSessionName = 'AD' } = @{ AuthSessionType = 'Credential'; Credential = $testCred } + } + + $session = $broker.AcquireAuthSession('AD', $null) + $session | Should -BeOfType [PSCredential] } - } - Context 'Optional SessionMap' { - It 'creates broker with only DefaultAuthSession (no SessionMap)' { - $broker = New-IdleAuthSession -DefaultAuthSession $testCred -AuthSessionType 'Credential' - - $broker | Should -Not -BeNullOrEmpty - $broker.DefaultAuthSession | Should -Not -BeNullOrEmpty - $broker.SessionMap | Should -BeNullOrEmpty + It 'supports mixed types in single SessionMap' { + $broker = New-IdleAuthSession -SessionMap @{ + @{ AuthSessionName = 'AD' } = @{ AuthSessionType = 'Credential'; Credential = $testCred } + @{ AuthSessionName = 'EXO' } = @{ AuthSessionType = 'OAuth'; Credential = $testToken } + } + + $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 'returns DefaultAuthSession when SessionMap is null' { + It 'throws when untyped value provided without AuthSessionType' { + { + New-IdleAuthSession -SessionMap @{ + @{ AuthSessionName = 'AD' } = $testCred # Untyped + } + } | Should -Throw '*Untyped session value*' + } + + It 'throws when invalid AuthSessionType provided' { + { + New-IdleAuthSession -SessionMap @{ + @{ AuthSessionName = 'AD' } = @{ AuthSessionType = 'InvalidType'; Credential = $testCred } + } + } | Should -Throw '*Invalid AuthSessionType*' + } + } + + Context 'Type validation' { + It 'validates Credential type matches PSCredential' { $broker = New-IdleAuthSession -DefaultAuthSession $testCred -AuthSessionType 'Credential' - - $session = $broker.AcquireAuthSession('AnyName', $null) - - $session | Should -Not -BeNullOrEmpty - $session.UserName | Should -Be 'TestUser' + $session = $broker.AcquireAuthSession('', $null) + $session | Should -BeOfType [PSCredential] } - It 'returns DefaultAuthSession when SessionMap is empty' { - $broker = New-IdleAuthSession -SessionMap @{} -DefaultAuthSession $testCred -AuthSessionType 'Credential' - - $session = $broker.AcquireAuthSession('AnyName', $null) - - $session | Should -Not -BeNullOrEmpty - $session.UserName | Should -Be 'TestUser' + It 'throws when Credential type receives non-PSCredential object' { + { + $broker = New-IdleAuthSession -SessionMap @{ + @{ AuthSessionName = 'AD' } = @{ AuthSessionType = 'Credential'; Credential = 'not-a-credential' } + } + $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 -AuthSessionType 'Credential' - } | 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 @{} -AuthSessionType 'Credential' - } | Should -Throw '*DefaultAuthSession must be provided*' + It 'throws when OAuth type receives invalid object type' { + { + $broker = New-IdleAuthSession -SessionMap @{ + @{ AuthSessionName = 'EXO' } = @{ AuthSessionType = 'OAuth'; Credential = [datetime]::Now } + } + $broker.AcquireAuthSession('EXO', $null) + } | Should -Throw '*Expected AuthSessionType=''OAuth''*' } } @@ -226,8 +176,6 @@ Describe 'New-IdleAuthSession' { } -AuthSessionType 'Credential' $session = $broker.AcquireAuthSession('AD', $null) - - $session | Should -Not -BeNullOrEmpty $session.UserName | Should -Be 'ADAdm' } @@ -238,8 +186,6 @@ Describe 'New-IdleAuthSession' { } -AuthSessionType 'Credential' $session = $broker.AcquireAuthSession('AD', @{ Role = 'ADRead' }) - - $session | Should -Not -BeNullOrEmpty $session.UserName | Should -Be 'ADRead' } @@ -249,8 +195,6 @@ Describe 'New-IdleAuthSession' { } -DefaultAuthSession $testCred -AuthSessionType 'Credential' $session = $broker.AcquireAuthSession('EXO', $null) - - $session | Should -Not -BeNullOrEmpty $session.UserName | Should -Be 'TestUser' } @@ -260,64 +204,50 @@ Describe 'New-IdleAuthSession' { @{ AuthSessionName = 'AD' } = $cred3 } -AuthSessionType 'Credential' - { $broker.AcquireAuthSession('AD', $null) } | - Should -Throw '*Ambiguous*' + { $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' $session = $broker.AcquireAuthSession('AD', @{ Role = 'Admin' }) - - $session | Should -Not -BeNullOrEmpty $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' $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' } = $cred1 - } -AuthSessionType 'Credential' + Context 'Optional SessionMap' { + It 'creates broker with only DefaultAuthSession' { + $broker = New-IdleAuthSession -DefaultAuthSession $testCred -AuthSessionType 'Credential' - { $broker.AcquireAuthSession('EXO', $null) } | - Should -Throw '*No matching auth session found*' + $broker | Should -Not -BeNullOrEmpty + $broker.DefaultAuthSession | Should -Not -BeNullOrEmpty } - It 'matches complex pattern: AuthSessionName + multiple options' { - $broker = New-IdleAuthSession -SessionMap @{ - @{ AuthSessionName = 'AD'; Role = 'Admin'; Environment = 'Prod' } = $cred1 - } -AuthSessionType 'Credential' - - $session = $broker.AcquireAuthSession('AD', @{ Role = 'Admin'; Environment = 'Prod' }) - - $session | Should -Not -BeNullOrEmpty - $session.UserName | Should -Be 'ADAdm' + It 'throws when SessionMap is null and DefaultAuthSession is not provided' { + { + New-IdleAuthSession -SessionMap $null -AuthSessionType 'Credential' + } | Should -Throw '*DefaultAuthSession must be provided*' } + } - It 'does not match when partial options provided' { - $broker = New-IdleAuthSession -SessionMap @{ - @{ AuthSessionName = 'AD'; Role = 'Admin'; Environment = 'Prod' } = $cred1 - } -DefaultAuthSession $testCred -AuthSessionType 'Credential' - - # 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 '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' } }