OpenID Connect (Duende Demo)

Authenticate users via OpenID Connect using the public demo server at demo.duendesoftware.com with PKCE, token persistence, and a cookie+policy scheme for clean route protection.

Full source

File: pwsh/tutorial/examples/8.11-OIDC.ps1

<#
    Sample: OpenID Connect (Duende Demo) - Multiple Client Modes
    Purpose: Demonstrate OIDC login with various Duende demo clients and configurations.

    Available Modes:
      1. interactive.confidential - Standard confidential client with client secret
      2. interactive.confidential.jwt - Confidential client with private_key_jwt authentication
      3. interactive.confidential.short - Confidential with short-lived tokens (75s)
      4. interactive.confidential.short.jwt - Short tokens + JWT authentication
      5. interactive.public - Public client (no secret, requires PKCE)
      6. interactive.public.short - Public client with short-lived tokens (75s)
      7. interactive.confidential.nopkce - Confidential without PKCE
      8. interactive.confidential.hybrid - Hybrid flow
      9. interactive.implicit - Implicit flow (legacy)

    Usage Examples:
      # Standard confidential client (1-hour tokens)
      .\8.11-OIDC.ps1
      .\8.11-OIDC.ps1 -Mode 'interactive.confidential'

      # JWT authentication (more secure than client secrets)
      .\8.11-OIDC.ps1 -Mode 'interactive.confidential.jwt'

      # Short-lived tokens (75 seconds) - test token expiration
      .\8.11-OIDC.ps1 -Mode 'interactive.confidential.short'

      # Public client (no secret, PKCE required)
      .\8.11-OIDC.ps1 -Mode 'interactive.public'

    Notes:
      - Schemes registered: 'oidc', 'oidc.Cookies', 'oidc.Policy'
      - Redirect URI: https://localhost:<Port>/signin-oidc
      - JWT modes use Duende's demo RSA keys from Assets/certs/*.pem
      - For refresh tokens, 'offline_access' scope is included by default
      - Do NOT commit real client secrets in production
#>
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingConvertToSecureStringWithPlainText', '')]
param(
    # Default HTTPS port changed to 5000 (common ASP.NET dev cert port; matches many demo redirect registrations)
    [int]$Port = 5000,
    [IPAddress]$IPAddress = [IPAddress]::Loopback,
    [string]$Authority = 'https://demo.duendesoftware.com',
    [ValidateSet(
        'interactive.confidential',
        'interactive.confidential.jwt',
        'interactive.confidential.short',
        'interactive.confidential.short.jwt',
        'interactive.public',
        'interactive.public.short',
        'interactive.confidential.nopkce',
        'interactive.confidential.hybrid',
        'interactive.implicit'
    )]
    [string]$Mode = 'interactive.confidential.jwt',
    # Force replacing 127.0.0.1 host with localhost in redirect_uri (helps when demo client registration only lists localhost)
    [switch]$ForceLocalhostRedirect,
    # Attempt a fallback to 'interactive.confidential' client_id for authorize if jwt client continues to fail (experimental)
    [switch]$EnableFallbackClient
)



