DuendeSoftware / Support

Support for Duende Software products
20 stars 0 forks source link

Issue making the IdentityServer host its own client #1303

Closed StuFrankish closed 3 weeks ago

StuFrankish commented 3 weeks ago

Which version of Duende IdentityServer are you using? 7.0.5

Which version of .NET are you using? .Net Core 8

Describe the bug Not a bug with Identity Server perse, but I'm having issues getting Identity Server to correctly receive its own authorization response message at the /signin-oidc endpoint.

To Reproduce I've added additional configuration within my Identity Server host to include the usual builder.Services.AddAuthentication(...).AddOpenIdConnect(...) code that would usually go into a separate hosted client, and pointed it back at itself as the authority.

The "client" is using the authorization code flow, with PKCE through a Pushed Authorization Request. This is working so much as I get the challenge, the login flow works as I would expect it to, but once the IdP returns to the "client" by posting to the /signin-oidc endpoint, I get errors.

Expected behavior I would have expected that the "client" portion of the host be able to process the incoming message and handle it exactly as it does with an externally hosted client, and return me to the correct redirect url (in this case, the /diagnostics page).

Log output/exception with stacktrace I've copied the console output and snipped some irrelevant bits (Hangfire heartbeats mostly).

Console Log & Stack Trace ``` [19:33:34 Debug] Duende.IdentityServer.ResponseHandling.AuthorizeResponseGenerator Creating Authorization Code Flow response. [19:33:34 Debug] Duende.IdentityServer.EntityFramework.Stores.PersistedGrantStore 16530947461157A4743644FDCE8DFF82FD9FFD0A3544F00456379620FBD4145D not found in database [19:33:34 Debug] Duende.IdentityServer.Endpoints.AuthorizeCallbackEndpoint Authorize endpoint response {"SubjectId": "8610167f-2b91-4b25-92cc-fb16bdf0c4f3", "ClientId": "mvc.par", "RedirectUri": "https://localhost:5001/signin-oidc", "State": "CfDJ8IbceeC5aLRBpu5IqhMr1V8I7ObkHHuUadGccguZwd7IV6bQxq_ptIu4b3zr8wrcqYN4Q_TDJIF294SfOFcPtTUNGTybm5GSkXGlY4107Qk2cS5KY0H37OhN3UwPYeasxTS4QxwBvOfpnylVOpkKCDTa5kOeJM3mY6UGZW5R2_FERQGzNaFSs8SWYiquQnkOcfVxeCt4jbcvGs9ERoKPfIM8X7VdOuPdrlM__CkP5BQta0NcHQf1iC8GeYZCkTZgIwoycNaSXD6gwdJp7Kb8vm0O3zB-ZUR_Usvg7WZs-edyGhXxHcxXu0a3jtBVU4azZNt7eL-tGDQqlwESoB2jPo9pU1bC6ynAkq14LBmgsiaDT4MsLwU3qePZyon84pn0WA", "Scope": "openid profile offline_access", "Error": null, "ErrorDescription": null, "$type": "AuthorizeResponseLog"} [19:33:34 Debug] Duende.IdentityServer.EntityFramework.Stores.PushedAuthorizationRequestStore removing gTFTkjR204Fr5PMkZi3sybmn3EzBONIXdxfbchFRTMI= pushed authorization from database [19:33:34 Debug] Duende.IdentityServer.Hosting.IdentityServerAuthenticationService Augmenting SignInContext [19:33:34 Debug] Duende.IdentityServer.EntityFramework.Stores.ServerSideSessionStore Found server-side session 75DA8CB509662C35DD0AA54C43BCD2487CB590F833403A6F8F96EC5A3D709D75 in database: True [19:33:34 Debug] Duende.IdentityServer.Stores.ServerSideTicketStore Renewing AuthenticationTicket for key 75DA8CB509662C35DD0AA54C43BCD2487CB590F833403A6F8F96EC5A3D709D75, with expiration: 07/03/2024 18:33:31 [19:33:34 Debug] Duende.IdentityServer.EntityFramework.Stores.ServerSideSessionStore Updated server-side session 75DA8CB509662C35DD0AA54C43BCD2487CB590F833403A6F8F96EC5A3D709D75 in database [19:33:34 Information] Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationHandler AuthenticationScheme: Cookies signed in. [19:33:34 Information] Serilog.AspNetCore.RequestLoggingMiddleware HTTP GET /connect/authorize/callback responded 200 in 65.2455 ms [19:33:37 Debug] Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectHandler Updating configuration [19:33:37 Debug] Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectHandler Redeeming code for tokens. [19:33:37 Error] Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectHandler Exception occurred while processing message. System.InvalidOperationException: An invalid request URI was provided. Either the request URI must be an absolute URI or BaseAddress must be set. at System.Net.Http.HttpClient.PrepareRequestMessage(HttpRequestMessage request) at System.Net.Http.HttpClient.SendAsync(HttpRequestMessage request, HttpCompletionOption completionOption, CancellationToken cancellationToken) at Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectHandler.RedeemAuthorizationCodeAsync(OpenIdConnectMessage tokenEndpointRequest) at Microsoft.AspNetCore.Authentication.OpenIdConnect.OpenIdConnectHandler.HandleRemoteAuthenticateAsync() [22:51:34 Information] Serilog.AspNetCore.RequestLoggingMiddleware HTTP POST /signin-oidc responded 302 in 109916.2101 ms [22:51:34 Debug] Duende.IdentityServer.Stores.ServerSideTicketStore Retrieve AuthenticationTicket for key 30C4D4934FF57FB93C48B7BB659536A69CD43D69D6A7D70B714A0519AB8CF607 [22:51:34 Debug] Duende.IdentityServer.EntityFramework.Stores.ServerSideSessionStore Found server-side session 30C4D4934FF57FB93C48B7BB659536A69CD43D69D6A7D70B714A0519AB8CF607 in database: True [22:51:34 Debug] Duende.IdentityServer.Stores.ServerSideTicketStore Ticket loaded for key: 30C4D4934FF57FB93C48B7BB659536A69CD43D69D6A7D70B714A0519AB8CF607, with expiration: 07/03/2024 21:49:33 [22:51:34 Debug] Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationHandler AuthenticationScheme: Cookies was successfully authenticated. [22:51:34 Debug] Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationHandler AuthenticationScheme: Cookies was successfully authenticated. [22:51:34 Debug] Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationHandler AuthenticationScheme: Cookies was successfully authenticated. [22:51:34 Information] Serilog.AspNetCore.RequestLoggingMiddleware HTTP GET /Error/AuthError responded 404 in 7.9477 ms ```

