oauth() plugin exposes through the plugin context.
createOAuthModule(db, config) returns the same three methods.
getAccessToken
{ accessToken, tokenType, expiresAt, refreshed }. expiresAt is null when the provider never said, and such a token is returned as is. refreshed is true when this call had to refresh.
If the stored token expires within expirySkewSeconds (default 60), the module exchanges the refresh token for a new one and saves the result before returning. Providers that do not rotate refresh tokens keep the old one. Concurrent calls for the same account in one process share a single refresh, so a provider that rotates refresh tokens sees one use, not several. If a refresh loses a race with another instance and fails, the module re-reads the row and returns the winner’s token when it is fresh.
Errors
Failures throw an error with a typedcode:
OAuthTokenError and OAuthRefreshError are exported from @glinr/theauth/auth. The error carries a code from the table above and the provider id.
Which providers can refresh
A provider can refresh when its definition hasrefreshTokens. All of these do:
- Atlassian, Discord, Dropbox, Figma, GitHub, GitLab, Google, LinkedIn, Microsoft, Reddit, Spotify, Twitch, Twitter and Zoom
genericOIDCand the presets built on it, such asfacebookProvider,tiktokProvider,auth0ProviderandoktaProvider
createAppleProvider, createSlackProvider and createNotionProvider. For them getAccessToken returns the stored token while it is fresh or has no expiry, and fails once an expiry has passed: no_refresh_token when none is stored, otherwise refresh_failed. The notionProvider preset is built on genericOIDC and does implement refresh.
Having refreshTokens is not the same as holding a refresh token. Providers only issue one in some cases, for example Google returns it on the first consent, and a later sign-in may not include a new one. When none was ever stored, an expired token fails with no_refresh_token.
A custom provider opts in by implementing refreshTokens(refreshToken) and throwing OAuthRefreshError with reauth: true on invalid_grant.
listAccounts
unlinkAccount
no_account when nothing is linked.
The last-credential guard
Unlinking is refused withlast_credential when the account is the user’s only way to sign in. These count as another way automatically:
- another linked OAuth account
- a username and password account
- a passkey
hasOtherSignInMethod:
HTTP routes
Theoauth() plugin also adds two signed-in routes, relative to your adapter prefix:
Both return 401 without a session. The access token itself is never exposed over HTTP. Read it server side with
getAccessToken.
Encrypt tokens at rest
By default provider tokens are stored as plain text. AddtokenEncryption to store them sealed with AES-256-GCM, the same envelope the token vault uses:
node -e "console.log(require('crypto').randomBytes(32).toString('base64url'))".
- Each value is bound to its account id and field, so a ciphertext copied to another row will not decrypt.
- Sealed values carry an
enc:prefix. Existing plaintext rows keep working and are re-encrypted the next time they are written (sign-in, linking or a refresh). - Startup throws if encryption is enabled without keys or with an
activeKeyIdthat is not inkeys. - To rotate, add a new key, point
activeKeyIdat it and keep the old key listed until every row has been rewritten. A row sealed with a key you removed fails withdecrypt_failed. - Set
enabled: falseto stop encrypting without removing the keys. Existing sealed rows still need the keys to be read.
Related
OAuth overview
Providers, the sign-in flow and account linking.
Token vault
Give agents delegated access to provider tokens with consent and audit.
Sign-in gate
The OAuth callback now goes through the gate before a session is issued.