# Define all available OIDC client modes
$allModes = @{
    'interactive.confidential' = @{
        ClientId = 'interactive.confidential'
        ClientSecret = 'secret'
        GrantType = 'authorization code and client credentials'
        ResponseType = 'code'
        ResponseMode = $null
        TokenLifetime = '1h'
        RequiresPKCE = $true
        UseJwtAuth = $false
        Scopes = @('openid', 'profile', 'email', 'offline_access', 'api')
        Description = 'Standard confidential client with client secret'
    }
    'interactive.confidential.jwt' = @{
        ClientId = 'interactive.confidential.jwt'
        ClientSecret = $null  # Uses private_key_jwt
        GrantType = 'authorization code and client credentials'
        ResponseType = 'code'
        ResponseMode = $null
        TokenLifetime = '1h'
        RequiresPKCE = $true
        UseJwtAuth = $true
        Scopes = @('openid', 'profile', 'email', 'offline_access', 'api')
        Description = 'Confidential client with private_key_jwt authentication (more secure)'
    }
    'interactive.confidential.short' = @{
        ClientId = 'interactive.confidential.short'
        ClientSecret = 'secret'
        GrantType = 'authorization code and client credentials'
        ResponseType = 'code'
        ResponseMode = $null
        TokenLifetime = '0h 1m 15s (75 seconds)'
        RequiresPKCE = $true
        UseJwtAuth = $false
        Scopes = @('openid', 'profile', 'email', 'offline_access', 'api')
        Description = 'Confidential client with short-lived tokens (useful for testing expiration)'
    }
    'interactive.confidential.short.jwt' = @{
        ClientId = 'interactive.confidential.short.jwt'
        ClientSecret = $null
        GrantType = 'authorization code and client credentials'
        ResponseType = 'code'
        ResponseMode = $null
        TokenLifetime = '0h 1m 15s (75 seconds)'
        RequiresPKCE = $true
        UseJwtAuth = $true
        Scopes = @('openid', 'profile', 'email', 'offline_access', 'api')
        Description = 'Short-lived tokens + JWT authentication'
    }
    'interactive.public' = @{
        ClientId = 'interactive.public'
        ClientSecret = $null
        GrantType = 'authorization code'
        ResponseType = 'code'
        ResponseMode = $null
        TokenLifetime = '1h'
        RequiresPKCE = $true
        UseJwtAuth = $false
        Scopes = @('openid', 'profile', 'email', 'offline_access', 'api')
        Description = 'Public client (no secret, PKCE required)'
    }
    'interactive.public.short' = @{
        ClientId = 'interactive.public.short'
        ClientSecret = $null
        GrantType = 'authorization code'
        ResponseType = 'code'
        ResponseMode = $null
        TokenLifetime = '0h 1m 15s (75 seconds)'
        RequiresPKCE = $true
        UseJwtAuth = $false
        Scopes = @('openid', 'profile', 'email', 'offline_access', 'api')
        Description = 'Public client with short-lived tokens'
    }
    'interactive.confidential.nopkce' = @{
        ClientId = 'interactive.confidential.nopkce'
        ClientSecret = 'secret'
        GrantType = 'authorization code and client credentials'
        ResponseType = 'code'
        ResponseMode = $null
        TokenLifetime = '1h'
        RequiresPKCE = $false
        UseJwtAuth = $false
        Scopes = @('openid', 'profile', 'email', 'offline_access', 'api')
        Description = 'Confidential client without PKCE (legacy compatibility)'
    }
    'interactive.confidential.hybrid' = @{
        ClientId = 'interactive.confidential.hybrid'
        ClientSecret = 'secret'
        GrantType = 'hybrid and client credentials'
        ResponseType = 'code id_token'  # Hybrid flow requires id_token at authorization endpoint
        ResponseMode = 'form_post'      # Hybrid flow typically uses form_post
        TokenLifetime = '1h'
        RequiresPKCE = $true
        UseJwtAuth = $false
        Scopes = @('openid', 'profile', 'email', 'offline_access', 'api')
        Description = 'Hybrid flow (returns id_token at authorization endpoint)'
    }
    'interactive.implicit' = @{
        ClientId = 'interactive.implicit'
        ClientSecret = $null
        GrantType = 'implicit'
        ResponseType = 'id_token token'  # Implicit flow returns tokens directly
        ResponseMode = 'fragment'        # Implicit flow uses fragment for security
        TokenLifetime = '1h'
        RequiresPKCE = $true
        UseJwtAuth = $false
        Scopes = @('openid', 'profile', 'email')  # No offline_access for implicit
        Description = 'Implicit flow (legacy, not recommended for new applications)'
    }
}

# Get the selected mode configuration
$modeConfig = $allModes[$Mode]
$ClientId = $modeConfig.ClientId
$ClientSecret = $modeConfig.ClientSecret
$Scopes = $modeConfig.Scopes
$UseJwtAuth = $modeConfig.UseJwtAuth


# Effective client id may be overridden for fallback logic (only if requested)
$EffectiveClientId = $ClientId

Initialize-KrRoot -Path $PSScriptRoot

# For JWT authentication modes, load the Duende demo RSA keys



# 1) Logging
New-KrLogger |
    Set-KrLoggerLevel -Value Debug |
    Add-KrSinkConsole |
    Register-KrLogger -Name 'console' -SetAsDefault | Out-Null


# 2) Server
New-KrServer -Name 'OIDC Duende Demo'

# 3) HTTPS endpoint (primary)
Add-KrEndpoint -Port $Port -IPAddress $IPAddress -SelfSignedCert

# 4) HTTP endpoint for redirect (optional - redirects HTTP to HTTPS)
# Add-KrEndpoint -Port 5001 -IPAddress $IPAddress
# Add-KrHttpsRedirection -HttpPort 5001 -HttpsPort $Port

