17

I'm using .net core with IdentityServer 4. I have a Web api, and an MVC app which accesses secure endpoints on the api. It's very similar in setup to the IdentityServer quickstart:

https://github.com/IdentityServer/IdentityServer4.Samples/tree/release/Quickstarts/6_AspNetIdentity

I'm finding that my access_tokens are expiring, and I'd like to understand how to renegotiate refresh_tokens.

Take the following code for example (taken from the quickstart here):

public async Task<IActionResult> CallApiUsingUserAccessToken()
    {
        var accessToken = await HttpContext.Authentication.GetTokenAsync("access_token");

        var client = new HttpClient();
        client.SetBearerToken(accessToken);
        var content = await client.GetStringAsync("http://localhost:5001/identity");

        ViewBag.Json = JArray.Parse(content).ToString();
        return View("json");
    }

If the access_token has expired, it will fail with 401 response. Is there a built-in mechanism for re-negotiating the access_token using the refresh_token?

Stephen Ellis
  • 2,561
  • 2
  • 24
  • 47

1 Answers1

22

There is not a build in system to refresh the access_token. However you can use the IdentityModel package to request a new access_token with a refresh_token.

The Client has a property AllowOfflineAccess which you should set to true in the IdentityServer. Note that this does not work for the implicit/client credentials flow.

  • Always refresh the access_token prior to making the call to the protected resource
  • Check if the current access_token is about to expire by checking its lifetime and request a new access_token with the refresh_token (personal preference)
  • Wait for the API to return the 401 and request a new access_token with the refresh_token

Prior to this code you can check the access_token lifetime and/or wrap this code in a service before you request a new access_token

    var discoveryResponse = await DiscoveryClient.GetAsync("IdentityServer url");
    if (discoveryResponse.IsError)
    {
        throw new Exception(discoveryResponse.Error);
    }
    
    var tokenClient = new TokenClient(discoveryResponse.TokenEndpoint, "ClientId", "ClientSecret");
    // This will request a new access_token and a new refresh token.
    var tokenResponse = await tokenClient.RequestRefreshTokenAsync(await httpContext.Authentication.GetTokenAsync("refresh_token"));
    
    if (tokenResponse.IsError)
    {
        // Handle error.
    }
    
    var oldIdToken = await httpContext.Authentication.GetTokenAsync("id_token");
    
    var tokens = new List<AuthenticationToken>
    {
        new AuthenticationToken
        {
            Name = OpenIdConnectParameterNames.IdToken,
            Value = oldIdToken
        },
        new AuthenticationToken
        {
            Name = OpenIdConnectParameterNames.AccessToken,
            Value = tokenResult.AccessToken
        },
        new AuthenticationToken
        {
            Name = OpenIdConnectParameterNames.RefreshToken,
            Value = tokenResult.RefreshToken
        }
    };
    
    var expiresAt = DateTime.UtcNow.AddSeconds(tokenResult.ExpiresIn);
    tokens.Add(new AuthenticationToken
    {
        Name = "expires_at",
        Value = expiresAt.ToString("o", CultureInfo.InvariantCulture)
    });
    
    // Sign in the user with a new refresh_token and new access_token.
    var info = await httpContext.Authentication.GetAuthenticateInfoAsync("Cookies");
    info.Properties.StoreTokens(tokens);
    await httpContext.Authentication.SignInAsync("Cookies", info.Principal, info.Properties);

Taken from and slightly modified: Source

Antonio Correia
  • 1,093
  • 1
  • 15
  • 22
user1336
  • 6,435
  • 2
  • 27
  • 34
  • 3
    "Always refresh the access_token prior to making the call to the protected resource" - this is not always necessary and kinda defeats one of the points of refresh-tokens (that is, to reduce the burden on authentication servers) by eliminating the need for every request to be re-authenticated. – Dai Jan 31 '19 at 04:26
  • TokenClient is obsolete, could you have an example with the new method? – Zaha May 22 '19 at 21:41
  • 3
    @JuanPablo Now you could use like that `var response = await client.RequestRefreshTokenAsync(new RefreshTokenRequest { Address = discoveryResponse.TokenEndpoint, ClientId = "mvc", ClientSecret = "secret", Scope = "my api", RefreshToken = refreshToken });` – Matheus Ferreira Jun 05 '19 at 20:04
  • And you also can find the discovery with the HttpClient `var discoveryResponse = await client.GetDiscoveryDocumentAsync("http://localhost:5500");` – Matheus Ferreira Jun 05 '19 at 20:05
  • 26
    For anyone wondering how to get the actual refresh token, when you first authenticate, you need to add `offline_access` to your scopes when you make the request. When you do that, the refresh_token will come back along with the `access_token`, `expires_in`, and `token_type`. – Eric Garrison Jun 20 '19 at 15:19
  • @EricGarrison That was exactly what I was desperately looking for. Thatswhy +1 – Andreas H. Sep 04 '19 at 09:05
  • I'm using "hybrid" grant type and when I make this call with Scope = "myapi offline_access", I'm getting an error returned "invalid_grant". I have myapi and offline_access as scopes for my clientId and AllowOfflineAccess = true in identity. – ScubaSteve Mar 20 '21 at 18:44
  • @EricGarrison: it has to be `expires_at` instead of `expires_in` – markus s Jun 01 '23 at 11:23