OpenIddict - Refresh Token Flow [EP.3]
Intro
In the previous blogs we covered the Client Credentials Flow and the Password Flow, and the two of them authorize in different ways. In this blog we'll talk about another flow that you could call an extension of the other flows: the Refresh Token Flow.
The Refresh Token Flow is one of the authorization flows. It takes the Refresh Token you got when you authorized, the one that came together with the Access Token, and exchanges it for a new Access Token and Refresh Token once the old Access Token expires. That way the user doesn't have to log in and type their credentials again.
Configuring OpenIddict
OpenIddict already ships with the Refresh Token Flow, so configuring it is just a matter of allowing the server to use the flow in the OpenIddict Server configuration.
builder.Services.AddOpenIddict()
.AddCore(options =>
{
// other core configure
})
.AddServer(options =>
{
options.SetTokenEndpointUris("/connect/token");
options.SetRefreshTokenLifetime(TimeSpan.FromDays(1));
options.SetAccessTokenLifetime(TimeSpan.FromDays(30));
options.AllowPasswordFlow()
.AllowRefreshTokenFlow();
// other configuration
})
That's all it takes for the Authorization Server to support the Refresh token flow.
Offline Access
To request a Refresh Token under the OpenID Connect standard, the user has to give consent to access their data first (accepted consent). Normally, with the Authorization Code Flow, the user is redirected to a Consent page after authorizing, which shows what data the Application wants from the system and asks whether the user agrees.
But some Authorization Flows, like the Password Flow, have no step for accepting consent on the Authorization Server. So to get a Refresh Token we have to put scope=offline_access in the request, and the Authorization Server has to grant the "offline_access" scope in the user's claims as well, once the Sign-in has succeeded.
// after sign-in succeeded
var identity = new ClaimsIdentity(
authenticationType: TokenValidationParameters.DefaultAuthenticationType,
nameType: Claims.Name,
roleType: Claims.Role);
identity.SetClaim(Claims.Subject, await userManager.GetUserIdAsync(user))
.SetClaim(Claims.Email, await userManager.GetEmailAsync(user))
.SetClaim(Claims.Name, await userManager.GetUserNameAsync(user))
.SetClaims(Claims.Role, (await userManager.GetRolesAsync(user)).ToImmutableArray());
// Set the list of scopes granted to the client application.
identity.SetScopes(new[]
{
Scopes.OpenId,
Scopes.Email,
Scopes.Profile,
Scopes.Roles,
Scopes.OfflineAccess,
}.Intersect(scopes));
identity.SetDestinations(GetDestinations);
After the Sign-in comes the part where we build the claim identity, and this is where we have to set the scopes the client is allowed to use.
Request token
curl -X POST \
'https://{your-token-endpoint}' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'username={username}' \
--data-urlencode 'password={password}' \
--data-urlencode 'grant_type=password' \
--data-urlencode 'scope=offline_access'
Example

As you can see, the server hands out a refresh token together with the access token.
Token Endpoint for Refresh Token
Now that we have a Refresh Token, our token endpoint needs to be able to exchange it for a new set of tokens. OpenIddict does the Authentication for us here: if the Authentication passes, we get the user's Identity back (if it doesn't, it's probably because the refresh token has expired, or some other reason).
app.MapPost("/connect/token", async (HttpContext context, SignInManager<IdentityUser> signInManager, UserManager<IdentityUser> userManager) =>
{
var request = context.GetOpenIddictServerRequest() ?? throw new Exception("User context not found.");
if (request.IsPasswordGrantType())
{
// handled password flow here.
}
else if (request.IsRefreshTokenGrantType())
{
var claimsPrincipal = (await context.AuthenticateAsync(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme)).Principal;
if (claimsPrincipal is not null)
{
return Results.SignIn(claimsPrincipal, null, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
}
}
throw new NotImplementedException("The specified grant type is not implemented.");
});
Re-issued from Refresh token
curl -X POST \
'https://{your-token-endpoint}' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'refresh_token={your-refresh-token}' \
--data-urlencode 'grant_type=refresh_token'
Example

As you can see, the refresh token we sent in gets exchanged for a brand new set of tokens. That's it, the user's session can now live longer without asking the user for their login credentials again after the access_token expires.
Ads
Thanks for reading all the way down here. If you want the sample source code, you can check it out at the Github repository
Also, if you'd like to follow my work you can follow my personal Github, or hit the yellow button below to cheer me on with some beer coffee money for the next piece.
Found this useful? You can support the blog with a coffee.
Buy me a coffee