Hello!
I’ve spent the afternoon figuring out how GPT Actions work with OAuth refresh tokens, expiries, and in particular how to do token revocation, since this isn’t documented anywhere. This guide assumes you have already implemented OAuth with non-expiring tokens and are interested in understanding either (a) when ChatGPT revokes those tokens, or (b) how ChatGPT handles expiries. I will not explain how to implement refresh tokens yourself (there are many guides for this).
Why should I use refresh tokens?
Two reasons.
First, ChatGPT cannot programatically revoke a token on demand.
If you experience a data breach or another security issue, there is no current way to log your users out of your GPT Action (there are often more mundane reasons to revoke tokens, too, such as password changes). All of their requests to your GPT actions will fail (assuming you blocked their access ), and you’ll have to ask them to manually sign out and back in again, which is cumbersome. If you use refresh tokens, your user’s tokens will expire after a certain amount of time, at which point you can direct ChatGPT to ask the user to sign in again. If you set a short enough expiry, this means your users will only have to experience a very small amount of downtime without manually signing out.
Second, if OpenAI or ChatGPT experience a data breach, then attackers would only need your token to make authenticated calls against your backend, and siphon your users’ data. With expiring tokens, you require attackers to at least have a refresh token and an expiring secret to issue a new token for your backend, which means they have to act within a time limit rather than indefinitely. (Because OpenAI store everything, likely in the same place, it’s not a huge security improvement but there are benefits nonetheless).
How do I provide a refresh token to ChatGPT?
The docs hint at this, but basically your /oauth/token
endpoint can return:
{
"access_token": "<example access token>",
"token_type": "Bearer",
"expires_in": 60,
"refresh_token": "<example refresh token>"
}
ChatGPT looks for the expires_in
field (not expires
) for the number of seconds before the token expires (this one will expire in 1 minute), and a refresh_token
field. Obviously, return a different refresh token from your access token.
See below for what to set expires_in
to.
How does ChatGPT refresh my tokens?
ChatGPT does not pre-emptively refresh tokens. Instead, the next time your user makes a request that involves an action, ChatGPT searches its secret store for your access token, and if the expiry date has passed, it will call /oauth/token
again with the following payload:
(If you’ve implemented OAuth so far with GPT Actions you’ll remember that this payload has Content-Type: application/x-www-form-urlencoded
and should be parsed as such; I’m representing it in JSON because it’s easier to understand)
{
"client_id": "<your client ID>",
"client_secret": "<your client secret>",
"grant_type": "refresh_token",
"refresh_token": "<your refresh token from before>"
}
It’s your responsibility to validate the client secret and refresh token. You can then respond with:
{
"access_token": "<example access token>",
"token_type": "Bearer"
}
Alternatively, if you respond with any 401
error (no need to worry about content types, headers, or anything else), ChatGPT will remove any of that user’s tokens from the storage and prompt them to sign in again. More on that in a moment.
How can I refresh my refresh tokens?
It’s usually best practice to:
- Delete all refresh tokens after a single use
- Expire your refresh tokens after a certain amount of time
However, since ChatGPT is securely storing both tokens, it might not be a requirement of your security model. But in case you want to do this, ChatGPT supports adding the expires_in
and refresh_token
parameters to the above response, like so:
{
"access_token": "<example access token>",
"token_type": "Bearer",
"expires_in": 60,
"refresh_token": "<new example refresh token>"
}
In the background, you would delete the refresh token the user has just used, and issue a new one. If ChatGPT doesn’t contact your server in time, you would expire the old token so that it can’t be used.
Just like above, if you respond with 401
, then ChatGPT will delete your user’s refresh token and prompt the user to sign in again.
How can I revoke an existing access or refresh token?
Firstly, you can’t revoke a token during a normal request. If ChatGPT requests your /api/do_cool_thing
, and you reply with the standard HTTP Auth 401
(read more on this):
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer error="invalid_token", error_description="The access token expired"
Nothing will happen. ChatGPT does not, as of time of writing, revoke your access token in response and prompt your user to sign in. The only time ChatGPT will revoke an access token is if it has expired.
You can trigger ChatGPT to prompt the user to sign in again by responding to /oauth/token
with any 401
response. This will delete any stored access or refresh tokens from their storage.
As such, the best way to implement a revocation flow is:
- Immediately start
401
ing your API routes - Wait until the token expires
- ChatGPT will contact
/oauth/token
with the refresh token - You respond with a
401
- ChatGPT will delete the token and prompt the user to sign in again
What should I set my token expiries to be?
It makes the most sense to set a short expiry on your access tokens (in the order of minutes), and a longer expiry on your refresh tokens (in the order of days).
The access token expiry can be thought of as:
- The minimum amount of time between requests to
/oauth/token
for refresh tokens - The maximum amount of time your users will have to wait if you expire their access tokens before ChatGPT prompts them to sign in again
The first is a lower bound, because you presumably don’t want to overload your systems with requests (or you might have caches), and the second is an upper bound, because you don’t want your users to experience confusing access errors and possibly churn from your product. If you plan on revoking your tokens very rarely, then this bound can be larger, but if you plan on users experiencing it frequently (i.e. from logging out), then you should set it lower.
The refresh token expiry can be thought of as the maximum amount of time your user can go without calling one of your actions before you sign them out. (The lower bound on your refresh token expiries would be the access token expiry; any lower and you’ll force the user to sign in again every time their access token expires). As ChatGPT has 100 million weekly active users, if you expect your users to use your GPT regularly you might set this as low a day or a week; typical refresh tokens usually expire within a month.
Remember to revoke refresh tokens as soon as they’re used, so that they can’t be re-used later. ChatGPT will always make requests using the latest refresh token.
How can my users revoke their access tokens?
Your users (or you, in development), can manually revoke your access tokens by signing out of the third party service. You’ll need to go to your GPT in the ChatGPT web app (no mobile support as of time of writing).
Start by clicking on the GPT’s name:
Then open ‘Privacy settings’ and click ‘Connected accounts’ in the sidebar. If you can’t see ‘Privacy settings’ or ‘Connected accounts’, that’s most likely because you’ve only been using the GPT via the editor. You’ll need to edit your GPT and publish the latest changes in some form. If you still can’t see it, try triggering an action from outside of the editor and it should sync everything up.
Then click ‘Log out’. The UI won’t update immediately but it will work.
Parting thoughts
If you’re serious about building GPT Actions with user-specific properties, you shouldn’t launch without refresh tokens. While the docs are a bit lacking, hopefully this guide clears some of that up.
Auth is a precise art, and there’s lots of easy footguns. I’ve deliberately avoided implementation details and code here because you shouldn’t copy and paste security-related code, you should be thinking precisely and carefully about what your requirements are and what will happen. That said, any security is better than no security.
Please ask any questions below or let me know if I’ve made mistakes.