Skip to main content

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.
stateStore
StateStore
required
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>
}
sessionStore
SessionStore
required
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>
}
keyset
Keyset | Iterable<Key>
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.
allowHttp
boolean
default:"false"
Allow HTTP connections for development. Never use in production.
fetch
Fetch
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

fetchMetadata

static async fetchMetadata({
  clientId: string,
  fetch?: Fetch,
  signal?: AbortSignal
}): Promise<OAuthClientMetadata>
Fetches client metadata from a discoverable client ID URL.
clientId
string
required
The client ID URL (must start with https://).
fetch
Fetch
Custom fetch implementation.
signal
AbortSignal
Signal to abort the request.
metadata
OAuthClientMetadata
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.
input
string
required
User’s handle (e.g., alice.bsky.social), DID (e.g., did:plc:xyz), or PDS URL.
options.state
string
Application state to preserve through the OAuth flow.
options.scope
string
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
options.ui_locales
string
Preferred locales for the authorization UI (e.g., en-US en).
options.redirect_uri
string
Override the default redirect URI from client metadata.
options.signal
AbortSignal
Signal to abort the authorization.
authorizationUrl
URL
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.
params
URLSearchParams
required
URL parameters from the callback (query or fragment depending on response mode).
options.redirect_uri
string
The redirect URI that was used (must match authorization request).
session
OAuthSession
The authenticated session object.
state
string
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.
sub
string
required
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
session
OAuthSession
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.
sub
string
required
The user’s DID to revoke.
Example:
await client.revoke('did:plc:xyz')
// Session is now revoked and deleted

Properties

jwks

get jwks(): 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.
keys
JsonWebKey[]
Array of public keys in JWK format.
Example:
app.get('/jwks.json', (req, res) => {
  res.json(client.jwks)
})

clientMetadata

get clientMetadata(): ClientMetadata
The validated client metadata being used by this client.

OAuthSession Class

Represents an authenticated session with automatic token management.

Properties

did
AtprotoDid
The user’s DID (decentralized identifier).
sub
AtprotoDid
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.
expiresAt
Date
When the access token expires.
expired
boolean
Whether the token is currently expired.
scope
string
The granted OAuth scopes.
iss
string
The issuer (authorization server URL).
aud
string
The audience (resource server URL).
sub
string
The subject (user’s DID).

fetchHandler

async fetchHandler(
  pathname: string,
  init?: RequestInit
): Promise<Response>
Makes an authenticated HTTP request with automatic token refresh.
pathname
string
required
The path to request (relative to the resource server).
init
RequestInit
Standard fetch options (method, headers, body, etc.).
response
Response
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
}
params
URLSearchParams
The callback parameters containing the error.
state
string | null
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'

Build docs developers (and LLMs) love