.Net Core 2 JWT Validation and Refreshing for access tokens

I have a web app using Auth0 (configured as regular web app) calling an API (also authorized with Auth0). I added the openid, profile, and email scopes when requesting and ID token. Following the tutorial here on Auth0, I am also getting an access token on user login by saving the token.

//startup.cs

 options.SaveTokens = true;
 options.Events = new OpenIdConnectEvents
                {
                    OnRedirectToIdentityProvider = context =>
                    {
                        context.ProtocolMessage.SetParameter("audience", "https://myapi/api");

                        return Task.FromResult(0);
                    };
}

I call my API using a typed httpclient. I initialize the base URI and authorization header by getting the access token from the httpcontext and calling the appropriate URI for the API.

 public RepositoryClient(HttpClient httpClient, IHttpContextAccessor httpContextAccessor)
        {
            _httpContextAccessor = httpContextAccessor;
            _httpClient = httpClient;

            var context = _httpContextAccessor.HttpContext;
            var token = context.GetTokenAsync("access_token").Result;

            if (token != null)
            {
                _httpClient.BaseAddress = new Uri("https://localhost:44335/");
                _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
            }
        }

I am able to access my API public and private methods correctly. In my API, the only validation is being done by the build in Jwt Middleware:

 string domain = $"https://{Configuration["Auth0:Domain"]}/";
            services.AddAuthentication(options =>
            {
                options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
                options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;

            }).AddJwtBearer(options =>
            {
                options.Authority = domain;
                options.Audience = Configuration["Auth0:ApiIdentifier"];
            });

Based on this setup, do I need to do any additional validation? I read that the ASP .NET Core built in JWT Middleware will decode, validate, and do all the heavy lifting needed. All that is needed in the configuration above. Is this right or do I need to do any additional validation?

Does the above configuration use the Authorization Code Grant or Client Credentials grant type? With authorization code grant, you need to get an authorization code to get a new access token. How would I do it in this case where the user is already logged in? The only way I could get a new access token was by using client credentials grant (and I had to also authorize my regular web app in the auth0 API in order to even request a new access token). How do I go about it without a client credentials grant or is that not possible? Should I instead be getting a refresh token and going that way (by enabling offline_scope)? My web app will only be used when you have internet access so wanted to clarify.

Also in my repository client, I am not checking to verify that the access token has not expired. Should I always be checking this? I.e., is the correct thing to do to extract the access token, store in a secure cookie, validate expiry, get a new access token, and update the cookie?

If anyone could help me with above questions, that would be really appreciated.

Correct. All the token validation is done by the JWT Bearer middleware. Note that a valid token implies that whoever presents the token (the client application) was given permission by the user to access resources on the user behalf. What that means, among other things, is that you still need to check that the user has access to the resource.
E.g. You might have a regular user that, through a client application, tries to access the “admin” part of the site. There will be a valid token issued, with that user as the “subject” of the token. It will pass the JWT validation, but your app will have to prevent that users from accessing the “admin” resources because this is just a regular user.

The OpenIdConnect middleware, on the regular web app, utilizes the Authorization Code Grant behind the scenes. This is the appropriate flow for regular web apps when a user is present. The “subject” of the token obtained is, as I mentioned before, the user who authenticated at the authorization server (Auth0).
The Client Credentials is used when the application itselft authenticates to the authorization server, and there is no user involved. The subject of the token is the application itself.
Think, for instance, of an application requesting a token to access a third-party SMS service like Twilio. The application that wants to use the SMS service would probably use the Client Credentials grant to obtain a token to call that service, no user involved in this case.

Assuming that you want a new access token because the old one has expired, there are two basic ways of doing that:

  • An interactive request (that is, you need the user to be present to follow any UI requirement like logging in again, giving consent, completing an MFA prompt, and so on). The interactive request is by redirecting the browser to the /authorize endpoint, which is exactly what the OpenIdConnect does when you issue the “Challenge” (as seen on the /Account/Login action). Note that if the user already has a valid session at the authorization server, there might not be any visible prompts at all.
  • A refresh token flow, that does not require the user to be present. With the refresh token, your application can request a new token by talking directly to Auth0 without the user or the browser being involved.

Use of one or the other method will vary depending on your application. Most web apps will be OK by doing an interactive token request once the token is expired, but others might have long-running background processes or other flows that require new tokens without involving the user.

