Skip to content
TDACorpIDENTITYSign in

Refresh token

When you get a refresh token, how long it lives, how rotation, reuse and the retry grace behave, and how to store and use one safely.

On this page

A refresh token lets your application get new access tokens without sending the person through sign-in again. It is opaque: not a JWT, nothing to read, nothing to verify. You store it and present it to the token endpoint.

When you get one#

On the code exchange, when both are true:

  • the client is registered to hold the refresh_token grant, and
  • the request asked for the offline_access scope.

A client_credentials token never has a refresh token, because there is no person to refresh for. The consent screen describes offline_access to the person as letting the app stay signed in without asking for the password again, ended at any time from Connected apps in their account.

Lifetime#

A refresh token has a sliding window and an absolute end.

Sliding windowAbsolute end
First-party clientThe client's registered lifetime. A newly registered client gets 30 days.The end of the sign-in session.
Third-party clientAt most 7 days, and never longer than registeredAt most 30 days from issue, and never past the session

An operator can shorten both third-party limits for the whole deployment. Each successful refresh moves the sliding window forward. The absolute end never moves. The sign-in session lasts at most 14 days by default, so a refresh token can never outlive the session it came from; see Sessions and tokens. Once the session ends, or the person signs out everywhere or revokes your application, the token stops working.

Rotation#

Every use returns a new refresh token and retires the one you sent. Store the new one before you discard the old one.

curl -s https://identity.tdacorp.in/token \  -H "DPoP: $DPOP_PROOF" \  --data-urlencode 'grant_type=refresh_token' \  --data-urlencode 'client_id=tdaid_c_example' \  --data-urlencode "client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer" \  --data-urlencode "client_assertion=$ASSERTION" \  --data-urlencode "refresh_token=$REFRESH_TOKEN"

The response has a new access_token, a new refresh_token, expires_in and scope. You may send scope to ask for fewer scopes than the token holds, never more, and resource to choose the audience. The scope that comes back is what you asked for.

The retry grace#

A refresh can succeed on identity and the reply be lost on the way to you. So a token that has just been used still works for 10 seconds (an operator can shorten this): if you send the retired token again inside that window, identity returns the same new token it returned the first time, and rotates nothing. This covers a dropped response and a client retry. It is not a way to run two refreshes at once, and nothing about it extends past ten seconds.

Reuse#

After the grace has passed, a retired refresh token is dead. If one is presented again, identity cannot tell your retry from a thief using a copy, so it assumes the worst.

  • The whole family of tokens that came from that sign-in is revoked, including the newest.
  • The sign-in session the family came from ends.
  • Identity can take further protective steps on the person's account.
  • Your client sees invalid_grant, with the same description as for any invalid token. You cannot tell reuse from expiry, on purpose.

For you the practical rule is: serialize refreshes per token, and do not count on the grace window. If two requests in your app both find an expired access token, only one of them may refresh. Let the other wait for the result.

Binding to a DPoP key#

A refresh token issued to a client that uses DPoP is bound to the thumbprint of that client's key. Present it with a proof from the same key, or the answer is invalid_dpop_proof and the token is not spent. Persist the key with the token. See DPoP.

When a refresh fails#

errorWhat it meansWhat to do
invalid_grantUnknown, expired, revoked or reused token; the session ended; the person or organization is no longer active; the session is signed in through a single sign-on connection that is no longer linked to the token's organization; or the client lost the scopes the token needs.Stop. Send the person through sign-in. Do not retry the same token.
invalid_targetThe resource's product is no longer installed for the token's organization.Stop asking for that resource.
insufficient_user_authenticationThe organization requires a recent single sign-on authentication (behind the sso_enforcement flag, which defaults OFF) and the session has none. The response is a 400 with acr_values and max_age.Send the person through a new authorization with those values. Do not retry the same token.
invalid_scopeYou asked for a scope the token does not hold.Ask for no more than it holds.
invalid_dpop_proofThe proof key is not the key the token is bound to.Use the stored key.
use_dpop_nonceThe proof needs the server's current nonce.Retry once with the DPoP-Nonce header.
invalid_clientClient authentication failed.Check your assertion and key.

A 429 with Retry-After is not a failure of the token. Wait, and use the same token again.

Revoke it#

Call the revocation endpoint when the person signs out of your application. The SDK's rp.revoke(token) does it.

Security notes#

  • Store it like a password. Server side, encrypted at rest, never in a URL, a log or a browser script.
  • One owner per token. Do not share a refresh token between instances that refresh independently.
  • Persist the new token before you use the new access token. If you crash in between, the old token is already retired.
  • A lost new token is a lost session. If you did not store the new token and more than 10 seconds have passed, sign the person in again.

Verified against commit ca46b57