Documentation Index Fetch the complete documentation index at: https://mintlify.com/bluesky-social/atproto/llms.txt
Use this file to discover all available pages before exploring further.
Overview
The @atproto/oauth-client package provides the core OAuth client implementation for AT Protocol. This is a platform-agnostic base library that serves as the foundation for environment-specific implementations.
This package is designed as a base library. For production use, consider:
Installation
npm install @atproto/oauth-client
When to Use This Package
Use @atproto/oauth-client directly when:
Building a custom OAuth client for a non-standard environment
Creating a new platform-specific implementation
You need full control over runtime dependencies
For most use cases, prefer the platform-specific packages that provide pre-configured runtime implementations.
Key Concepts
OAuth Client
The main OAuthClient class manages the complete OAuth flow:
Authorization : Initiate OAuth flows with AT Protocol servers
Token Management : Handle access and refresh tokens automatically
DPoP : Built-in Demonstrating Proof-of-Possession support
PKCE : Automatic Proof Key for Code Exchange
Session Management : Persistent session storage and restoration
Runtime Implementation
The client requires a runtime implementation that provides platform-specific cryptographic operations:
interface RuntimeImplementation {
createKey : ( algs : string []) => Promise < Key >
getRandomValues : ( length : number ) => Uint8Array | Promise < Uint8Array >
digest : ( data : Uint8Array , alg : DigestAlgorithm ) => Uint8Array | Promise < Uint8Array >
requestLock ?: ( name : string , fn : () => Promise < T >) => Promise < T >
}
Core Types
OAuthClientOptions
Configuration options for creating an OAuth client.
clientMetadata
OAuthClientMetadataInput
required
Client metadata including client_id, redirect_uris, and other OAuth client configuration. {
client_id : 'https://my-app.com/client-metadata.json' ,
redirect_uris : [ 'https://my-app.com/callback' ],
scope : 'atproto' ,
grant_types : [ 'authorization_code' , 'refresh_token' ],
response_types : [ 'code' ],
token_endpoint_auth_method : 'none' ,
dpop_bound_access_tokens : true
}
responseMode
'query' | 'fragment' | 'form_post'
required
How the authorization response is returned:
query: Parameters in URL query string (recommended for backend)
fragment: Parameters in URL fragment (recommended for browser)
form_post: Parameters via HTTP POST (backend only)
runtimeImplementation
RuntimeImplementation
required
Platform-specific implementation of cryptographic operations.
Storage for OAuth state during authorization flows. Must implement: interface StateStore {
set ( key : string , state : InternalStateData ) : Promise < void >
get ( key : string ) : Promise < InternalStateData | undefined >
del ( key : string ) : Promise < void >
}
Storage for authenticated sessions. Must implement: interface SessionStore {
set ( sub : string , session : Session ) : Promise < void >
get ( sub : string ) : Promise < Session | undefined >
del ( sub : string ) : Promise < void >
}
Private keys for client authentication (backend clients only). Required when using private_key_jwt authentication.
handleResolver
string | URL | HandleResolver
Service URL or instance for resolving AT Protocol handles to DIDs.
Allow HTTP connections for development. Never use in production.
Custom fetch implementation for HTTP requests.
onUpdate
(sub: string, session: Session) => void
Callback when a session is updated (e.g., token refresh).
onDelete
(sub: string, cause: Error) => void
Callback when a session is deleted or revoked.
Main Class: OAuthClient
Constructor
const client = new OAuthClient ( options : OAuthClientOptions )
Creates a new OAuth client instance with the provided configuration.
Static Methods
static async fetchMetadata ({
clientId: string ,
fetch? : Fetch ,
signal? : AbortSignal
}): Promise < OAuthClientMetadata >
Fetches client metadata from a discoverable client ID URL.
The client ID URL (must start with https://).
Custom fetch implementation.
Signal to abort the request.
The parsed and validated client metadata.
Instance Methods
authorize
async authorize (
input : string ,
options ?: AuthorizeOptions
): Promise < URL >
Initiates an OAuth authorization flow. Returns the URL to redirect the user to.
User’s handle (e.g., alice.bsky.social), DID (e.g., did:plc:xyz), or PDS URL.
Application state to preserve through the OAuth flow.
OAuth scopes to request (defaults to client metadata scope).
options.prompt
'none' | 'login' | 'consent'
none: Attempt silent sign-in
login: Force re-authentication
consent: Force re-consent
Preferred locales for the authorization UI (e.g., en-US en).
Override the default redirect URI from client metadata.
Signal to abort the authorization.
The URL to redirect the user to for authorization.
Example:
const url = await client . authorize ( 'alice.bsky.social' , {
state: 'my-app-state' ,
prompt: 'none' , // Silent sign-in
ui_locales: 'fr-CA fr en'
})
// Redirect user to url
window . location . href = url . toString ()
callback
async callback (
params : URLSearchParams ,
options ?: CallbackOptions
): Promise < { session : OAuthSession ; state ?: string } >
Processes the OAuth callback after the user is redirected back.
URL parameters from the callback (query or fragment depending on response mode).
The redirect URI that was used (must match authorization request).
The authenticated session object.
The application state from the authorization request.
Example:
const params = new URLSearchParams ( window . location . search )
const { session , state } = await client . callback ( params )
console . log ( 'Authenticated as:' , session . did )
console . log ( 'App state:' , state )
restore
async restore (
sub : string ,
refresh ?: boolean | 'auto'
): Promise < OAuthSession >
Restores a previously authenticated session from storage.
The user’s DID (subject identifier).
refresh
boolean | 'auto'
default: "'auto'"
true: Force token refresh
false: Use cached tokens
'auto': Refresh if tokens are expired
The restored session object.
Throws:
TokenRefreshError if the session cannot be refreshed
TokenRevokedError if the session was revoked
Example:
try {
const session = await client . restore ( 'did:plc:xyz' )
// Use session for authenticated requests
} catch ( err ) {
if ( err instanceof TokenRefreshError ) {
// Session expired, need to re-authenticate
}
}
revoke
async revoke ( sub : string ): Promise < void >
Revokes an active session and deletes it from storage.
The user’s DID to revoke.
Example:
await client . revoke ( 'did:plc:xyz' )
// Session is now revoked and deleted
Properties
jwks
Returns the public JWKS (JSON Web Key Set) for this client. Use this to expose your client’s public keys at the jwks_uri endpoint.
Array of public keys in JWK format.
Example:
app . get ( '/jwks.json' , ( req , res ) => {
res . json ( client . jwks )
})
get clientMetadata (): ClientMetadata
The validated client metadata being used by this client.
OAuthSession Class
Represents an authenticated session with automatic token management.
Properties
The user’s DID (decentralized identifier).
Alias for did - the subject identifier.
serverMetadata
OAuthAuthorizationServerMetadata
Metadata of the authorization server that issued this session.
Methods
getTokenInfo
async getTokenInfo (
refresh ?: boolean | 'auto'
): Promise < TokenInfo >
Retrieves information about the current access token.
When the access token expires.
Whether the token is currently expired.
The granted OAuth scopes.
The issuer (authorization server URL).
The audience (resource server URL).
The subject (user’s DID).
fetchHandler
async fetchHandler (
pathname : string ,
init ?: RequestInit
): Promise < Response >
Makes an authenticated HTTP request with automatic token refresh.
The path to request (relative to the resource server).
Standard fetch options (method, headers, body, etc.).
The HTTP response from the server.
Example:
const response = await session . fetchHandler ( '/xrpc/com.atproto.repo.getRecord' , {
method: 'GET' ,
headers: { 'Content-Type' : 'application/json' }
})
const data = await response . json ()
signOut
async signOut (): Promise < void >
Revokes the session tokens on the server and deletes the local session.
Example:
await session . signOut ()
// User is now signed out
Error Classes
OAuthCallbackError
Thrown when the OAuth callback contains an error response.
class OAuthCallbackError extends Error {
readonly params : URLSearchParams
readonly state : string | null
}
The callback parameters containing the error.
The state parameter from the request.
TokenRefreshError
Thrown when a token refresh fails.
class TokenRefreshError extends Error {
readonly sub : string
readonly cause ?: Error
}
TokenRevokedError
Thrown when a session is revoked.
class TokenRevokedError extends Error {
readonly sub : string
}
TokenInvalidError
Thrown when a token is rejected by the resource server.
class TokenInvalidError extends Error {
readonly sub : string
}
OAuth Flow Example
Complete Authorization Flow
import { OAuthClient , OAuthSession } from '@atproto/oauth-client'
import { JoseKey } from '@atproto/jwk-jose'
// 1. Initialize the client
const client = new OAuthClient ({
clientMetadata: {
client_id: 'https://my-app.com/client-metadata.json' ,
redirect_uris: [ 'https://my-app.com/callback' ],
scope: 'atproto' ,
grant_types: [ 'authorization_code' , 'refresh_token' ],
response_types: [ 'code' ],
token_endpoint_auth_method: 'private_key_jwt' ,
dpop_bound_access_tokens: true ,
jwks_uri: 'https://my-app.com/jwks.json'
},
responseMode: 'query' ,
handleResolver: 'https://bsky.social' ,
runtimeImplementation: {
createKey : ( algs ) => JoseKey . generate ( algs ),
getRandomValues : ( len ) => crypto . getRandomValues ( new Uint8Array ( len )),
digest : ( data , alg ) => {
if ( alg . name === 'sha256' ) {
return crypto . subtle . digest ( 'SHA-256' , data )
. then ( buf => new Uint8Array ( buf ))
}
throw new Error ( `Unsupported algorithm: ${ alg . name } ` )
}
},
stateStore: myStateStore ,
sessionStore: mySessionStore ,
keyset: await Promise . all ([
JoseKey . fromImportable ( process . env . PRIVATE_KEY_1 ),
JoseKey . fromImportable ( process . env . PRIVATE_KEY_2 )
])
})
// 2. Start authorization
const authUrl = await client . authorize ( 'alice.bsky.social' , {
state: 'my-app-state-123'
})
// Redirect user to authUrl...
// 3. Handle callback
const params = new URLSearchParams ( req . query )
const { session , state } = await client . callback ( params )
console . log ( 'Authenticated:' , session . did )
console . log ( 'State:' , state ) // 'my-app-state-123'
// 4. Make authenticated requests
const response = await session . fetchHandler ( '/xrpc/com.atproto.repo.listRecords' , {
method: 'GET'
})
// 5. Later, restore the session
const restoredSession = await client . restore ( session . did )
// 6. Sign out when done
await session . signOut ()
Events
The OAuthClient extends EventTarget and emits the following events:
updated
Fired when a session is updated (e.g., after token refresh).
client . addEventListener ( 'updated' , ( event : CustomEvent < Session >) => {
const session = event . detail
console . log ( 'Session updated:' , session . tokenSet )
})
deleted
Fired when a session is deleted from storage.
client . addEventListener ( 'deleted' , (
event : CustomEvent <{ sub : string ; cause : Error }>
) => {
const { sub , cause } = event . detail
console . log ( `Session ${ sub } deleted:` , cause . message )
})
Integration with @atproto/api
Use OAuthSession as a fetch handler for the Agent class:
import { Agent } from '@atproto/api'
const session = await client . restore ( 'did:plc:xyz' )
const agent = new Agent ( session )
// Make authenticated API calls
const profile = await agent . getProfile ({ actor: agent . did })
await agent . post ({ text: 'Hello, AT Protocol!' })
// Sign out
await agent . signOut ()
Security Best Practices
Follow these security guidelines when implementing OAuth clients:
State Management
Always use state parameter to prevent CSRF attacks
Store state securely with short TTL (≤1 hour)
Validate state matches on callback
Key Storage
Backend clients : Store private keys securely (environment variables, key management services)
Frontend clients : Use token_endpoint_auth_method: 'none' (no private keys)
Never expose private keys in client-side code
Token Handling
Store sessions securely (encrypted database, secure cookies)
Implement proper session cleanup
Use requestLock to prevent concurrent token refreshes
Handle token expiration gracefully
Network Security
Production : Only use HTTPS connections (allowHttp: false)
Development : Use secure tunneling (ngrok) or loopback addresses
Validate all OAuth server metadata
Error Handling
try {
const session = await client . restore ( userId )
} catch ( err ) {
if ( err instanceof TokenRefreshError ) {
// Session expired - redirect to login
} else if ( err instanceof TokenRevokedError ) {
// Session was revoked - clear local state
} else if ( err instanceof TokenInvalidError ) {
// Tokens rejected by server - re-authenticate
} else {
// Unexpected error - log and handle appropriately
}
}
@atproto/oauth-client-browser Browser-specific OAuth client with IndexedDB storage
@atproto/oauth-client-node Node.js OAuth client with built-in runtime
@atproto/oauth-provider OAuth authorization server implementation
@atproto/api High-level AT Protocol API client
Advanced Topics
Custom Handle Resolver
import { HandleResolver } from '@atproto/oauth-client'
class CustomHandleResolver implements HandleResolver {
async resolve ( handle : string ) : Promise < string | null > {
// Custom resolution logic
const response = await fetch ( `https://my-resolver.com/ ${ handle } ` )
return response . ok ? response . text () : null
}
}
const client = new OAuthClient ({
handleResolver: new CustomHandleResolver (),
// ... other options
})
Silent Sign-In
Attempt authentication without user interaction:
try {
const authUrl = await client . authorize ( handle , {
prompt: 'none' ,
state: 'silent-signin'
})
// Redirect...
} catch ( err ) {
if ( err instanceof OAuthCallbackError ) {
const error = err . params . get ( 'error' )
if ( error === 'login_required' || error === 'consent_required' ) {
// Fallback to interactive sign-in
const authUrl = await client . authorize ( handle , {
state: 'interactive-signin'
})
}
}
}
Multi-Account Support
const sessions = new Map < string , OAuthSession >()
// Authenticate multiple users
for ( const handle of [ 'alice.bsky.social' , 'bob.bsky.social' ]) {
const authUrl = await client . authorize ( handle )
// ... complete authorization flow
sessions . set ( session . did , session )
}
// Switch between accounts
const currentUser = 'did:plc:alice'
const session = sessions . get ( currentUser )
const agent = new Agent ( session )
TypeScript Types
All types are fully typed with TypeScript:
import type {
OAuthClient ,
OAuthClientOptions ,
OAuthSession ,
Session ,
StateStore ,
SessionStore ,
RuntimeImplementation ,
TokenInfo ,
InternalStateData ,
AuthorizeOptions ,
CallbackOptions
} from '@atproto/oauth-client'