# 5) OpenID Connect auth (adds 'oidc', 'oidc.Cookies', 'oidc.Policy')
if ($UseJwtAuth) {
    # JWT authentication requires custom event handling to inject client_assertion
    Write-KrLog -Level Information -Message 'Configuring OIDC with private_key_jwt authentication'

    # Create options object to configure events
    $oidcOptions = [Kestrun.Authentication.OidcOptions]::new()
    $oidcOptions.Authority = $Authority
    $oidcOptions.ClientId = $EffectiveClientId
    $oidcOptions.UsePkce = $modeConfig.RequiresPKCE
    # Enable token persistence and userinfo claims retrieval
    $oidcOptions.SaveTokens = $true
    $oidcOptions.GetClaimsFromUserInfoEndpoint = $true
    $oidcOptions.UseSecurityTokenValidator = $true
    # Name claim mapping Important to fill Identity.Name
    $oidcOptions.TokenValidationParameters.NameClaimType = 'name'
    $oidcOptions.PushedAuthorizationBehavior = 'Disable'
    # Add scopes
    $oidcOptions.Scope.Clear()
    foreach ($scope in $Scopes) {
        $oidcOptions.Scope.Add($scope) | Out-Null
    }

    # Add ResponseType if specified
    if ($modeConfig.ResponseType) {
        switch ($modeConfig.ResponseType) {
            'code' { $oidcOptions.ResponseType = [Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectResponseType]::Code }
            'code id_token' { $oidcOptions.ResponseType = [Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectResponseType]::CodeIdToken }
        }
        Write-KrLog -Level Debug -Message 'Using ResponseType: {responseType}' -Values $modeConfig.ResponseType
    }

    # Add ResponseMode if specified
    if ($modeConfig.ResponseMode) {
        switch ($modeConfig.ResponseMode) {
            'form_post' { $oidcOptions.ResponseMode = [Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectResponseMode]::FormPost }
        }
        Write-KrLog -Level Debug -Message 'Using ResponseMode: {responseMode}' -Values $modeConfig.ResponseMode
    }
    <#
    # The './Assets/certs/client-jwt-cert.key' file contains the RSA private key in PEM format
    # The corresponding public key certificate is in './Assets/certs/client-jwt-cert.pem'
    # The Duende demo server is pre-configured to trust this key for the 'interactive.confidential.jwt' client.
    # and has beed generate using the ConvertFrom-KrJwkJsonToCertificate function below

    $ClientAssertionJwkJson = @'
    {
        "d":"GmiaucNIzdvsEzGjZjd43SDToy1pz-Ph-shsOUXXh-dsYNGftITGerp8bO1iryXh_zUEo8oDK3r1y4klTonQ6bLsWw4ogjLPmL3yiqsoSjJa1G2Ymh_"+
        "RY_sFZLLXAcrmpbzdWIAkgkHSZTaliL6g57vA7gxvd8L4s82wgGer_JmURI0ECbaCg98JVS0Srtf9GeTRHoX4foLWKc1Vq6NHthzqRMLZe-aRBNU9IMvXNd7kCcIbHCM3GTD_8cFj135nBPP2HOgC_ZXI1txsEf-djqJj8W5vaM7ViKU28IDv1gZGH3CatoysYx6jv1XJVvb2PH8RbFKbJmeyUm3Wvo-rgQ",
        "dp":"YNjVBTCIwZD65WCht5ve06vnBLP_Po1NtL_4lkholmPzJ5jbLYBU8f5foNp8DVJBdFQW7wcLmx85-NC5Pl1ZeyA-Ecbw4fDraa5Z4wUKlF0LT6VV79rfOF19y8kwf6MigyrDqMLcH_CRnRGg5NfDsijlZXffINGuxg6wWzhiqqE",
        "dq":"LfMDQbvTFNngkZjKkN2CBh5_MBG6Yrmfy4kWA8IC2HQqID5FtreiY2MTAwoDcoINfh3S5CItpuq94tlB2t-VUv8wunhbngHiB5xUprwGAAnwJ3DL39D2m43i_3YP-UO1TgZQUAOh7Jrd4foatpatTvBtY3F1DrCrUKE5Kkn770M",
        "e":"AQAB",
        "kid":"ZzAjSnraU3bkWGnnAqLapYGpTyNfLbjbzgAPbbW2GEA",
        "kty":"RSA",
        "n":"wWwQFtSzeRjjerpEM5Rmqz_DsNaZ9S1Bw6UbZkDLowuuTCjBWUax0vBMMxdy6XjEEK4Oq9lKMvx9JzjmeJf1knoqSNrox3Ka0rnxXpNAz6sATvme8p9mTXyp0cX4lF4U2J54xa2_S9NF5QWvpXvBeC4GAJx7QaSw4zrUkrc6XyaAiFnLhQEwKJCwUw4NOqIuYvYp_IXhw-5Ti_icDlZS-282PcccnBeOcX7vc21pozibIdmZJKqXNsL1Ibx5Nkx1F1jLnekJAmdaACDjYRLL_6n3W4wUp19UvzB1lGtXcJKLLkqB6YDiZNu16OSiSprfmrRXvYmvD8m6Fnl5aetgKw",
        "p":"7enorp9Pm9XSHaCvQyENcvdU99WCPbnp8vc0KnY_0g9UdX4ZDH07JwKu6DQEwfmUA1qspC-e_KFWTl3x0-I2eJRnHjLOoLrTjrVSBRhBMGEH5PvtZTTThnIY2LReH-6EhceGvcsJ_MhNDUEZLykiH1OnKhmRuvSdhi8oiETqtPE",
        "q":"0CBLGi_kRPLqI8yfVkpBbA9zkCAshgrWWn9hsq6a7Zl2LcLaLBRUxH0q1jWnXgeJh9o5v8sYGXwhbrmuypw7kJ0uA3OgEzSsNvX5Ay3R9sNel-3Mqm8Me5OfWWvmTEBOci8RwHstdR-7b9ZT13jk-dsZI7OlV_uBja1ny9Nz9ts",
        "qi":"pG6J4dcUDrDndMxa-ee1yG4KjZqqyCQcmPAfqklI2LmnpRIjcK78scclvpboI3JQyg6RCEKVMwAhVtQM6cBcIO3JrHgqeYDblp5wXHjto70HVW6Z8kBruNx1AH9E8LzNvSRL-JVTFzBkJuNgzKQfD0G77tQRgJ-Ri7qu3_9o1M4"
    }
    '@
    $clientCert = ConvertFrom-KrJwkJsonToCertificate -Jwk ($ClientAssertionJwkJson) -SubjectName 'CN=Duende Demo Client JWT'
    Export-KrCertificate -Certificate $clientCert -FilePath './Assets/certs/client-jwt-cert' -Format Pem -IncludePrivateKey
#>

    $oidcOptions.JwkJson = ConvertTo-KrJwkJson -RsaPrivateKeyPath './Assets/certs/client-jwt-cert.key'


    #   $oidcOptions.JwkJson = ($clientAssertionJwkJson | ConvertTo-Json -Compress)
    #>
    Add-KrOpenIdConnectAuthentication -AuthenticationScheme 'oidc' -Options $oidcOptions

} else {
    # Standard configuration with client secret
    # Apply fallback at configuration time for non-JWT modes if requested (rare)
    if ($EnableFallbackClient.IsPresent -and $ClientId -eq 'interactive.confidential.jwt') {
        $EffectiveClientId = 'interactive.confidential'
        Write-KrLog -Level Warning -Message 'Fallback client id applied at configuration: interactive.confidential'
    }
    $oidcOptions = [Kestrun.Authentication.OidcOptions]::new()
    $oidcOptions.Authority = $Authority
    $oidcOptions.ClientId = $EffectiveClientId
    $oidcOptions.UsePkce = $modeConfig.RequiresPKCE
    # Enable token persistence and userinfo claims retrieval
    $oidcOptions.SaveTokens = $true
    $oidcOptions.GetClaimsFromUserInfoEndpoint = $true
    $oidcOptions.UseSecurityTokenValidator = $true
    # Name claim mapping Important to fill Identity.Name
    $oidcOptions.TokenValidationParameters.NameClaimType = 'name'
    $oidcOptions.PushedAuthorizationBehavior = [Microsoft.AspNetCore.Authentication.OpenIdConnect.PushedAuthorizationBehavior]::Disable
    # Add scopes
    $oidcOptions.Scope.Clear()
    foreach ($scope in $Scopes) {
        $oidcOptions.Scope.Add($scope) | Out-Null
    }
    $oidcOptions.ResponseType = [Microsoft.IdentityModel.Protocols.OpenIdConnect.OpenIdConnectResponseType]::Code

    # Add client secret
    if ($ClientSecret) {
        $oidcOptions.ClientSecret = $ClientSecret
    }

    Add-KrOpenIdConnectAuthentication -Options $oidcOptions -Name 'oidc'
}

