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:
The authorization server can contain this risk by detecting refresh token reuse using refresh token rotation. If your application uses refresh token rotation, it can now store it in local storage or browser memory. You can use a service like Auth0 that supports token rotation.
This means that even if an authorisation consent is, say, 90 days, the refresh token must be used within a 30-day window in order to “refresh” the access — at which point the refresh token lifetime is reset once again. This is all well and good.
The most secure option is for the authorization server to issue a new refresh token each time one is used. This is the recommendation in the latest Security Best Current Practice which enables authorization servers to detect if a refresh token is stolen.
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");
...
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With