3

Environment:

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

Collin Barrett
  • 2,441
  • 5
  • 32
  • 53
  • What library are you using to make the request? Are you sure any options that relate to cookies are being sent? The options request is typically made by the browser without any aid (or influence) from an API call so even though there were headers in the options request doesn't mean there will be in subsequent requests – Randy Feb 26 '19 at 03:56
  • We are using [oidc-client-js](https://github.com/IdentityModel/oidc-client-js) to send the requests. To your second question, I'm really not sure, but will investigate. Thanks. – Collin Barrett Feb 26 '19 at 13:07

2 Answers2

2

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.

Anders Abel
  • 67,989
  • 17
  • 150
  • 217
  • Awesome. Still dissecting your suggestions. Just added screenshots of the two requests to the end of the question. – Collin Barrett Feb 20 '19 at 17:02
  • Hmm, using Fiddler I see "This request did not send any cookie data." for both requests to the UserInfo endpoint. I also see no value for `SameSite` in the `idsrv.session` cookie (see the last screenshot of the cookies in the question). Any other ideas? Thanks. – Collin Barrett Feb 20 '19 at 17:24
  • 1
    I see that there is no samesite value. But then the cookies should be included in both requests as headers... So I'm lost here. – Anders Abel Feb 20 '19 at 17:30
2

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).

Randy
  • 1,212
  • 8
  • 14