# 6) Finalize pipeline
Enable-KrConfiguration

# 7) Landing page
Add-KrMapRoute -Verbs Get -Pattern '/' -ScriptBlock {
    Write-KrHtmlResponse -FilePath './Assets/wwwroot/openId/oidcdemo.html' -Variables @{
        mode = $ClientId
        authority = $Authority
        clientId = $ClientId
        description = $modeConfig.Description
        grantType = $modeConfig.GrantType
        tokenLifetime = $modeConfig.TokenLifetime
        requiresPkce = if ($modeConfig.RequiresPKCE) { 'Yes' } else { 'No' }
        authMethod = if ($UseJwtAuth) { 'private_key_jwt (RSA signature)' } elseif ($ClientSecret) { "client_secret_post (secret: $($ClientSecret.Substring(0,3))***)" } else { 'none (public client)' }
        scopes = ($Scopes -join ', ')
        port = $Port
    }
}

Add-KrMapRoute -Verbs Get -Pattern '/login' -ScriptBlock {
    # Manual pre-challenge diagnostics: construct expected authorize URL
    $hostHeader = $Context.Request.Host.Value
    $scheme = if ($Context.Request.IsHttps) { 'https' } else { 'http' }
    $redirectHost = if ($ForceLocalhostRedirect.IsPresent -and $hostHeader -like '127.0.0.1*') { 'localhost' } else { $hostHeader }
    if ($hostHeader -like '127.0.0.1*' -and -not $ForceLocalhostRedirect.IsPresent) {
        Write-KrLog -Level Warning -Message '127.0.0.1 host detected; consider -ForceLocalhostRedirect for demo client compatibility.'
    }
    $redirectUri = "${scheme}://${redirectHost}/signin-oidc"
    $responseType = if ($modeConfig.ResponseType) { $modeConfig.ResponseType } else { 'code' }
    $scopeValue = ($Scopes -join ' ')
    $authorizeEndpoint = "$Authority/connect/authorize"
    $authUrlPreview = "$authorizeEndpoint?client_id=$ClientId&response_type=$responseType&scope=$([Uri]::EscapeDataString($scopeValue))&redirect_uri=$([Uri]::EscapeDataString($redirectUri))"
    Write-KrLog -Level Warning -Message 'Authorize diagnostics: client_id={clientId}, response_type={rt}, redirect_uri={ru}, scope={scope}' -Values $ClientId, $responseType, $redirectUri, $scopeValue
    Write-KrLog -Level Warning -Message 'Authorize base URL: {url}' -Values $authorizeEndpoint
    Write-KrLog -Level Warning -Message 'Authorize assembled (partial preview): {preview}' -Values $authUrlPreview
    if ($scheme -eq 'https' -and $hostHeader -like '127.0.0.1*') {
        Write-KrLog -Level Warning -Message 'Host 127.0.0.1 detected; some demo clients are registered for localhost. Try https://localhost:{port}/ if invalid_client continues.' -Values $Port
    }
    # Proceed with challenge
    Invoke-KrChallenge -Scheme 'oidc' -RedirectUri '/hello'
} -AllowAnonymous

