Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Silent refresh authenticates on OPTIONS preflight but not on GET to UserInfo endpoint

Environment:

  • IdentityServer4 instance supporting implicit flow
  • Angular 7 client apps using oidc-client-js
  • ASP.NET Framework Web API resources using the IdentityServer3 Katana Access Token Validation Middleware

Basic token issuance and validation is working fine in our environment. I am now trying to enable the silent refresh technique (as documented here). After enabling automaticSilentRenew and a short AccessTokenLifetime, I can see the silent requests firing off in my browser console as I would expect.

I can see two subsequent calls to the UserInfo endpoint of IS4 (see screenshot below). The first is the CORS preflight OPTIONS request. At a breakpoint in my custom implementation of IProfileService.IsActiveAsync(), I can see that this request successfully authenticates (by inspecting httpContext).

enter image description here

public class ProfileService : IProfileService
{
    private readonly HttpContext _httpContext;

    public ProfileService(IHttpContextAccessor httpContextAccessor)
    {
        _httpContext = httpContextAccessor.HttpContext;
    }

    ...

    public async Task IsActiveAsync(IsActiveContext context)
    {
        var temp = _httpContext.User; // breakpoint here
        // call external API with _httpContext.User info to get isActive
    }
}

However, the second request (GET) to the UserInfo endpoint does not authenticate. My breakpoint in IProfileService.IsActiveAsync() shows no user authenticated, so my routine for verifying if the user is active (calling out to another API) returns false which is translated to a 401. I can see this header on the failing GET request WWW-Authenticate: error="invalid_token".

I have tried specifying an IdentityTokenLifetime that is less than the AccessTokenLifetime per this with no success.

Here are the logs of the two requests:

Microsoft.AspNetCore.Hosting.Internal.WebHost:Information: Request starting HTTP/1.1 OPTIONS http://localhost:5000/connect/userinfo  
Microsoft.AspNetCore.Cors.Infrastructure.CorsService:Information: CORS policy execution successful.
Microsoft.AspNetCore.Hosting.Internal.WebHost:Information: Request finished in 8.5635ms 204 
Microsoft.AspNetCore.Hosting.Internal.WebHost:Information: Request starting HTTP/1.1 GET http://localhost:5000/connect/userinfo  
Microsoft.AspNetCore.Cors.Infrastructure.CorsService:Information: CORS policy execution successful.
Microsoft.AspNetCore.Cors.Infrastructure.CorsService:Information: CORS policy execution successful.
IdentityServer4.Hosting.IdentityServerMiddleware:Information: Invoking IdentityServer endpoint: IdentityServer4.Endpoints.UserInfoEndpoint for /connect/userinfo
IdentityServer4.Validation.TokenValidator:Error: User marked as not active: f84db3aa-57b8-48e4-9b59-6deee3d288ad
Microsoft.AspNetCore.Hosting.Internal.WebHost:Information: Request finished in 94.7189ms 401

Question:

How can I get the GET request to the UserInfo endpoint during the silent refresh to authenticate into HttpContext?

Update:

Adding screenshots of all of the headers of the two requests and the resulting browser cookies to investigate the answer by @Anders.

<code>OPTIONS</code> request to UserInfo endpoint <code>GET</code> request to UserInfo endpoint resulting cookies in browser

like image 288
Collin Barrett Avatar asked Jan 29 '26 10:01

Collin Barrett


2 Answers

Alright so I think I have an idea of what's going on. You make the silent refresh token request (which succeeds from what I can see from the redirect to oidc-silent-refresh.html) and the first ProfileService's 'IsActiveAsync' gets called (since it needs to go through the profile service to generate tokens for the silent refresh). You were able to see 'HttpContext.User' because an iframe was opened up which allowed all necessary cookies to be sent.

The user info preflight request didn't even make it to the ProfileService as you can see from the logs only having 'Invoking IdentityServer endpoint: IdentityServer4.Endpoints.UserInfoEndpoint for /connect/userinfo' shown once. Additionally the UserInfoEndpoint prevents any options request from going through (which you can verify to be true in the beginning of the 'ProcessAsync' method here).

Now when you make the actual user info request you're trying to get information from the HttpContext (which is usually populated using information from the cookie), but it's inaccessible because no cookies are sent in the request. The request is made here in the 'getJson' method (from the oidc-client-js library), and you can see that no cookies are sent in the request (the 'withCredentials' property would be set to true on the request if they were).

So how do we get the necessary information about the user? To get this information we can refer to the 'context' passed into the ProfileService's 'IsActiveAsync' that contains a principal populated by the access token claims (this process can be seen in the 'ValidateAccessTokenAsync' method here).

like image 174
Randy Avatar answered Jan 30 '26 23:01

Randy


The request to /connect/userinfo is authenticated by a session authentication cookie in the IdentityServer domain/path.

My guess is that the cookie is properly included in the OPTIONS request but not in the subsequent GET request. That is something you can verify in the browser dev tools by looking at the request.

If my guess is right, the reason is probably a samesite attribute. All auth cookies in ASP.NET Core (which IdentityServer4 uses) have a samesite attribute by default to prevent Cross Site Request Forgery attacks.

According to the information I can find a cookie with samesite=lax is not allowed from an AJAX Get request. I cannot find anything however if it is allowed in the OPTIONS request. You can verify (using the browswer dev tools) if there is a samesite setting in the cookie header in the response from the first request to /connect/authorize.

The setting itself is in the Cookie section of the cookie options in the call to AddCookie(). The MS docs says that it defaults to lax.

On the other hand, there is a GitHub thread in the Idsrv4 repo which says that they've changed the default to "none" for the Idsrv4 session cookie.

I'm obviously guessing a bit here, but it should be fairly simple to verify my assumptions as outlined above.

like image 23
Anders Abel Avatar answered Jan 30 '26 23:01

Anders Abel



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!