Introduction
Refresh tokens are an important part of the OAuth and OpenID Connect protocols, which are used to authenticate and authorize users in web and mobile applications. Refresh tokens are issued to the client application along with an access token, which is used to authenticate requests to protected resources. The access token has a limited lifespan, usually a few hours, and when it expires, the client application can use the refresh token to obtain a new access token.
Refresh tokens are important because they allow the client application to continue accessing protected resources on behalf of the user, without the user having to re-enter their login credentials. This helps to improve the user experience and reduce the risk of unauthorized access to protected resources. Refresh tokens are typically long-lived, with a lifespan of several weeks or months, and are stored securely by the client application. They can be revoked by the user or the authorization server at any time, which means that the client application must be prepared to handle the case where a refresh token is no longer valid.
Configure the use of Refresh Tokens
Here is an example of how you can configure refresh tokens in a C# application using the OpenIDDict library:
First, you will need to install the OpenIDDict NuGet package in your project. You can do this using the following command in the Package Manager Console:
dotnet add package OpenIddict.AspNetCore
Next, you can configure the refresh token flow in your Program.cs file by adding the following code after creating the webapplication builder instance:
builder.Services.AddOpenIddict()
.AddCore(options =>
{
// Configure OpenIddict to use the Entity Framework Core stores and entities.
options.UseEntityFrameworkCore()
.UseDbContext<ApplicationDbContext>();
})
.AddServer(options =>
{
// Enable the authorization, token, and refresh token endpoints.
options.SetAuthorizationEndpointUris("/connect/authorize")
.SetTokenEndpointUris("/connect/token")
.SetRefreshTokenEndpointUris("/connect/token/refresh");
// Enable the password flow.
options.AllowPasswordFlow();
// Accept token responses that are sent as form-encoded data.
options.AcceptFormEncodedResponse = true;
// Enable the refresh token flow.
options.UseRefreshTokens();
});
(Here we assume that the application uses Entity Framework Core for communication with the database and the configuration for that is correctly in place.)
This code configures OpenIDDict to use the Entity Framework Core stores and entities, and enables the authorization, token, and refresh token endpoints. It also enables the password flow and the refresh token flow, and sets the endpoint URIs for each flow.
Finally, you can add the following code in the Configure method to enable the refresh token flow in the OAuth2 middleware:
app.UseOAuthValidation();
app.UseOpenIddict();
With these changes, your C# application should be able to issue and refresh access tokens using the OpenIDDict library.
Configure it to use reference tokens for refresh tokens
To configure OpenIDDict to use reference tokens for refresh tokens, you can add the following code in the configure services section of your Program.cs file:
builder.Services.AddOpenIddict()
.AddCore(options =>
{
// Omitted code for brevity
})
.AddServer(options =>
{
// Omitted code for brevity
// Enable the refresh token flow.
options.UseRefreshTokens(new OpenIddictServerOptions.RefreshTokenFormatOptions
{
// Use reference tokens for refresh tokens.
UseReferenceTokens = true
});
});
This code configures OpenIDDict to use reference tokens for refresh tokens by setting the UseReferenceTokens
property to true
. Reference tokens are tokens that are stored in the authorization server and are associated with a unique identifier. When a client application wants to refresh an access token, it sends the refresh token identifier to the authorization server, which uses it to look up the corresponding reference token and issue a new access token.
Configure it to use reference tokens for access tokens too
To configure OpenIDDict to use reference tokens for both access and refresh tokens, you can add the following code in the configure services method of your Program.cs file:
builder.Services.AddOpenIddict()
.AddCore(options =>
{
// Omitted code for brevity
})
.AddServer(options =>
{
// Omitted code for brevity
// Use reference tokens for access tokens.
options.UseReferenceAccessTokens();
});
This code configures OpenIDDict to use reference tokens for both access and refresh tokens by setting the UseReferenceTokens property to true in the UseRefreshTokens method and by calling the UseReferenceAccessTokens method. Reference tokens are tokens that are stored in the authorization server and are associated with a unique identifier. When a client application wants to refresh an access token or access a protected resource, it sends the token identifier to the authorization server, which uses it to look up the corresponding reference token and issue a new access token or return the requested resource.
By using reference tokens for both access and refresh tokens, you can improve the security of your OAuth2 implementation, as the actual tokens are not stored on the client side and are less likely to be compromised. However, it is important to note that reference tokens do have some limitations, such as the need to store the tokens in a secure, centralized location, and the need to handle token revocation properly.
Set Introspection Endpoint
When you use reference tokens as access tokens, they need to be introspected against the issuer, to check the validity of the token.
To configure an introspection endpoint in an OpenIDDict-based OAuth2 implementation, you can add the following code in the configure services section of your Program.cs file:
builder.Services.AddOpenIddict()
.AddCore(options =>
{
// Omitted code for brevity
})
.AddServer(options =>
{
// Omitted code for brevity
// Enable the introspection endpoint.
options.SetIntrospectionEndpointUris("/connect/introspect");
});
This code configures OpenIDDict to enable the introspection endpoint and sets the endpoint URI to /connect/introspect. The introspection endpoint is an OAuth2-defined endpoint that allows a client application to verify the status and validity of an access token. When a client application sends an access token to the introspection endpoint, the authorization server responds with a JSON object that contains information about the token, such as its expiration time, the associated client application, and the scope of the associated resources.
By enabling the introspection endpoint, you can allow client applications to verify the status of an access token before making a request to a protected resource, which can help to improve the security of your OAuth2 implementation. However, it is important to note that the introspection endpoint requires secure communication and should be protected by TLS/SSL.
Last thing to configure, is to override the introspection server event to include the token payload in the response for correct authorization at the API level:
builder.Services.AddOpenIddict()
.AddCore(options =>
{
// Omitted code for brevity
})
.AddServer(options =>
{
// Omitted code for brevity
options.AddEventHandler<OpenIddictServerEvents.ApplyIntrospectionResponseContext>(
o => o.UseScopedHandler<AuthServerApplyIntrospectionResponse>());
});
internal sealed class
AuthServerApplyIntrospectionResponse : IOpenIddictServerHandler<OpenIddictServerEvents.ApplyIntrospectionResponseContext>
{
private readonly OpenIddictTokenManager<AuthServerToken> _tokenManager;
public SosicredApplyIntrospectionResponse( OpenIddictTokenManager<AuthServerToken> tokenManager )
{
_tokenManager = tokenManager;
}
public async ValueTask HandleAsync( OpenIddictServerEvents.ApplyIntrospectionResponseContext context )
{
if ( context.Request?.Token is not null ) // Bonus part. Try to figure it out on your own and leave a comment.
{
if ( await _tokenManager.FindByReferenceIdAsync(context.Request.Token).ConfigureAwait(false) is not
{
ExpirationDate: { }
} authServerToken ) return;
if ( authServerToken.Type == OpenIddictConstants.TokenTypeHints.AccessToken )
if ( authServerToken.ExpirationDate.Value <= DateTime.UtcNow.AddMinutes(15) )
{
authServerToken.ExpirationDate = authServerToken.ExpirationDate.Value.AddMinutes(30);
await _tokenManager.UpdateAsync(authServerToken).ConfigureAwait(false);
}
context.Response.AddParameter("token_payload", new OpenIddictParameter(authServerToken.Payload));
}
}
}
Configure you API to correctly introspect reference tokens
At you API side, you need following authentication configuration to correctly distinguish between reference tokens and normal JWT tokens, and in case of a reference token, introspect the token against the authorization server for checking the validity and getting user information.
Add the following package reference to your csproj file:
<PackageReference Include="IdentityModel.AspNetCore.AccessTokenValidation" Version="1.0.0-preview.3" />
Then insert the following code in the configure services section of your Program.cs:
builder.Services.AddOptions<JwtBearerOptions>().Configure(o => ConfigureJwtOptions(o, configuration));
builder.Services.AddAuthentication()
.AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, o => ConfigureJwtOptions(o, configuration))
.AddOAuth2Introspection(
"introspection", o => {
o.Authority = new Uri(configuration["PublicHost"]!).ToString();
o.SkipTokensWithDots = true;
o.SaveToken = true;
o.Events = new OAuth2IntrospectionEvents
{
OnTokenValidated = context => {
var tokenPayload = context.Principal?.FindFirstValue("token_payload");
if ( !string.IsNullOrWhiteSpace(tokenPayload) ) ExtractClaimsFromTokenPayload(context, tokenPayload);
return Task.CompletedTask;
}
};
});
private static void ConfigureJwtOptions( JwtBearerOptions o, IConfiguration configuration )
{
o.Authority = new Uri(configuration["PublicHost"]!).ToString();
o.ClaimsIssuer = new Uri(configuration["PublicHost"]!).ToString();
o.SaveToken = true;
o.TokenValidationParameters = new TokenValidationParameters
{
NameClaimType = OpenIddictConstants.Claims.Subject,
RoleClaimType = OpenIddictConstants.Claims.Role,
ValidIssuer = new Uri(configuration["PublicHost"]!).ToString(),
ValidateIssuer = true,
ValidateIssuerSigningKey = true,
TokenDecryptionKey = new X509SecurityKey(
new X509Certificate2(Resources.encryption_certificate, configuration["ENCRYPTION_KEY_PASSWORD"])),
IssuerSigningKey = new X509SecurityKey(
new X509Certificate2(Resources.signing_certificate, configuration["SIGNING_KEY_PASSWORD"])),
ValidateAudience = true,
ValidAudiences = new[] { "authserver" }
};
o.Events = new JwtBearerEvents
{
OnAuthenticationFailed = context => {
Log.Information(context.Exception, "Failed authentication by bearer token.");
return Task.CompletedTask;
}
};
// if token does not contain a dot, it is a reference token
o.ForwardDefaultSelector = Selector.ForwardReferenceToken("introspection");
}
private static void ExtractClaimsFromTokenPayload(
IdentityModel.AspNetCore.OAuth2Introspection.TokenValidatedContext context, string tokenPayload )
{
var jwtOptions = context.HttpContext.RequestServices.GetRequiredService<IOptions<JwtBearerOptions>>().Value;
foreach (var validator in jwtOptions.SecurityTokenValidators)
{
if ( !validator.CanReadToken(tokenPayload) ) continue;
try
{
context.Principal = validator.ValidateToken(tokenPayload, jwtOptions.TokenValidationParameters, out _);
break;
}
catch (Exception ex)
{
if ( jwtOptions is { RefreshOnIssuerKeyNotFound: true, ConfigurationManager: { } }
&& ex is SecurityTokenSignatureKeyNotFoundException ) jwtOptions.ConfigurationManager.RequestRefresh();
}
}
}
Congratulation! With this setup, you now should have a completely working authentication server that supports reference tokens.
Evaluation and conclusion
I was able to gain 77X lower data being sent over the wire in each API request just by using reference tokens. Original JWT tokens (shown in the picture below) were around 3.2KB:
While the equivalent reference token is only 44B. With this improvement, your servers must be able to handle more parallel requests, therefore the whole service is reachable to more users concurrently.
However, performance is always subjective to the use case. Even though this improvement reduce the amount of data being sent over the wire in each API request, it increases background API calls for introspecting tokens which also requires database call. In the context of SaaS, server resources are often assumed to be very high and cheap whereas client resources are considered limited and more precious, therefore such pitfalls must be tolerable.
Mohammadali Forouzesh - A passionate software engineer
06/11/2022
LinkedIn - Instagram
Top comments (1)
Hi, what needs to be changed to make it work on cookies?