# 8) Protected route group using the policy scheme
#Add-KrRouteGroup -Prefix '/oidc' -AuthorizationScheme 'oidc'  {
Add-KrMapRoute -Verbs Get -Pattern '/hello' -AuthorizationScheme 'oidc' -ScriptBlock {
    Write-Host ($Context.User.Identity | ConvertTo-Json -Depth 5)
    $name = $Context.User.Identity.Name ?? '(no name)'
    $email = $Context.User.FindFirst('email')?.Value ?? 'No email claim'
    $sub = $Context.User.FindFirst('sub')?.Value ?? 'No sub claim'

    # Get authentication details
    $authType = $Context.User.Identity.AuthenticationType ?? 'Unknown'
    $isAuthenticated = $Context.User.Identity.IsAuthenticated

    Write-KrHtmlResponse -FilePath './Assets/wwwroot/openId/protected.html' -Variables @{
        name = $name
        email = $email
        sub = $sub
        authType = $authType
        isAuthenticated = $isAuthenticated
    }
}

Add-KrMapRoute -Verbs Get -Pattern '/me' -AuthorizationScheme 'oidc' -ScriptBlock {
    $claims = foreach ($c in $Context.User.Claims) { @{ Type = $c.Type; Value = $c.Value } }
    Write-KrJsonResponse @{ scheme = 'oidc'; authenticated = $Context.User.Identity.IsAuthenticated; claims = $claims }
}

