Per B
Per B

Reputation: 321

Best practices refreshing access tokens in a MVC Core App containing both views and resources, using Identity Server 4

I have been reading up (and watching videos) on how to best implement access and refresh tokens when using a MVC Core application with a lot of ajax call in it. I think I got it right but just wanted to know if there is a better way of doing this. I will edit this post so It can serve as a reference for anyone looking for this information.

My setup: I have a MVC Core application with a lot of JavaScript. The JavaScripts are using ajax calls to retrieve json or call actions.

Since I don't want my users to be able to access my api using cookie authentication, I'm using app.Map to split my application in two parts. One where the users can access Views using the identity token and one that will require a access token. I'm also adding a cookie to hold the time when I need to refresh my access token.

Startup.cs (I removed the parts that's not importent)

  app.UseCookieAuthentication(new CookieAuthenticationOptions
  {
    AuthenticationScheme = "Cookies",
    AutomaticAuthenticate = true,
    ExpireTimeSpan = TimeSpan.FromMinutes(60)
  });

  JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();

  var oidcOptions = new OpenIdConnectOptions
  {
    AuthenticationScheme = "oidc",
    SignInScheme = "Cookies",

    Authority = LoginServerUrl,
    RequireHttpsMetadata = false,
    ClientId = "MyApp",
    ClientSecret = "*****",
    ResponseType = "code id_token",
    SaveTokens = true,
    Events = new OpenIdConnectEvents()
    {
      OnTicketReceived = async notification =>
      {
        notification.Response.Cookies.Append("NextAccessTokenRefresh", DateTime.Now.AddMinutes(30).ToString());
        notification.Response.Cookies.Delete("AccessToken");
      },
    },

    TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters
    {
      NameClaimType = JwtClaimTypes.Name,
      RoleClaimType = JwtClaimTypes.Role,
    },
  };

  oidcOptions.Scope.Clear();
  oidcOptions.Scope.Add("openid");
  oidcOptions.Scope.Add("roles");
  oidcOptions.Scope.Add("offline_access");

  app.UseOpenIdConnectAuthentication(oidcOptions);

  app.Map("/api", (context) =>
  {
    var bearerTokenOptions = new IdentityServerAuthenticationOptions
    {
      AuthenticationScheme = "Bearer",
      Authority = LoginServerUrl,,
      RequireHttpsMetadata = false,
      ScopeName = "MyApi",
      AutomaticAuthenticate = true
    };

    context.UseIdentityServerAuthentication(bearerTokenOptions);
    context.UseMvcWithDefaultRoute();
  });

All ajax calls to controller actions are done using the following url /api/[Controller]/[Action].

I do not want my api to be accessible using the identity token so I also add Authorize(ActiveAuthenticationSchemes = "Bearer") attribute to the controller action. So now my controller actions that are getting called by javascript looks like this:

[HttpPost, Authorize(ActiveAuthenticationSchemes = "Bearer")]
public async Task<JsonResult> DoStuff()
{
}

When a javascript needs to access a api resource, the controler first retrives the access token and injects it into the javascript using a custom javascript init method.

This C# method is responsible for refreshing and retriving the access cookie.

public async Task<string> GetAccessTokenAsync()
{
  var accessToken = _contextAccessor.HttpContext.Request.Cookies["AccessToken"];
  var nextAccessTokenRefresh = _contextAccessor.HttpContext.Request.Cookies["NextAccessTokenRefresh"];
  if (string.IsNullOrEmpty(nextAccessTokenRefresh) || string.IsNullOrEmpty(accessToken) || DateTime.Parse(nextAccessTokenRefresh) <= DateTime.Now)
  {
    var refreshToken = await _contextAccessor.HttpContext.Authentication.GetTokenAsync("refresh_token");
    var tokenClient = new TokenClient(_appSettings.LoginServerUrl + "/connect/token", _appSettings.LoginClientId, _appSettings.LoginClientSecret);
    var response = await tokenClient.RequestRefreshTokenAsync(refreshToken);
    accessToken = response.AccessToken;

    //Set cookies for next refresh
    _contextAccessor.HttpContext.Response.Cookies.Append("NextAccessTokenRefresh", DateTime.Now.AddMinutes(30).ToString());
    _contextAccessor.HttpContext.Response.Cookies.Append("AccessToken", response.AccessToken);
  }

  return accessToken;
}

On all my $.ajax I have added the following parameter:

beforeSend: function(xhr, settings) { xhr.setRequestHeader('Authorization','Bearer ' + accessToken); }

Thats it. The default access token expiration is one hour. I always refresh it after half that time.

Now to my questions:

  1. Can I improve my code in any way ?
  2. Do you see any security related risks doing it this way ?
  3. Can I retrieve the access token in the OnTicketReceived ?

Upvotes: 8

Views: 2477

Answers (1)

adem caglin
adem caglin

Reputation: 24083

Can I improve my code in any way ?

Your Startup.cs should be something like this(because Map works for only '/api' path. For more info see https://docs.asp.net/en/latest/fundamentals/middleware.html#run-map-and-use):

app.MapWhen(context => !context.Request.Path.Value.StartsWith("/api"), builder=>
{
    app.UseCookieAuthentication(options);
    ...
    app.UseOpenIdConnectAuthentication(oidcOptions);
    ....
});

app.MapWhen(context => context.Request.Path.Value.StartsWith("/api"), builder=>
{
    var bearerTokenOptions = new IdentityServerAuthenticationOptions
    {
      AuthenticationScheme = "Bearer",
      Authority = LoginServerUrl,,
      RequireHttpsMetadata = false,
      ScopeName = "MyApi",
      AutomaticAuthenticate = true
    };

    context.UseIdentityServerAuthentication(bearerTokenOptions);
    context.UseMvcWithDefaultRoute();
});

Second point, your cookie expire time is 60 minutes, in this case your refresh token's lifetime will be 60 minutes. I think this may be a problem.

Do you see any security related risks doing it this way ?

I haven't got enough experience for using refresh tokens, so i can't say that your implementation is secure or not. But i think refresh token(for your implementation) increases complexity also security risks(this is just my opinion and i am not a security expert). I would use only long lived access token with implicit flow(because of simplicity).

Can I retrieve the access token in the OnTicketReceived ?

Yes, you can:

OnTicketReceived = ctx =>
{
     var token = ctx.Ticket.Properties.GetTokenValue("access_token");
     ...
}

Upvotes: 3

Related Questions