# @atproto/lex-password-session Password-based session authentication for AT Protocol Lexicons. See the [Changelog](./CHANGELOG.md) for version history. ```bash npm install @atproto/lex-password-session ``` - Session management with automatic token refresh - Hooks for persisting and monitoring session state - PDS endpoint discovery from DID documents - Two-factor authentication support > [!IMPORTANT] > > This package is currently in **preview**. The API and features are subject to change before the stable release. **What is this?** `@atproto/lex-password-session` provides a `PasswordSession` class that implements the `Agent` interface from `@atproto/lex-client`. It handles password-based authentication with AT Protocol services, including: 1. Creating sessions with username/password credentials 2. Automatic token refresh when access tokens expire 3. Session persistence through lifecycle hooks 4. Graceful logout with server-side session cleanup ```typescript import { Client } from '@atproto/lex-client' import { PasswordSession } from '@atproto/lex-password-session' import * as app from './lexicons/app.js' // Login with credentials const session = await PasswordSession.login({ service: 'https://bsky.social', identifier: 'alice.bsky.social', password: 'app-password', onUpdated: (data) => saveToStorage(data), onDeleted: (data) => clearStorage(data.did), }) const client = new Client(session) // Make authenticated requests const profile = await client.call(app.bsky.actor.getProfile, { actor: session.did, }) ``` - [Quick Start](#quick-start) - [PasswordSession](#passwordsession) - [Login](#login) - [Two-Factor Authentication](#two-factor-authentication) - [Resume Session](#resume-session) - [Logout](#logout) - [Static Delete](#static-delete) - [Create Account](#create-account) - [Session Hooks](#session-hooks) - [onUpdated](#onupdated) - [onUpdateFailure](#onupdatefailure) - [onDeleted](#ondeleted) - [onDeleteFailure](#ondeletefailure) - [Session Data](#session-data) - [Error Handling](#error-handling) - [Using with Client](#using-with-client) - [License](#license) ## Quick Start **1. Install the package** ```bash npm install @atproto/lex-password-session @atproto/lex-client ``` **2. Login and make requests** ```typescript import { Client } from '@atproto/lex-client' import { PasswordSession } from '@atproto/lex-password-session' const session = await PasswordSession.login({ service: 'https://bsky.social', identifier: 'your-handle.bsky.social', password: 'your-app-password', }) const client = new Client(session) // Make authenticated API calls console.log('Logged in as:', session.did) ``` ## PasswordSession The `PasswordSession` class manages password-based authentication sessions. ### Login Create a new session with username and password: ```typescript import { PasswordSession } from '@atproto/lex-password-session' const session = await PasswordSession.login({ service: 'https://bsky.social', identifier: 'alice.bsky.social', // handle or email password: 'app-password', onUpdated: (data) => { // Persist session for later restoration localStorage.setItem('session', JSON.stringify(data)) }, onDeleted: () => { localStorage.removeItem('session') }, }) console.log('Logged in as:', session.did) ``` The `login()` method throws on failure. For expected errors like invalid credentials, an `XrpcResponseError` is thrown. For 2FA requirements, a `LexAuthFactorError` is thrown. ### Two-Factor Authentication > [!CAUTION] > > Two-factor authentication only applies when using **main account credentials**, which is **strongly discouraged**. Password authentication should be used with [app passwords](https://bsky.app/settings/app-passwords) only because they are designed for programmatic access (bots, scripts, CLI tools). For user-facing applications, use OAuth via [@atproto/oauth-client](../../../oauth/oauth-client) which provides better security and user control. If the account has 2FA enabled, login will throw a `LexAuthFactorError`: ```typescript import { PasswordSession, LexAuthFactorError, } from '@atproto/lex-password-session' async function loginWith2FA( identifier: string, password: string, authFactorToken?: string, ): Promise { try { return await PasswordSession.login({ service: 'https://bsky.social', identifier, password, authFactorToken, onUpdated: (data) => saveToStorage(data), onDeleted: (data) => removeFromStorage(data.did), }) } catch (err) { if (err instanceof LexAuthFactorError && !authFactorToken) { // 2FA required - prompt user for code const token = await promptUserFor2FACode(err.message) return loginWith2FA(identifier, password, token) } throw err } } ``` ### Resume Session Restore a previously saved session: ```typescript import { PasswordSession, SessionData } from '@atproto/lex-password-session' // Load session from storage const savedSession: SessionData = JSON.parse(localStorage.getItem('session')!) // Resume the session (automatically refreshes tokens) const session = await PasswordSession.resume(savedSession, { onUpdated: (data) => { localStorage.setItem('session', JSON.stringify(data)) }, onDeleted: () => { localStorage.removeItem('session') }, }) console.log('Session resumed for:', session.did) // Access session properties console.log(session.did) // User's DID console.log(session.handle) // User's handle console.log(session.destroyed) // false (session is active) ``` > [!NOTE] > > `resume()` automatically calls `refresh()` to ensure the session is valid and tokens are current. ### Logout End the session and notify the server: ```typescript await session.logout() ``` After logout: - The `onDeleted` hook is called - The session is marked as destroyed (`session.destroyed === true`) - Further requests will throw `'Logged out'` ### Static Delete Delete a session without creating a session instance: ```typescript import { PasswordSession, SessionData } from '@atproto/lex-password-session' const data: SessionData = JSON.parse(localStorage.getItem('session')!) // Delete the session on the server await PasswordSession.delete(data) ``` This is useful for cleanup scenarios where you don't need to make additional requests. ### Create Account Create a new account and get an authenticated session: ```typescript import { PasswordSession } from '@atproto/lex-password-session' const session = await PasswordSession.createAccount( { handle: 'alice.bsky.social', email: 'alice@example.com', password: 'secure-password', }, { service: 'https://bsky.social', onUpdated: (data) => saveToStorage(data), onDeleted: (data) => removeFromStorage(data.did), }, ) console.log('Account created:', session.did) ``` ## Session Hooks Hooks provide callbacks for session lifecycle events. All hooks receive the session instance as `this` context. ### onUpdated Called when the session is successfully created or refreshed: ```typescript const session = await PasswordSession.login({ service: 'https://bsky.social', identifier: 'alice.bsky.social', password: 'app-password', onUpdated(data) { // `this` is the PasswordSession instance console.log('Session updated for:', this.did) // Persist the updated session saveSession(data) }, }) ``` > [!IMPORTANT] > > Requests are blocked while `onUpdated` is running. Keep this callback fast to avoid delays. ### onUpdateFailure Called when token refresh fails due to transient errors (network issues, server unavailability): ```typescript { onUpdateFailure(data, error) { console.warn('Token refresh failed:', error.message) // Session may still be valid - consider retry logic } } ``` ### onDeleted Called when the session is terminated (logout or server-side invalidation): ```typescript { onDeleted(data) { console.log('Session ended for:', data.did) clearPersistedSession(data.did) redirectToLogin() } } ``` ### onDeleteFailure Called when logout fails due to transient errors: ```typescript { onDeleteFailure(data, error) { console.error('Logout failed:', error.message) // Consider queuing for retry to avoid orphaned sessions queueLogoutRetry(data) } } ``` > [!WARNING] > > Ignoring delete failures can leave sessions active on the server. Implement retry logic for security-sensitive applications. ## Session Data The `SessionData` type contains all data needed to authenticate and restore sessions: ```typescript type SessionData = { // Session credentials and user info from createSession response accessJwt: string refreshJwt: string did: string handle: string email?: string emailConfirmed?: boolean didDoc?: object // ... other fields from createSession // Original service URL used for login service: string } ``` ## Error Handling The `PasswordSession` class uses exception-based error handling: ```typescript import { PasswordSession, LexAuthFactorError, } from '@atproto/lex-password-session' import { XrpcResponseError } from '@atproto/lex-client' try { const session = await PasswordSession.login({ service: 'https://bsky.social', identifier: 'alice.bsky.social', password: 'wrong-password', }) } catch (err) { if (err instanceof LexAuthFactorError) { console.error('2FA required') } else if (err instanceof XrpcResponseError) { switch (err.error) { case 'AuthenticationRequired': console.error('Invalid credentials') break case 'AccountTakedown': console.error('Account has been suspended') break default: console.error('Login failed:', err.message) } } else { throw err } } ``` Common error codes: | Error Code | Description | | ------------------------- | ------------------------------ | | `AuthenticationRequired` | Invalid username or password | | `AuthFactorTokenRequired` | 2FA code needed | | `AccountTakedown` | Account suspended | | `ExpiredToken` | Token has expired (on refresh) | | `InvalidToken` | Token is invalid | ## Using with Client The `PasswordSession` implements the `Agent` interface and can be used directly with `Client`: ```typescript import { Client } from '@atproto/lex-client' import { PasswordSession } from '@atproto/lex-password-session' import * as app from './lexicons/app.js' const session = await PasswordSession.login({ service: 'https://bsky.social', identifier: 'alice.bsky.social', password: 'app-password', }) const client = new Client(session) // The client automatically uses the session for authentication const profile = await client.call(app.bsky.actor.getProfile, { actor: client.assertDid, }) // Tokens are automatically refreshed when expired const timeline = await client.call(app.bsky.feed.getTimeline, { limit: 50, }) // Create records await client.create(app.bsky.feed.post, { text: 'Hello from lex-password-session!', createdAt: new Date().toISOString(), }) ``` The session handles: - Adding `Authorization` headers to requests - Detecting expired tokens (401 responses or `ExpiredToken` errors) - Automatically refreshing tokens and retrying failed requests - Routing requests to the correct PDS based on DID document ## License MIT or Apache2