Skip to content
4 changes: 3 additions & 1 deletion src/IdLE.Core/Private/Copy-IdleRedactedObject.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@ function Copy-IdleRedactedObject {
'accessToken',
'refreshToken',
'credential',
'privateKey'
'privateKey',
'AccountPassword',
'AccountPasswordAsPlainText'
)

$effectiveKeys = if ($null -ne $RedactedKeys -and $RedactedKeys.Count -gt 0) {
Expand Down
69 changes: 69 additions & 0 deletions src/IdLE.Provider.AD/Private/New-IdleADAdapter.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,14 @@ function New-IdleADAdapter {

.PARAMETER Credential
Optional PSCredential for AD operations. If not provided, uses integrated auth.

.NOTES
PSScriptAnalyzer suppression: This function intentionally uses ConvertTo-SecureString -AsPlainText
as an explicit escape hatch for AccountPasswordAsPlainText. This is a documented design decision
with automatic redaction via Copy-IdleRedactedObject.
#>
[CmdletBinding()]
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingConvertToSecureStringWithPlainText', '', Justification = 'Intentional escape hatch for AccountPasswordAsPlainText with explicit opt-in and automatic redaction')]
param(
[Parameter()]
[AllowNull()]
Expand Down Expand Up @@ -171,6 +177,69 @@ function New-IdleADAdapter {
$params['EmailAddress'] = $Attributes['EmailAddress']
}

# Password handling: support SecureString, ProtectedString, and explicit PlainText
$hasAccountPassword = $Attributes.ContainsKey('AccountPassword')
$hasAccountPasswordAsPlainText = $Attributes.ContainsKey('AccountPasswordAsPlainText')

if ($hasAccountPassword -and $hasAccountPasswordAsPlainText) {
throw "Ambiguous password configuration: both 'AccountPassword' and 'AccountPasswordAsPlainText' are provided. Use only one."
}

if ($hasAccountPassword) {
$passwordValue = $Attributes['AccountPassword']

if ($null -eq $passwordValue) {
throw "AccountPassword: Value cannot be null. Provide a SecureString or ProtectedString (from ConvertFrom-SecureString)."
}

if ($passwordValue -is [securestring]) {
# Mode 1: SecureString - use directly
$params['AccountPassword'] = $passwordValue
}
elseif ($passwordValue -is [string]) {
# Mode 2: ProtectedString (from ConvertFrom-SecureString)
try {
$params['AccountPassword'] = ConvertTo-SecureString -String $passwordValue -ErrorAction Stop
}
catch {
$errorMsg = "AccountPassword: Expected a ProtectedString (output from ConvertFrom-SecureString without -Key) but conversion failed. "
$errorMsg += "Only DPAPI-scoped ProtectedStrings are supported (created under the same Windows user and machine). "
$errorMsg += "Key-based protected strings (using -Key or -SecureKey) are not supported. "
if ($null -ne $_.Exception) {
$errorMsg += "Exception type: $($PSItem.Exception.GetType().FullName). "
if (-not [string]::IsNullOrWhiteSpace($_.Exception.Message)) {
$errorMsg += "Message: $($_.Exception.Message)"
}
}
throw $errorMsg
}
}
else {
throw "AccountPassword: Expected a SecureString or ProtectedString (string from ConvertFrom-SecureString), but received type: $($passwordValue.GetType().FullName)"
Comment thread
blindzero marked this conversation as resolved.
Comment thread
blindzero marked this conversation as resolved.
}
}

if ($hasAccountPasswordAsPlainText) {
$plainTextPassword = $Attributes['AccountPasswordAsPlainText']

if ($null -eq $plainTextPassword) {
throw "AccountPasswordAsPlainText: Value cannot be null. Provide a non-empty plaintext password string."
}

if ($plainTextPassword -isnot [string]) {
throw "AccountPasswordAsPlainText: Expected a string but received type: $($plainTextPassword.GetType().FullName)"
}

if ([string]::IsNullOrWhiteSpace($plainTextPassword)) {
throw "AccountPasswordAsPlainText: Password cannot be null or empty."
}

# Mode 3: Explicit plaintext - convert with -AsPlainText
# This is an intentional escape hatch with explicit opt-in via AccountPasswordAsPlainText.
# The value is redacted from logs/events via Copy-IdleRedactedObject.
$params['AccountPassword'] = ConvertTo-SecureString -String $plainTextPassword -AsPlainText -Force
}

if ($null -ne $this.Credential) {
$params['Credential'] = $this.Credential
}
Expand Down
48 changes: 48 additions & 0 deletions tests/Core/Copy-IdleRedactedObject.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -112,5 +112,53 @@ Describe 'Copy-IdleRedactedObject - deterministic redaction utility' {

$copy.password | Should -Be '<redacted>'
}

It 'redacts AccountPassword key' {
$input = @{
userName = 'testuser'
AccountPassword = 'ProtectedStringValue'
otherField = 'visible'
}

$copy = Copy-IdleRedactedObject -Value $input

$copy.userName | Should -Be 'testuser'
$copy.AccountPassword | Should -Be '[REDACTED]'
$copy.otherField | Should -Be 'visible'
}

It 'redacts AccountPasswordAsPlainText key' {
$input = @{
userName = 'testuser'
AccountPasswordAsPlainText = 'PlainTextPassword'
otherField = 'visible'
}

$copy = Copy-IdleRedactedObject -Value $input

$copy.userName | Should -Be 'testuser'
$copy.AccountPasswordAsPlainText | Should -Be '[REDACTED]'
$copy.otherField | Should -Be 'visible'
}

It 'redacts both password fields if present in nested structure' {
$input = @{
userDetails = @{
name = 'alice'
AccountPassword = 'SecureValue'
}
settings = [pscustomobject]@{
AccountPasswordAsPlainText = 'PlainTextValue'
enabled = $true
}
}

$copy = Copy-IdleRedactedObject -Value $input

$copy.userDetails.name | Should -Be 'alice'
$copy.userDetails.AccountPassword | Should -Be '[REDACTED]'
$copy.settings.AccountPasswordAsPlainText | Should -Be '[REDACTED]'
$copy.settings.enabled | Should -Be $true
}
}
}
Loading
Loading