OAuth Patterns

OAuth implementation examples

TypeScript

Go

Atproto OAuth Example Apps

These simple example apps demonstrate usage of their respective SDKs.

cookbook/react-native-oauth
LanguageTypeScript
SDK@atproto/oauth-client-expo
ArchitectureMobile App (no backend)
Confidential Client?No
Localhost development client ID?No (requires hosted client metadata)
cookbook/vanillajs-oauth-web-app
LanguageJavaScript
SDK@atproto/oauth-client-browser
ArchitectureWeb SPA (no backend)
Confidential Client?No
Localhost development client ID?Optionally
cookbook/nextjs-oauth
LanguageTypeScript
SDK@atproto/oauth-client-node
ArchitectureWeb BFF
Confidential Client?Optionally
Localhost development client ID?Optionally
cookbook/go-oauth-web-app
LanguageGo
SDKindigo
ArchitectureWeb BFF
Confidential Client?Optionally
Localhost development client ID?Optionally
cookbook/go-oauth-cli-app
LanguageGo
SDKindigo
ArchitectureNative CLI
Confidential Client?No
Localhost development client ID?No (requires hosted client metadata)
cookbook/python-oauth-web-app
LanguagePython
SDKNone
ArchitectureWeb BFF
Confidential Client?Optionally
Localhost development client ID?Optionally

Types of App

The simplest type of app is one where the OAuth session is established directly between the user's device and the PDS, as demonstrated in the cookbook/vanillajs-oauth-web-app example. It's simple because it doesn't require a dedicated backend service, only hosting static resources.

If your app doesn't need long-lived sessions, then this approach is completely fine. But for other use cases, session lifetime will be limited because the app is a "public client", as opposed to a "confidential client". Becoming a confidential client involves establishing a secret key, bound to the client ID. If the client is publicly available and runs on the user's own device, it cannot protect a client secret. Thus, implementing a confidential client necessitates hosting some backend infrastructure, which the cookbook/go-oauth-web-app and cookbook/python-oauth-web-app examples demonstrate (both using the "BFF" pattern, see below).

See Types of Clients for more technical details on confidential vs. public clients.

There are several design strategies that can be used to "upgrade" a public client into a confidential client:

  • Backend For Frontend (BFF): The OAuth session is established between the app backend server and the PDS. This server-side session is associated with the user's frontend session via mechanisms such as session cookies. The frontend makes requests to the PDS by proxying them through the backend, which handles the OAuth authorization.
  • Token-Mediating Backend (TMB): Similar to the BFF pattern, except the backend passes OAuth access tokens to the frontend once the session has been established, allowing the frontend to make direct requests to the PDS.
  • Client Assertion Backend: A proposed alternative to the TMB pattern with simplified server-side logic.

Of these three, the BFF pattern is the currently recommended approach for building confidential-client applications. This is demonstrated in the cookbook/go-oauth-web-app and cookbook/python-oauth-web-app examples.

Understanding DPoP

DPoP (Demonstrating Proof of Possession) cryptographically binds OAuth tokens to a key held by the client. Even if an attacker intercepts your access token, they can't use it without your private key. This is especially important for confidential clients with long-lived sessions (up to 2 years).

DPoP is mandatory in atproto OAuth. If you're using an SDK, it's handled automatically. If you're implementing OAuth manually (e.g., for a sidecar that writes records), the key things to know are:

  • Generate an ES256 key pair once per session and reuse it for all requests
  • Every request needs a fresh DPoP proof JWT in the DPoP header, with Authorization: DPoP {access_token}
  • Track nonces separately for the authorization server and PDS — they may differ
  • When you get a 401 with error="use_dpop_nonce", retry with the nonce from the DPoP-Nonce response header
  • For PDS requests, include an ath claim containing the SHA-256 hash of the access token

The cookbook/python-oauth-web-app contains a complete manual DPoP implementation in atproto_oauth.py. For the full specification, see DPoP in the OAuth spec.

Further Reading and Resources