Additional context In both my working client and in the internal client, I've implement ParOidcEvents using an example I found some time ago. In this I have pulled up both public override Task RemoteFailure(RemoteFailureContext context) and public override Task MessageReceived(MessageReceivedContext context)

Placing a break point on MessageReceived gives me access to the context, I've had a look around and it looks to be right as far as I understand it. However stepping out of that method throws me immediately into the RemoteFailure method, with the above error in the stack trace.

I don't know what to check to ensure the URI is correct? I haven't done anything more special in the other test client I'm using and both are using the same client entity configuration (both redirect URI's for ports 5001 and 5002 are set).

This is the full configuration I'm using for the internal client ```c# public static void AddAndConfigureInternalIdentityClient(this WebApplicationBuilder builder) { // Configure our options objects. builder.Services.Configure(builder.Configuration.GetSection(key: ConfigurationSections.IdentityProvider)); // Create a local instance of the IDP options for immediate use. var identityProviderOptions = new IdentityProviderOptions(); builder.Configuration.GetSection(ConfigurationSections.IdentityProvider).Bind(identityProviderOptions); // Setup the rest of the client. builder.Services.AddTransient(); builder.Services.AddSingleton(_ => new DiscoveryCache(identityProviderOptions.Authority)); // Add PAR interaction httpClient builder.Services.AddHttpClient(name: "par_interaction_client", options => { options.BaseAddress = new Uri(uriString: identityProviderOptions.Authority); }); // Add session builder.Services.AddDistributedMemoryCache(); builder.Services.AddSession(options => { options.Cookie.Name = "mvc.par_session"; options.Cookie.SecurePolicy = Microsoft.AspNetCore.Http.CookieSecurePolicy.Always; options.IdleTimeout = TimeSpan.FromMinutes(30); options.Cookie.HttpOnly = true; options.Cookie.IsEssential = true; }); // add automatic token management builder.Services.AddOpenIdConnectAccessTokenManagement(); // add cookie-based session management with OpenID Connect authentication builder.Services.AddAuthentication(options => { options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme; options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme; }) .AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, options => { options.Cookie.Name = "mvc.par_app"; options.Cookie.HttpOnly = true; options.Cookie.IsEssential = true; options.Cookie.SecurePolicy = CookieSecurePolicy.Always; options.AccessDeniedPath = "/Error/AccessDenied"; options.Events.OnSigningOut = async e => { // automatically revoke refresh token at signout time await e.HttpContext.RevokeRefreshTokenAsync(); }; }) .AddOpenIdConnect(OpenIdConnectDefaults.AuthenticationScheme, options => { // Needed to add PAR support options.EventsType = typeof(ParOidcEvents); // Setup Client options.Authority = identityProviderOptions.Authority; options.ClientId = identityProviderOptions.ClientId; options.ClientSecret = identityProviderOptions.ClientSecret; options.CallbackPath = new PathString("/signin-oidc"); // code flow + PKCE (PKCE is turned on by default and required by the identity provider in this sample) options.ResponseType = OpenIdConnectResponseType.Code; options.UsePkce = true; options.Scope.Clear(); options.Scope.Add(OpenIdConnectScopes.OpenId); options.Scope.Add(OpenIdConnectScopes.Profile); options.Scope.Add(OpenIdConnectScopes.OfflineAccess); options.ClaimActions.ApplyCustomClaimsActions(); options.GetClaimsFromUserInfoEndpoint = true; options.SaveTokens = true; options.MapInboundClaims = false; options.DisableTelemetry = false; options.TokenValidationParameters = new TokenValidationParameters { NameClaimType = JwtClaimTypes.Name, RoleClaimType = JwtClaimTypes.Role }; }); } ```