Add-KrMapRoute -Verbs Get -Pattern '/logout' -AllowAnonymous -ScriptBlock {
    # Use enhanced Invoke-KrCookieSignOut with OIDC support
    # Construct full redirect URI for post-logout redirect
    $uriScheme = if ($Context.Request.IsHttps) { 'https' } else { 'http' }
    $hostValue = $Context.Request.Host.Value
    $postLogoutUri = "${uriScheme}://${hostValue}/"

    Write-KrLog -Level Debug -Message 'Logging out with post_logout_redirect_uri: {uri}' -Values $postLogoutUri
    Invoke-KrCookieSignOut -AuthKind 'Oidc' -Scheme 'oidc' -RedirectUri $postLogoutUri
}

# 9) Start server
Write-KrLog -Level Information -Message '=== OIDC Duende Demo ==='
Write-KrLog -Level Information -Message 'Mode: {mode} ({description})' -Values $Mode, $modeConfig.Description
Write-KrLog -Level Information -Message 'Authority: {authority}' -Values $Authority
Write-KrLog -Level Information -Message 'Client ID (requested): {clientId}' -Values $ClientId
if ($EffectiveClientId -ne $ClientId) {
    Write-KrLog -Level Information -Message 'Client ID (effective): {clientId}' -Values $EffectiveClientId
}
Write-KrLog -Level Information -Message 'Grant Type: {grantType}' -Values $modeConfig.GrantType
Write-KrLog -Level Information -Message 'Token Lifetime: {lifetime}' -Values $modeConfig.TokenLifetime
Write-KrLog -Level Information -Message 'Requires PKCE: {pkce}' -Values $modeConfig.RequiresPKCE
Write-KrLog -Level Information -Message 'Scopes: {scopes}' -Values ($Scopes -join ', ')

if ($UseJwtAuth) {
    Write-KrLog -Level Information -Message '✅ JWT Bearer authentication enabled with RSA private key signing!'
    Write-KrLog -Level Information -Message 'Using private_key_jwt client assertion for enhanced security.'
}

Write-KrLog -Level Information -Message 'Visit: https://localhost:{port} (redirect host fix={fix}, fallback={fallback})' -Values $Port, ($ForceLocalhostRedirect.IsPresent), ($EnableFallbackClient.IsPresent)

Start-KrServer -CloseLogsOnExit

Step-by-step

  1. Logger: Register a console logger at Debug level.
  2. Server: Create a server named “OIDC Duende Demo”.
  3. Listener: Bind HTTPS on loopback and port 5000 with a self-signed cert.
  4. OIDC: Call Add-KrOpenIdConnectAuthentication (adds Oidc, Oidc.Cookies, and Oidc.Policy). Defaults to PKCE + SaveTokens. Use -Scope to add email or offline_access (for refresh tokens, if allowed).
  5. Configuration: Enable the Kestrun pipeline via Enable-KrConfiguration.
  6. Routes: Add a public landing and protected routes using -AuthorizationScheme 'oidc' (policy scheme forwards challenge to OIDC and authenticates via cookie).
  7. Start: Run the server with Start-KrServer.

Try it

# Optional: set Duende confidential client secret (demo value is 'secret')
$env:DUENDE_CLIENT_SECRET = 'secret'

# Start sample from repo root
pwsh docs/_includes/examples/pwsh/8.11-OIDC.ps1
# After login at Duende, fetch claims
Invoke-WebRequest https://localhost:5000/me -SkipCertificateCheck | Select -Expand Content

Key points

  • Three schemes are registered: oidc, oidc.Cookies, oidc.Policy. The policy scheme authenticates via cookies and forwards challenges to OIDC.
  • PKCE and token persistence are enabled by default.
  • Default scopes include openid and profile; add email or others with the -Scope parameter.
  • Callback URL must match https://localhost:<port><CallbackPath>. Default path is /signin-oidc. Change it with -CallbackPath if needed.

Troubleshooting

Symptom Cause Resolution
“redirect_uri not valid” Callback mismatch Ensure https://localhost:5000/signin-oidc is registered or change -CallbackPath.
Empty claims Provider returned minimal set Add scopes like email; verify GetUserInfo (default on).
Loop on challenge Cookies blocked Allow third-party cookies or test in a fresh profile.

References


Previous / Next