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.
What is Authentication in AT Protocol?
AT Protocol uses JWT (JSON Web Token) based authentication with a dual-token system:
Access Token - Short-lived token for API requests
Refresh Token - Long-lived token to obtain new access tokens
This approach balances security with user experience, allowing secure API access without requiring frequent re-authentication.
Authentication Flow
Creating a Session
Use the AtpAgent class for automatic session management:
import { AtpAgent } from '@atproto/api'
// Create agent
const agent = new AtpAgent ({
service: 'https://bsky.social'
})
// Login (creates session)
await agent . login ({
identifier: 'alice.bsky.social' , // Handle or DID
password: 'hunter2'
})
// Session is now active
console . log ( 'DID:' , agent . did )
console . log ( 'Handle:' , agent . session ?. handle )
console . log ( 'Access JWT:' , agent . session ?. accessJwt )
// Make authenticated requests
await agent . post ({
text: 'Hello from authenticated session!' ,
createdAt: new Date (). toISOString ()
})
Session Data Structure
interface AtpSessionData {
accessJwt : string // Short-lived access token
refreshJwt : string // Long-lived refresh token
handle : string // User's handle
did : string // User's DID
email ?: string // User's email (if available)
emailConfirmed ?: boolean // Email verification status
emailAuthFactor ?: boolean // If email 2FA is enabled
active : boolean // Account status
status ?: string // Account status details
}
Persisting Sessions
Persist sessions across app restarts:
import { AtpAgent , AtpSessionData , AtpSessionEvent } from '@atproto/api'
// Session persistence handler
const persistSession = ( evt : AtpSessionEvent , session ?: AtpSessionData ) => {
if ( evt === 'create' || evt === 'update' ) {
// Save session to storage
localStorage . setItem ( 'session' , JSON . stringify ( session ))
} else if ( evt === 'expired' ) {
// Clear session from storage
localStorage . removeItem ( 'session' )
}
}
// Create agent with persistence
const agent = new AtpAgent ({
service: 'https://bsky.social' ,
persistSession
})
// On app startup, resume previous session
const savedSession = localStorage . getItem ( 'session' )
if ( savedSession ) {
const session = JSON . parse ( savedSession )
await agent . resumeSession ( session )
console . log ( 'Session resumed for:' , session . handle )
}
Session Events:
create - New session created
create-failed - Session creation failed
update - Session refreshed
expired - Session expired or logged out
network-error - Transient refresh failure
Automatic Token Refresh
AtpAgent automatically handles token refresh:
// Create agent and login
const agent = new AtpAgent ({ service: 'https://bsky.social' })
await agent . login ({ identifier: 'alice.bsky.social' , password: 'pass' })
// Make requests - agent automatically:
// 1. Includes access token in Authorization header
// 2. Detects when access token expires (401 response)
// 3. Uses refresh token to get new access token
// 4. Retries original request with new token
for ( let i = 0 ; i < 100 ; i ++ ) {
// Even if access token expires during loop,
// agent handles refresh transparently
await agent . post ({
text: `Post ${ i } ` ,
createdAt: new Date (). toISOString ()
})
await new Promise ( resolve => setTimeout ( resolve , 60000 )) // 1 minute
}
Manual Token Refresh
Manually refresh when needed:
// Check if session exists
if ( ! agent . hasSession ) {
throw new Error ( 'No active session' )
}
// The agent automatically refreshes, but you can also trigger manually:
// (This is handled internally, rarely needed in application code)
Creating Accounts
Create a new account and session:
import { AtpAgent } from '@atproto/api'
const agent = new AtpAgent ({
service: 'https://bsky.social'
})
// Create account
const response = await agent . createAccount ({
email: 'alice@example.com' ,
handle: 'alice.bsky.social' ,
password: 'secure-password-here' ,
inviteCode: 'bsky-social-invite-code' // If required
})
// Session is automatically created
console . log ( 'Account created:' , response . data . did )
console . log ( 'Handle:' , response . data . handle )
console . log ( 'Access JWT:' , response . data . accessJwt )
// Can immediately make authenticated requests
await agent . api . app . bsky . actor . profile . create (
{ repo: agent . did },
{
displayName: 'Alice' ,
description: 'New to AT Protocol!'
}
)
Logging Out
End a session:
// Logout - revokes refresh token
await agent . logout ()
// Session is cleared
console . log ( 'Has session:' , agent . hasSession ) // false
Using Custom Session Managers
For advanced use cases, implement custom session management:
import { Agent , CredentialSession } from '@atproto/api'
// Create custom session manager
const session = new CredentialSession ({
service: new URL ( 'https://bsky.social' ),
fetch: globalThis . fetch ,
persistSession : ( evt , data ) => {
// Custom persistence logic
console . log ( 'Session event:' , evt , data )
}
})
// Use with Agent
const agent = new Agent ( session )
// Login through session manager
await session . login ({
identifier: 'alice.bsky.social' ,
password: 'hunter2'
})
// Make authenticated requests
await agent . api . app . bsky . feed . getTimeline ()
Multi-Factor Authentication
Handle 2FA when required:
try {
await agent . login ({
identifier: 'alice.bsky.social' ,
password: 'hunter2'
})
} catch ( error ) {
if ( error . message . includes ( 'AuthFactorTokenRequired' )) {
// User has 2FA enabled
const token = prompt ( 'Enter your 2FA code:' )
await agent . login ({
identifier: 'alice.bsky.social' ,
password: 'hunter2' ,
authFactorToken: token // Email code or TOTP
})
}
}
PDS Endpoint Discovery
AtpAgent automatically discovers and uses the correct PDS endpoint:
// Start with any AT Protocol service
const agent = new AtpAgent ({
service: 'https://bsky.social' // Entryway/aggregator
})
await agent . login ({
identifier: 'alice.bsky.social' ,
password: 'hunter2'
})
// Agent automatically discovers user's actual PDS from DID document
console . log ( 'Service URL:' , agent . serviceUrl )
// https://bsky.social
console . log ( 'PDS URL:' , agent . pdsUrl )
// https://morel.us-east.host.bsky.network (user's actual PDS)
// Subsequent requests go to the correct PDS
await agent . post ({ text: 'Hello!' , createdAt: new Date (). toISOString () })
Session Security
Token Storage
Access Tokens:
Short-lived (typically 2 hours)
Can be stored in memory
Less sensitive than refresh tokens
Refresh Tokens:
Long-lived (months or longer)
Should be stored securely
Used to obtain new access tokens
Best Practices
Store refresh tokens securely
Use secure storage mechanisms:
Web: httpOnly cookies or encrypted localStorage
Mobile: Secure enclave/keychain
Never expose refresh tokens to XSS attacks
Implement session timeout
Clear sessions after periods of inactivity to reduce exposure.
Handle token refresh failures
If refresh fails, prompt user to re-authenticate rather than silently failing.
Always call logout() to revoke refresh tokens server-side.
Never transmit credentials or tokens over unencrypted connections.
Error Handling
import { XRPCError } from '@atproto/xrpc'
try {
await agent . login ({
identifier: 'alice.bsky.social' ,
password: 'wrong-password'
})
} catch ( error ) {
if ( error instanceof XRPCError ) {
if ( error . status === 401 ) {
console . error ( 'Invalid credentials' )
} else if ( error . error === 'AuthFactorTokenRequired' ) {
console . error ( '2FA required' )
} else if ( error . error === 'AccountTakedown' ) {
console . error ( 'Account suspended' )
}
}
throw error
}
Advanced: Direct XRPC Calls
For fine-grained control, use XRPC directly:
import { XrpcClient } from '@atproto/xrpc'
const client = new XrpcClient (
( url , init ) => fetch ( url , init ),
'https://bsky.social'
)
// Create session
const { data } = await client . call (
'com.atproto.server.createSession' ,
{},
{
identifier: 'alice.bsky.social' ,
password: 'hunter2'
}
)
const { accessJwt , refreshJwt , did , handle } = data
// Make authenticated request
const timeline = await client . call (
'app.bsky.feed.getTimeline' ,
{ limit: 50 },
undefined ,
{
headers: {
authorization: `Bearer ${ accessJwt } `
}
}
)
Session Lifecycle Example
Complete session management implementation:
import { AtpAgent , AtpSessionData , AtpSessionEvent } from '@atproto/api'
class SessionManager {
private agent : AtpAgent
constructor ( private storageKey = 'atproto-session' ) {
this . agent = new AtpAgent ({
service: 'https://bsky.social' ,
persistSession: this . handleSessionEvent . bind ( this )
})
}
private handleSessionEvent ( evt : AtpSessionEvent , session ?: AtpSessionData ) {
switch ( evt ) {
case 'create' :
case 'update' :
if ( session ) {
localStorage . setItem ( this . storageKey , JSON . stringify ( session ))
console . log ( 'Session saved:' , session . handle )
}
break
case 'expired' :
localStorage . removeItem ( this . storageKey )
console . log ( 'Session expired' )
break
case 'create-failed' :
console . error ( 'Failed to create session' )
break
case 'network-error' :
console . warn ( 'Network error during session refresh' )
break
}
}
async init () {
const saved = localStorage . getItem ( this . storageKey )
if ( saved ) {
try {
const session = JSON . parse ( saved )
await this . agent . resumeSession ( session )
console . log ( 'Resumed session for:' , session . handle )
return true
} catch ( error ) {
console . error ( 'Failed to resume session:' , error )
localStorage . removeItem ( this . storageKey )
}
}
return false
}
async login ( identifier : string , password : string ) {
await this . agent . login ({ identifier , password })
return this . agent . session
}
async logout () {
await this . agent . logout ()
}
get isAuthenticated () {
return this . agent . hasSession
}
get api () {
return this . agent . api
}
}
// Usage
const sessionMgr = new SessionManager ()
// On app startup
await sessionMgr . init ()
if ( ! sessionMgr . isAuthenticated ) {
await sessionMgr . login ( 'alice.bsky.social' , 'hunter2' )
}
// Use API
await sessionMgr . api . app . bsky . feed . post . create (
{ repo: sessionMgr . agent . did },
{
text: 'Posted with session manager!' ,
createdAt: new Date (). toISOString ()
}
)
Additional Resources
@atproto/api Package NPM package documentation
XRPC Specification XRPC protocol specification
JWT.io Learn more about JSON Web Tokens
Server API Reference Authentication API endpoints