Also in my repository client, I am not checking to verify that the access token has not expired. Should I always be checking this? I.e., is the correct thing to do to extract the access token, store in a secure cookie, validate expiry, get a new access token, and update the cookie?

An ideal client should be prepared to handle the possibility of an Unauthorized response from the backend API caused by an invalid token (either because the token has expired, was blacklisted, or any other reason) and, in that case, trigger the flow to get a new token (a refresh token flow might be less disruptive, but remember that refresh tokens can also be revoked).
Checking beforehand for an expired token could be though as an extra optimization, to avoid a request with a token that you know is no longer valid. Note that you should never inspect the access_token itself in the client, it should be seen as an obscure string. But the token response includes the token expiration, that you can use for this logic.
It is OK to store the refresh token in a secure cookie, as long as you take the necessary precautions.

Hope that helps clarify things a little.

1 Like

Thank you for the reply. I am checking the roles which are set based on app metadata on the user to limit what users can do on the client application.

Regarding your suggestion for the interactive request, I came up with below to not only validate the expiration time but also get a new access token (just copying data to ViewData for test purposes):

JwtSecurityTokenHandler handler = new JwtSecurityTokenHandler();
var decodedToken = handler.ReadJwtToken(accessToken);

if (DateTime.Now > decodedToken.ValidTo.ToLocalTime())
      {
          ViewData["expired"] = "expired";
          await HttpContext.ChallengeAsync("Auth0", new AuthenticationProperties() { RedirectUri = "/" });
          //below won't trigger due to redirecting user to login again
          string accessTokenNew = await HttpContext.GetTokenAsync("access_token") ?? "Empty new access token";
           ViewData["newaccess"] = accessTokenNew;
       }
 else
       {
          ViewData["newaccess"] = "new access token not set";
          ViewData["expired"] = "not expired " + decodedToken.ValidTo.ToLocalTime();
       }

This redirects the user to log in again every time the token is expired. In your above response you mentioned that if there is a valid session, there might not be any prompts at all. With a challenge, it will force a login. Is the only way to handle this without a forced login is to go the refresh token route when the user is already logged in?

I’m assuming the reason why the access token should not be stored in a cookie is due to users being able to access it. Is there a reason the refresh token can be stored in a cookie? Is it because you need the clientID and clientSecret to get a new access token? Wanted to make sure doing something like storing an access token in a session cookie is bad practice or not.

If you check “Enable Seamless SSO” in you tenant’s advanced settings, there will be no prompt if the user still has a session at Auth0. You can configure the duration of the session in that same dialog.

I’m assuming the reason why the access token should not be stored in a cookie is due to users being able to access it. Is there a reason the refresh token can be stored in a cookie? Is it because you need the clientID and clientSecret to get a new access token? Wanted to make sure doing something like storing an access token in a session cookie is bad practice or not.

The access token should not get to the browser in a regular web application where you have better alternatives like session cookies. (In SPA, the token goes to the browser because there’s no session at the server).

Now, by default ASP.Net Core use stateful cookies, so that they contain information about the user (claims). You can store the access token in there (as well as the refresh token) because the content of that cookie is encrypted by the framework (the cookie will be useless for anyone without the decryption key).
As a side note, ASP.Net Core can also store all that information in a server-side session store, and use the session cookie just to hold a reference to that session store, but this is an optional configuration of the Cookie middleware.
Now, client applications should not inspect or decode the access token (not even assume it’s a JWT token):

// don't do this, treat the access token as an obscure string
JwtSecurityTokenHandler handler = new JwtSecurityTokenHandler();
var decodedToken = handler.ReadJwtToken(accessToken);

Instead, you should use the information provided by the authorization server in the token response. The AS returns an expires_in value as part of the token response, that the ASP.Net OpenIdConnect middleware converts to an expires_at property that can be retrieved like this:


var expiresAt = DateTime.Parse(await HttpContext.GetTokenAsync("expires_at"), CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind);
2 Likes

Thank you. That helps a lot. I didn’t realize I should be accessing the expires_in as the response instead of decoding it. I was able to turn on SSO integration per your instructions and it worked. User does not have to sign in again.

I’ll look into using session variables to store tokens and work on the other approach you noted (using refresh token to get a new access token instead of interactive request). Thanks a lot for your help.

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.