Refresh tokens are credentials that grant an application the ability to obtain a new access token after the original expires — without requiring the user to re-authenticate.
- Contain the information needed to obtain a new
access_tokenorid_token - Subject to strict storage requirements — treat like a password
- Typically do not expire on their own; revoke them if a security issue arises
- Only issued for: Authorization Code and Authorization Code with PKCE
- Require the
offline_accessscope in the initial authorization request
Refresh Token Request (HTTP)
The OpenID Connect specification defines the refresh flow as a POST to the token endpoint with grant_type=refresh_token:
POST /token HTTP/1.1
Host: server.example.com
Content-Type: application/x-www-form-urlencoded
client_id=s6BhdRkqt3
&client_secret=some_secret12345
&grant_type=refresh_token
&refresh_token=8xLOxBtZp8
&scope=openid%20profileRefresh Token Response
A successful response includes a new access token and optionally a rotated refresh token:
HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: no-store
Pragma: no-cache
{
"access_token": "TlBN45jURg",
"token_type": "Bearer",
"refresh_token": "9yNOxJtZa5",
"expires_in": 3600
}Token Response Model
// Token response model
public class TokenResponse
{
public string AccessToken { get; set; }
public string RefreshToken { get; set; }
public int ExpiresIn { get; set; }
public string TokenType { get; set; }
}Exchange Authorization Code for Tokens
After the user authenticates and the authorization endpoint redirects with a code, exchange it for an access token and refresh token via the token endpoint:
// Exchange authorization code for access + refresh token (HttpClient, manual approach)
public async Task<TokenResponse> ExchangeCodeAsync(string authorizationCode)
{
using var client = new HttpClient();
var body = new FormUrlEncodedContent(new Dictionary<string, string>
{
["grant_type"] = "authorization_code",
["code"] = authorizationCode,
["redirect_uri"] = "https://myapp.com/callback",
["client_id"] = "my_client",
["client_secret"] = "my_secret",
});
var response = await client.PostAsync("https://is4server.com/connect/token", body);
var json = await response.Content.ReadAsStringAsync();
return JsonSerializer.Deserialize<TokenResponse>(json);
}Exchange Refresh Token for New Access Token
When the access token expires, send the refresh token to the same token endpoint withgrant_type=refresh_token to receive a fresh access token without user interaction:
// Exchange refresh token for a new access token
public async Task<TokenResponse> RefreshTokenAsync(string refreshToken)
{
using var client = new HttpClient();
var body = new FormUrlEncodedContent(new Dictionary<string, string>
{
["grant_type"] = "refresh_token",
["refresh_token"] = refreshToken,
["client_id"] = "my_client",
["client_secret"] = "my_secret",
});
var response = await client.PostAsync("https://is4server.com/connect/token", body);
var json = await response.Content.ReadAsStringAsync();
return JsonSerializer.Deserialize<TokenResponse>(json);
}Using IdentityModel Helpers
The IdentityModel NuGet package wraps the raw HttpClient calls. It discovers endpoint URLs from /.well-known/openid-configuration automatically:
// IdentityModel helpers — cleaner API over HttpClient
// NuGet: IdentityModel
// 1. Load the discovery document (populates endpoint URLs from /.well-known/openid-configuration)
using var httpClient = new HttpClient();
var disco = await httpClient.GetDiscoveryDocumentAsync("https://is4server.com");
if (disco.IsError) throw new Exception(disco.Error);
// 2. Exchange authorization code for tokens
var tokenResponse = await httpClient.RequestAuthorizationCodeTokenAsync(
new AuthorizationCodeTokenRequest
{
Address = disco.TokenEndpoint,
ClientId = "my_client",
ClientSecret = "my_secret",
Code = authorizationCode,
RedirectUri = "https://myapp.com/callback",
});
string accessToken = tokenResponse.AccessToken;
string refreshToken = tokenResponse.RefreshToken;
// 3. Later: exchange the refresh token for a new access token
var refreshResponse = await httpClient.RequestRefreshTokenAsync(
new RefreshTokenRequest
{
Address = disco.TokenEndpoint,
ClientId = "my_client",
ClientSecret = "my_secret",
RefreshToken = refreshToken,
});
string newAccessToken = refreshResponse.AccessToken;