Skip to content

OAuth Client and RFC 9700

RFC 9700 is the OAuth 2.0 Security Best Current Practice. It gives recommendations for clients, for authorization servers, and for resource servers. This page checks the Seamless Auth API against the ones that apply to it today.

Today the Seamless Auth API is an OAuth client only. When a provider is configured (Google, GitHub, Microsoft Entra ID, or any OAuth 2.0 or OpenID Connect provider, see OAuth Login), the API sends the user to that provider, exchanges the authorization code, reads the profile, and then issues its own Seamless Auth session. It is not an authorization server for other applications until the OpenID Provider planned for v0.21, so the authorization server recommendations are listed below as “applies from v0.21”, not as passes or gaps.

Every code link points at the v0.19.2 tag of the API. The flow lives in oauthService.ts, oauth.ts (the controller), and oauth.routes.ts.

Status values:

  • Meets: the recommendation holds by default.
  • Meets, configurable: it holds by default, and an operator setting can turn it off.
  • Partial: some of the recommendation holds, and the row says which part does not.
  • Not used: the API does not use the feature the recommendation is about.

RFC 9700 Recommendation Status How, in v0.19.2
2.1, 4.7 Prevent CSRF: the state (or PKCE verifier, or OpenID Connect nonce) must be bound to the user agent that started the flow Meets, configurable, since 0.19.2 /start returns a random binding to the client that asked, never to the provider, and signs only its SHA-256 into the state (createOAuthState). The callback must present it, compared in constant time before the replay cache records the state (checkOAuthStateBinding, consumeOAuthState), so a state and code taken from one sign-in cannot be finished in another browser. The state is also HMAC-SHA256 signed, expires after ten minutes, and is refused on reuse. The client SDKs keep and send the binding (@seamless-auth/client 0.7.1 and later). OAUTH_ALLOW_UNBOUND_STATE=true accepts a callback with no binding while older clients are upgraded; it reopens this gap while set, warns at boot, and is removed in v0.20 (unboundOAuthStateAllowed). Version 0.19.1 and earlier did not bind the state (#463).
2.1, 4.4 A client that uses more than one authorization server must defend against mix-up attacks, with the iss response parameter (RFC 9207) or a distinct redirect URI per authorization server Partial The signed state names the provider, and a callback for another provider is refused (verifyOAuthState). That stops a state from one provider being used with another, but it does not stop the mix-up attack itself. The callback body has no iss and none is checked (FinishOAuthLoginRequestSchema). A provider can have its own redirectUri, but without one every provider shares <first origin>/oauth/callback (resolveOAuthRedirectUri). Every provider is configured by the operator, so the attack needs a provider the operator added to be malicious or compromised. Give each provider its own redirect URI.
2.1.1, 4.5 Prevent authorization code injection, with PKCE or the OpenID Connect nonce Meets, configurable PKCE is on unless a provider sets pkce: false (pkceEnabled). The verifier is derived per sign-in on the server and never leaves it, so a code stolen from one sign-in fails the exchange in another. An OpenID Connect provider also gets a nonce, signed into the state and checked against the verified ID token (verifyOAuthIdToken). A provider with pkce: false and no issuer or jwksUri has neither defence. Set pkce: false only for a provider that refuses PKCE.
2.1.1 Use the S256 PKCE challenge method Meets Only S256 is sent (buildOAuthAuthorizationUrl).
2.1.2, 4.6 Do not use the implicit grant, or any response type that returns an access token in the authorization response Meets The authorization request always uses response_type=code (buildOAuthAuthorizationUrl). Tokens come only from the token endpoint, over a back-channel request from the API.
2.4 Do not use the resource owner password credentials grant Not used The only grant sent to a provider is authorization_code (exchangeOAuthTokens). Seamless Auth has no password flow.
4.11 Do not expose an open redirector Meets A requested redirectUri must exactly match the provider’s redirectUris, or, when a provider lists none, share an origin with a configured origin (allowedRedirect). returnTo must share an origin with a configured origin (allowedReturnTo), and is read back from the signed state at the callback rather than from the callback request (finishOAuthLogin). Configure exact redirectUris in production.
4.6 Validate ID tokens, so a token issued for another client or sign-in is not accepted Meets An OpenID Connect provider’s ID token is checked for signature against its JWKS, issuer, audience (this client), expiry, issued-at, subject, azp when there are several audiences, and the nonce. Only asymmetric algorithms are accepted, so none and shared-key algorithms are refused (verifyOAuthIdToken).
4.9 Send an access token only to the resource it is meant for, in a header rather than a URL Meets The provider access token is sent only to the provider’s configured userInfoUrl (and, for GitHub, the email list on the same origin), in an Authorization: Bearer header (fetchOAuthProfile). An OpenID Connect provider’s profile comes from the verified ID token and the access token is not used at all.
4.13 Protect refresh tokens Not used The API reads only access_token and id_token from the provider’s token response (exchangeOAuthTokens). A provider refresh token is never kept, and the provider access token is used once during the callback, then dropped. Neither is stored, logged, or returned to the client.
2.2 Use sender-constrained access tokens (mTLS or DPoP) Not used Provider access tokens are plain bearer tokens. Each is used once, server to server, within the callback, which leaves little window for replay. DPoP and mTLS are not supported.
2.3 Request only the privileges the client needs Operator setting The scopes sent are the provider’s configured scopes (buildOAuthAuthorizationUrl). The API needs a subject and a verified email, so openid email (or the provider’s equivalent, such as user:email on GitHub) is enough.
2.5 Authenticate the client, preferably with an asymmetric method (mTLS or private_key_jwt) Partial The client authenticates with client_secret_post (exchangeOAuthTokens). The secret is read from the environment variable the provider’s clientSecretEnv names, never stored in system_config, and never returned by the API. Asymmetric client authentication is not supported.
4.2, 4.3 Keep the authorization code and state out of Referer headers and browser history Application The provider redirects to the application’s own callback page, which then posts code and state to its backend. That page is not served by the API, so this is up to the application: load no third-party content on it and leave the URL once the sign-in is finished. The code is single use at the provider and, with PKCE, useless without the verifier only the API holds.
4.1 Use TLS for every endpoint of the flow Operator setting Provider authorizationUrl, tokenUrl, userInfoUrl, issuer and jwksUri must be URLs, but http is not refused (provider schema). Configure https endpoints, which every public provider uses.
4.7 Treat a state as single use Partial, tracked in #414 A consumed state is remembered for ten minutes, but only by the API instance that consumed it (consumeOAuthState). With several instances, another instance could accept a replayed callback inside that window. The state is still signed and expiring, and the provider’s code is single use. Until #414 moves the cache to shared storage, keep OAuth callbacks on one instance.

These apply to an authorization server. The Seamless Auth API does not act as one until the OpenID Provider planned for v0.21, so none of them is scored yet. They are listed so the v0.21 work is checked against them.

RFC 9700 Recommendation Status
2.1, 4.1 Match redirect URIs exactly against the registered value Applies from v0.21
2.1.1, 4.8 Support PKCE, enforce it, and prevent PKCE downgrade Applies from v0.21
2.1, 4.4 Return iss in the authorization response (RFC 9207) Applies from v0.21
2.1.2, 2.4 Do not offer the implicit or resource owner password credentials grants Applies from v0.21
2.2, 4.13 Sender-constrain or rotate refresh tokens for public clients Applies from v0.21
2.3, 4.10 Restrict access tokens to an audience and to the privileges granted Applies from v0.21
2.5 Authenticate confidential clients, preferably with asymmetric methods Applies from v0.21
4.11, 4.12 Do not act as an open redirector, and do not redirect with HTTP 307 Applies from v0.21
4.14 Keep clients from choosing a client_id that impersonates a resource owner Applies from v0.21
4.15 Prevent clickjacking of the authorization page Applies from v0.21
4.16 Do not redirect to a client after a failed authentication without the user’s interaction Applies from v0.21
2.6 Do not support CORS on the authorization endpoint Applies from v0.21

The Seamless Auth session itself (its access token, single-use refresh token rotation, and reuse detection) is not an OAuth grant and is described in Security Posture.