The ParOidcEvents class is taken more or less entirely from your sample, I've only made some adjustments around getting the client secret from options to use in private async Task<ParResponse> PushAuthorizationParameters(RedirectContext context, string clientId)

StuFrankish commented 3 weeks ago

I've done a bit of digging into the OpenIdConnectHandler and have found where I think the process is failing for me, RedeemAuthorizationCodeAsync

Specifically this section;

var requestMessage = new HttpRequestMessage(HttpMethod.Post, tokenEndpointRequest.TokenEndpoint ?? _configuration?.TokenEndpoint);
requestMessage.Content = new FormUrlEncodedContent(tokenEndpointRequest.Parameters);
requestMessage.Version = Backchannel.DefaultRequestVersion;
var responseMessage = await Backchannel.SendAsync(requestMessage, Context.RequestAborted);

I can only assume that the both tokenEndpointRequest.TokenEndpoint and _configuration?.TokenEndpoint are null or incomplete. But as above, I'm not sure yet how to step in before this gets executed to find out why that's the case.

In a known good client authorization, the MessageReceivedContext always has the ProtocalMessage.TokenEndpoint property as null and the Options.Configuration property as null.

In both good and bad cases, Options.ConfigurationManager is not null and identical. Assuming the token endpoint is somehow derived from there using discovery, why would that fail? - the endpoint is correct and reachable.

image

brockallen commented 3 weeks ago

Not a bug with Identity Server perse, but I'm having issues getting Identity Server to correctly receive its own authorization response message at the /signin-oidc endpoint.

Since that code is co-hosted in the same app as IdentityServer itself, why do you need it to be it's own full blown OIDC client? Since it's co-hosted you have the cookie, and so that should allow you to know the identity of the user. Do you need anything more than that?

StuFrankish commented 3 weeks ago

Since that code is co-hosted in the same app as IdentityServer itself, why do you need it to be it's own full blown OIDC client? Since it's co-hosted you have the cookie, and so that should allow you to know the identity of the user. Do you need anything more than that?

Ultimately I want to embed some functionality into the host that I want secured in the same way other client applications are. Making use of the authorize endpoint and the extensibility of registering a customer interaction response generator.

The cookie created when the user first enters their username/password feels like it's not going to play well with this idea. I'm trying to figure out how it was done in the Skoruba Admin UI, which embeds a web app, but limited time means progress is very slow.

This option felt like a possible solution.

brockallen commented 3 weeks ago

If you have your UI co-hosted with IdentityServer and you then also need access tokens, then you can look here for more info: https://docs.duendesoftware.com/identityserver/v7/tokens/internal/

As for the Skoruba UI, I don't know -- I suspect he's fairly responsive on his repo, so you could ask there?