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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/reference/cmdlets/New-IdleAuthSession.md
Original file line number Diff line number Diff line change
Expand Up @@ -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>]
```

Expand Down Expand Up @@ -43,7 +43,7 @@ Type: Hashtable
Parameter Sets: (All)
Aliases:

Required: True
Required: False
Position: 1
Default value: None
Accept pipeline input: False
Expand Down
52 changes: 44 additions & 8 deletions src/IdLE.Core/Public/Invoke-IdleProviderMethod.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ function Invoke-IdleProviderMethod {
provider methods with proper authentication handling.

Key features:
- Acquires auth sessions via Context.AcquireAuthSession when With.AuthSessionName is present
- Acquires auth sessions via Context.AcquireAuthSession when AuthSessionBroker is available
- If With.AuthSessionName is present, uses it for session routing
- If With.AuthSessionName is absent but broker exists, attempts to acquire default session
- Detects whether provider methods support AuthSession parameter (backwards compatible)
- Passes AuthSession to provider methods that support it
- Validates provider existence and method implementation
Expand Down Expand Up @@ -70,17 +72,15 @@ function Invoke-IdleProviderMethod {

# Auth session acquisition (optional, data-only)
$authSession = $null
if ($With.ContainsKey('AuthSessionName')) {
$sessionName = [string]$With.AuthSessionName
$sessionOptions = if ($With.ContainsKey('AuthSessionOptions')) { $With.AuthSessionOptions } else { $null }


# Validate AuthSessionOptions early (regardless of broker availability)
if ($With.ContainsKey('AuthSessionOptions')) {
$sessionOptions = $With.AuthSessionOptions
if ($null -ne $sessionOptions -and -not ($sessionOptions -is [hashtable])) {
throw "With.AuthSessionOptions must be a hashtable or null."
}

$authSession = $Context.AcquireAuthSession($sessionName, $sessionOptions)
}

$provider = $Context.Providers[$ProviderAlias]

# Check if provider method exists
Expand All @@ -91,6 +91,42 @@ function Invoke-IdleProviderMethod {

# Check if method supports AuthSession parameter
$supportsAuthSession = Test-IdleProviderMethodParameter -ProviderMethod $providerMethod -ParameterName 'AuthSession'

# Check if context can acquire auth sessions
$canAcquireAuth = $Context.PSObject.Methods.Name -contains 'AcquireAuthSession'

# Only acquire auth session if provider method can use it or if explicitly requested
$shouldAcquireAuth = $With.ContainsKey('AuthSessionName') -or ($supportsAuthSession -and $canAcquireAuth)

if ($shouldAcquireAuth) {
if (-not $canAcquireAuth) {
# AuthSessionName was explicitly set but context cannot acquire sessions
throw "With.AuthSessionName is set to '$($With.AuthSessionName)' but Context does not have an AcquireAuthSession method."
}

# If AuthSessionName is provided, use it for routing
if ($With.ContainsKey('AuthSessionName')) {
Comment thread
blindzero marked this conversation as resolved.
$sessionName = [string]$With.AuthSessionName
$sessionOptions = if ($With.ContainsKey('AuthSessionOptions')) { $With.AuthSessionOptions } else { $null }

$authSession = $Context.AcquireAuthSession($sessionName, $sessionOptions)
}
else {
# No AuthSessionName provided but provider supports auth - try to acquire default session
# Use empty string to signal default request without conflicting with user session names
try {
$authSession = $Context.AcquireAuthSession('', $null)
}
catch {
# If provider method supports AuthSession and default acquisition fails, rethrow
if ($supportsAuthSession) {
throw
}
# Otherwise, continue without auth session for backward compatibility
$authSession = $null
}
}
Comment thread
blindzero marked this conversation as resolved.
}

# Call provider method with appropriate signature
if ($supportsAuthSession -and $null -ne $authSession) {
Expand Down
177 changes: 141 additions & 36 deletions src/IdLE.Core/Public/New-IdleAuthSessionBroker.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -13,22 +13,28 @@ function New-IdleAuthSessionBroker {
AcquireAuthSession method.

.PARAMETER SessionMap
Comment thread
blindzero marked this conversation as resolved.
A hashtable that maps session configurations to auth sessions. Each key is a hashtable
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.

Common patterns:
- @{ Role = 'Tier0' } -> $tier0Credential (for Credential type)
- @{ Role = 'Admin' } -> $adminToken (for OAuth type)
- @{ Server = 'Server01' } -> $remoteSession (for PSRemoting type)
- @{ Environment = 'Production' } -> $prodCred
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)

SessionMap is optional if DefaultAuthSession is provided.

.PARAMETER DefaultAuthSession
Optional default auth session to return when no session options are provided or
Comment thread
blindzero marked this conversation as resolved.
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.

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.
Expand All @@ -39,17 +45,21 @@ function New-IdleAuthSessionBroker {
- 'Credential': Credential-based authentication (e.g., Active Directory, mock providers)

.EXAMPLE
# Simple role-based broker with Credential session type
$broker = New-IdleAuthSessionBroker -SessionMap @{
@{ Role = 'Tier0' } = $tier0Credential
@{ Role = 'Admin' } = $adminCredential
} -DefaultAuthSession $adminCredential -AuthSessionType 'Credential'
# 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
}

.EXAMPLE
# AuthSessionName-based routing with roles
$broker = New-IdleAuthSessionBroker -SessionMap @{
@{ AuthSessionName = 'AD'; Role = 'ADAdm' } = $tier0Credential
@{ AuthSessionName = 'AD'; Role = 'ADRead' } = $readOnlyCredential
} -DefaultAuthSession $adminCredential -AuthSessionType 'Credential'

Comment thread
blindzero marked this conversation as resolved.
.EXAMPLE
# OAuth broker with token strings
$broker = New-IdleAuthSessionBroker -SessionMap @{
Expand All @@ -69,13 +79,21 @@ function New-IdleAuthSessionBroker {
@{ Server = 'AADConnect01' } = $remoteSessionCred
} -AuthSessionType 'PSRemoting'

.EXAMPLE
# Environment-based routing for multi-environment scenarios
$broker = New-IdleAuthSessionBroker -SessionMap @{
@{ Environment = 'Production' } = $prodCred
@{ Environment = 'Test' } = $testCred
} -DefaultAuthSession $devCred -AuthSessionType 'Credential'

.OUTPUTS
PSCustomObject with AcquireAuthSession method
#>
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[ValidateNotNull()]
[Parameter()]
[AllowNull()]
[AllowEmptyCollection()]
[hashtable] $SessionMap,

[Parameter()]
Expand All @@ -87,6 +105,11 @@ function New-IdleAuthSessionBroker {
[string] $AuthSessionType
)

# Validate: If SessionMap is empty/null, DefaultAuthSession must be provided
if (($null -eq $SessionMap -or $SessionMap.Count -eq 0) -and $null -eq $DefaultAuthSession) {
throw "SessionMap is empty or null. DefaultAuthSession must be provided when SessionMap is not used."
}

$broker = [pscustomobject]@{
PSTypeName = 'IdLE.AuthSessionBroker'
SessionMap = $SessionMap
Expand All @@ -97,59 +120,141 @@ function New-IdleAuthSessionBroker {
$broker | Add-Member -MemberType ScriptMethod -Name AcquireAuthSession -Value {
param(
[Parameter(Mandatory)]
[ValidateNotNullOrEmpty()]
[AllowEmptyString()]
[string] $Name,

[Parameter()]
[AllowNull()]
[hashtable] $Options
)

# $Name is part of the broker contract but not used in this simple implementation
# This broker routes based on Options only; custom brokers may use Name for additional routing
$null = $Name

# 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

# If no options provided, return default
if ($null -eq $Options -or $Options.Count -eq 0) {
# Empty string signals default session request
if ([string]::IsNullOrEmpty($Name)) {
if ($null -ne $this.DefaultAuthSession) {
return $this.DefaultAuthSession
}
throw "No default auth session configured."
}

# 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
}
throw "No auth session options provided and no default auth session configured."
throw "No SessionMap configured and no default auth session available."
}

# Find matching session in map
# Matching logic:
# 1. If Name provided: try to match entries with AuthSessionName key
# 2. If Options provided: match all key/value pairs
# 3. Fall back to DefaultAuthSession
# 4. Fail with clear error

$authSessionNameMatches = @()
$legacyMatches = @()

foreach ($entry in $this.SessionMap.GetEnumerator()) {
$pattern = $entry.Key
$credential = $entry.Value

# Check if all keys in pattern match Options
$matches = $true
foreach ($key in $pattern.Keys) {
if (-not $Options.ContainsKey($key) -or $Options[$key] -ne $pattern[$key]) {
$matches = $false
break

# Check if pattern includes AuthSessionName
if ($pattern.ContainsKey('AuthSessionName')) {
# AuthSessionName must match
if ($pattern.AuthSessionName -ne $Name) {
continue
}

# If pattern has ONLY AuthSessionName (no other keys)
if ($pattern.Keys.Count -eq 1) {
# Only match if Options is null or empty
if ($null -eq $Options -or $Options.Count -eq 0) {
$authSessionNameMatches += $entry
}
continue
}

# Pattern has additional keys beyond AuthSessionName
# All other keys in pattern must match Options (if Options provided)
$matches = $true
foreach ($key in $pattern.Keys) {
if ($key -eq 'AuthSessionName') {
continue # Already checked
}

# If Options is null or doesn't contain the key, no match
if ($null -eq $Options -or -not $Options.ContainsKey($key) -or $Options[$key] -ne $pattern[$key]) {
$matches = $false
break
}
}

if ($matches) {
$authSessionNameMatches += $entry
}
}

if ($matches) {
return $credential
else {
# Legacy: pattern without AuthSessionName - match based on Options only
if ($null -eq $Options -or $Options.Count -eq 0) {
continue # No options to match
}

$matches = $true
foreach ($key in $pattern.Keys) {
if (-not $Options.ContainsKey($key) -or $Options[$key] -ne $pattern[$key]) {
$matches = $false
break
}
}

if ($matches) {
$legacyMatches += $entry
}
}
}

# No match found
# Prioritize AuthSessionName-based matches over legacy matches
$matchingEntries = @()
if (@($authSessionNameMatches).Count -gt 0) {
$matchingEntries = @($authSessionNameMatches)
} else {
$matchingEntries = @($legacyMatches)
}

# Return first match if exactly one found
if ($matchingEntries.Count -eq 1) {
return $matchingEntries[0].Value
}

# If multiple matches, this is ambiguous - fail with clear error
if ($matchingEntries.Count -gt 1) {
$matchDetails = ($matchingEntries | ForEach-Object {
$currentEntry = $_
$keyStr = ($currentEntry.Key.Keys | ForEach-Object { "$_=$($currentEntry.Key[$_])" }) -join ', '
"{ $keyStr }"
}) -join '; '
throw "Ambiguous auth session match for Name='$Name'. Multiple entries matched: $matchDetails. Provide AuthSessionOptions to disambiguate."
}

# No match found - fall back to default
if ($null -ne $this.DefaultAuthSession) {
return $this.DefaultAuthSession
}

$optionsStr = ($Options.Keys | ForEach-Object { "$_=$($Options[$_])" }) -join ', '
throw "No matching auth session found for options: $optionsStr"
# No match and no default
$nameStr = "Name='$Name'"
$optionsPart = if ($null -ne $Options -and $Options.Count -gt 0) {
$optsStr = ($Options.Keys | ForEach-Object { "$_=$($Options[$_])" }) -join ', '
", Options={ $optsStr }"
} else {
""
}
throw "No matching auth session found for $nameStr$optionsPart and no default auth session configured."
} -Force

return $broker
Expand Down
9 changes: 6 additions & 3 deletions src/IdLE/Public/New-IdleAuthSession.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,9 @@ function New-IdleAuthSession {
#>
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[ValidateNotNull()]
[Parameter()]
[AllowNull()]
[AllowEmptyCollection()]
[hashtable] $SessionMap,

[Parameter()]
Expand All @@ -61,9 +62,11 @@ function New-IdleAuthSession {

# Delegate to IdLE.Core implementation.
$params = @{
SessionMap = $SessionMap
AuthSessionType = $AuthSessionType
}
if ($PSBoundParameters.ContainsKey('SessionMap')) {
$params['SessionMap'] = $SessionMap
}
if ($PSBoundParameters.ContainsKey('DefaultAuthSession')) {
$params['DefaultAuthSession'] = $DefaultAuthSession
}
Expand Down
Loading
Loading