DuendeSoftware / Support

Support for Duende Software products
21 stars 0 forks source link

Multiple external schemes use incorrect IDataProtector and fail to decrypt state during login #740

Closed shashwatsingh closed 1 year ago

shashwatsingh commented 1 year ago

Which version of Duende IdentityServer are you using? v6.3.0

Which version of .NET are you using? 6.0

Describe the bug Already searched issues in this repo and official docs.

My problem is this:

Using multiple external authentication oidc schemes having same SignInScheme works for idp1 but fails for idp2: it fails to decrypt state parameter for idp2 (because it is using the IDataProtector for idp1?)

To Reproduce Use the starter for microsoft identity and add two different azure-ad tenants similar to https://docs.duendesoftware.com/identityserver/v6/ui/login/external/#state-url-length-and-isecuredataformat.

builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
  .AddOpenIdConnect("idp1", options => 
  {
    options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme;

    options.Authority = "https://login.microsoftonline.com/tenant1/v2.0";
    options.ClientId = "...";
    options.ClientSecret = "...";
  })
  .AddOpenIdConnect("idp2", options => 
  {
    options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme;

    options.Authority = "https://login.microsoftonline.com/tenant2/v2.0";
    options.ClientId = "...";
    options.ClientSecret = "...";
  })

Also configured DataProtector:

builder.Services.AddDataProtection()
    .SetApplicationName("my-sso")
    .PersistKeysToFileSystem(new DirectoryInfo("c:\\temp\\my-sso\\keys"));

Then login using idp2.

Expected behavior

state parameter should be decoded using the respective data protector, and login should succeed.

Log output/exception with stacktrace

dbug: Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationHandler[9]
      AuthenticationScheme: Identity.Application was not authenticated.
trce: Microsoft.AspNetCore.DataProtection.KeyManagement.KeyRingBasedDataProtector[31]
      Performing protect operation to key {76239108-c7fb-44b9-87ec-269afd93c97b} with purposes ('my-sso', 'Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectHandler', 'System.String', 'idp2', 'v1').
trce: Microsoft.AspNetCore.DataProtection.KeyManagement.KeyRingBasedDataProtector[31]
      Performing protect operation to key {76239108-c7fb-44b9-87ec-269afd93c97b} with purposes ('my-sso', 'Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectHandler', 'idp2', 'v1').
dbug: Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectHandler[53]
      HandleChallenge with Location: https://login.microsoftonline.com/.../oauth2/v2.0/authorize?client_id=...&redirect_uri=...&response_type=id_token&scope=openid%20profile&response_mode=form_post&nonce=...&state=...; and Set-Cookie: .AspNetCore.OpenIdConnect.Nonce....=N; expires=Thu, 22 Jun 2023 15:54:16 GMT; path=/signin-oidc; secure; samesite=none; httponly,.AspNetCore.Correlation.6R3Zc9ENewvm9qbtQTOgZvQMCp4BTNEQztun63DG0Lk=N; expires=Thu, 22 Jun 2023 15:54:16 GMT; path=/signin-oidc; secure; samesite=none; httponly.
info: Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectHandler[12]
      AuthenticationScheme: idp2 was challenged.
dbug: Duende.IdentityServer.Hosting.CorsPolicyProvider[0]
      CORS request made for path: /signin-oidc from origin: https://login.microsoftonline.com but was ignored because path was not for an allowed IdentityServer CORS endpoint
trce: Microsoft.AspNetCore.DataProtection.KeyManagement.KeyRingBasedDataProtector[5]
      Performing unprotect operation to key {76239108-c7fb-44b9-87ec-269afd93c97b} with purposes ('my-sso', 'Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectHandler', 'idp1', 'v1').
trce: Microsoft.AspNetCore.DataProtection.KeyManagement.KeyRingBasedDataProtector[5]
      Performing unprotect operation to key {76239108-c7fb-44b9-87ec-269afd93c97b} with purposes ('my-sso', 'Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectHandler', 'idp1', 'v1').
dbug: Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectHandler[11]
      Unable to read the message.State.
info: Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectHandler[4]
      Error from RemoteAuthentication: Unable to unprotect the message.State..
fail: Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware[1]
      An unhandled exception has occurred while executing the request.
      System.Exception: An error was encountered while handling the remote login.
       ---> System.Exception: Unable to unprotect the message.State.
         --- End of inner exception stack trace ---
         at Microsoft.AspNetCore.Authentication.RemoteAuthenticationHandler`1.HandleRequestAsync()
         at Duende.IdentityServer.Hosting.FederatedSignOut.AuthenticationRequestHandlerWrapper.HandleRequestAsync() in /_/src/IdentityServer/Hosting/FederatedSignOut/AuthenticationRequestHandlerWrapper.cs:line 38
         at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context)
         at Duende.IdentityServer.Hosting.DynamicProviders.DynamicSchemeAuthenticationMiddleware.Invoke(HttpContext context) in /_/src/IdentityServer/Hosting/DynamicProviders/DynamicSchemes/DynamicSchemeAuthenticationMiddleware.cs:line 50
         at Duende.IdentityServer.Hosting.BaseUrlMiddleware.Invoke(HttpContext context) in /_/src/IdentityServer/Hosting/BaseUrlMiddleware.cs:line 27
         at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.Invoke(HttpContext context)
brockallen commented 1 year ago

You need to use distinct CallbackPath, RemoteSignOutPath, and SignedOutCallbackPath for each different handler/scheme. This is due to how the Microsoft implementation works for the OIDC handlers (and is unrelated to IdentityServer). Are you missing those?

shashwatsingh commented 1 year ago

That was it, thank you. Here's the configuration that works for both schemes:

builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
  .AddOpenIdConnect("idp1", "Login using idp1",options =>
  {
    options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme;

    options.Authority = "https://login.microsoftonline.com/tenant1/v2.0";
    options.ClientId = "...";
    options.ClientSecret = "...";

    options.CallbackPath = "/signin-oidc-idp1";
    options.RemoteSignOutPath = "/signout-callback-oidc-idp1";
    options.SignedOutCallbackPath = "/signout-oidc-idp1";
  })
  .AddOpenIdConnect("idp2", "Login using idp2",options =>
  {
    options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme;

    options.Authority = "https://login.microsoftonline.com/tenant2/v2.0";
    options.ClientId = "...";
    options.ClientSecret = "...";

    options.CallbackPath = "/signin-oidc-idp2";
    options.RemoteSignOutPath = "/signout-callback-oidc-idp2";
    options.SignedOutCallbackPath = "/signout-oidc-idp2";
  })

Do you think it will be useful to add a note about "every occurrence of OIDC handler (of same type) requires unique callback paths" here: https://docs.duendesoftware.com/identityserver/v6/ui/login/external/#registering-authentication-handlers-for-external-providers? If yes, I can submit a PR.

brockallen commented 1 year ago

Do you think it will be useful to add a note about "every occurrence of OIDC handler (of same type) requires unique callback paths" here: https://docs.duendesoftware.com/identityserver/v6/ui/login/external/#registering-authentication-handlers-for-external-providers? If yes, I can submit a PR.

@josephdecock, thoughts?

josephdecock commented 1 year ago

@shashwatsingh, that makes sense to me - thanks a lot!