OpenID Connect with Certificate Credentials?

We’ve got PSU running with OIDC authentication (MS Azure/Entra) and client secret credentials currently, but security requirements obligate us to rotate that every 90 days. If we swap to certificate credentials, we can move to a much longer rotation period and save ourselves some busy-work every quarter.

Microsoft identity platform certificate credentials | Microsoft Learn indicates it’s supported on their end but there’s no option for certificate-based credentials on the PSU side. Is this something that could be included in a future release?

Product: PowerShell Universal
Version: 3.8.4

Sidenote: I just remembered a thread/discord from years ago, regarding the OIDC auth working without specifying a valid secret value. I thought that was a bug that would be fixed.

Seems like that’s not the case, as my secret expired a month ago but still works. I just tried deleting my Client secret, both in azure, but also in appsettings - still works.

Certificate based auth would be nice, but a workaround seems to just let it expire, or delete the secret entirely.

Even if this would work on Microsoft’s end, it’s in no way an acceptable solution for us. It’s a large entreprise environment and it’s not optional. The security requirements have teeth. :blush:

Sure, but what added security does a client secret add, if the functionality is the same with or without a valid Client secret?
it seems to work without a CS, with an expired CS, and random letters.

Having a Client Secret does nothing to prevent the use of the integration, as anyone else effectively just needs to know the TenantID and App ID. The only layer of “security” seems to be the allowlist of redirect URLS.

Seems like a bug or design error.

I don’t disagree with you, but it’s outside the scope of my concern. Ironman Software can’t really help me with a bug or security issue with Microsoft Entra itself, but could potentially support a different assertion format to use with its OIDC configuration. :man_shrugging:

Set-PSUAuthenticationMethod actually has a -Configure parameter. We added this to allow for custom configuration of the OIDC options.

It’s a script block where $args[0] is a OpenIdConnectOptions object. It’s what we use to configure the OIDC setup.

I haven’t looked into it much but you might be able to take advantage of that to do some custom stuff that works with a cert.

Feel free to raise a feature request for built in cert support.

This sounds like potentially what I need to get facebook OIDC working (I’ve only been configuring OIDC with docker env vars so far) which I’ve had trouble with (ended up falling back to google OIDC which is fine) so while it’s a slightly different topic from OP, thanks for this answer, I’ll look into that for my issue too!

Feel free to raise a feature request for built in cert support.

There actually is an enhancement open for this from our first discussion on it in March 2023! :]

Certificate Support for OpenID Connect · Issue #2382 · ironmansoftware/issues (github.com)

I was poking at this today and had the proper config added in our dev tenant for Entra, and also figured out how to construct the request. I got a successful token response from Entra, but I still have to marry that with the PSU context of using an OpenIDConnectOptions object on behalf of the user to execute it. @adam, in looking at the below code do you have an idea of how to go about this?

using assembly 'C:\Program Files (x86)\Universal\Microsoft.AspNetCore.Authentication.OpenIdConnect.dll'
using assembly 'C:\Program Files (x86)\Universal\Microsoft.IdentityModel.Tokens.dll'
using assembly 'C:\Program Files (x86)\Universal\Microsoft.IdentityModel.JsonWebTokens.dll'

# [redacted tenant/app definitions]

$PSUAppSettings = Get-Content 'C:\ProgramData\PowerShellUniversal\appsettings.json' | ConvertFrom-Json

$SecurityTokenDescriptor = [Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor]::new()
$JwtClaims = [System.Collections.Generic.Dictionary`2[[string],[object]]]::new()
$JwtClaims.Add('aud', "https://login.microsoftonline.com/$tenantId/oauth2/v2.0/token")
$JwtClaims.Add('iss', $appId)
$JwtClaims.Add('jti', [guid]::NewGuid().Guid)
$JwtClaims.Add('sub', $appId)
$SecurityTokenDescriptor.Claims = $JwtClaims

$HATCertificate = [System.Security.Cryptography.X509Certificates.X509Certificate2]::new($PSUAppSettings.Kestrel.Endpoints.Https.Certificate.Path, ($PSUAppSettings.Kestrel.Endpoints.Https.Certificate.Password | ConvertTo-SecureString -AsPlainText -Force))
$SecurityTokenDescriptor.SigningCredentials = [Microsoft.IdentityModel.Tokens.X509SigningCredentials]::new($HATCertificate)

$EncodedAssertion = [Microsoft.IdentityModel.JsonWebTokens.JsonWebTokenHandler]::new().CreateToken($SecurityTokenDescriptor)

$JwtQueryParams = @{
	Uri         = "https://login.microsoftonline.com/$tenantId/oauth2/v2.0/token"
	Method      = 'POST'
	Body        = "client_id=$appId&client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer&client_assertion=$EncodedAssertion&grant_type=client_credentials&scope=https://graph.microsoft.com/.default"
	ContentType = 'application/x-www-form-urlencoded'
}

Invoke-RestMethod @JwtQueryParams

token_type expires_in ext_expires_in access_token
---------- ---------- -------------- ------------
Bearer           3599           3599 #redacted