User Subdomain Overview (DDD Approach)#
This document outlines the Domain-Driven Design (DDD) approach for the User subdomain, focusing on authentication via ATProto OAuth.
Domain Layer#
Entities and Aggregates#
User (Aggregate Root)#
- Properties:
id: UniqueEntityID (based on the user's DID)did: DID (Value Object)handle: Handle (Value Object, optional)linkedAt: Date (when the account was first linked)lastLoginAt: Date (timestamp of last successful authentication)
Value Objects#
- DID: Represents a validated
did:plcstring- Validates format
- Provides methods for comparison and string representation
- Handle: Represents a validated Bluesky handle
- Validates format
- Provides methods for comparison and string representation
Domain Events#
- UserLinked: Fired when a user completes OAuth flow for the first time
- UserLoggedIn: Fired on successful authentication
Domain Services#
- UserAuthenticationService: Core domain logic for user authentication
- Validates authentication requests
- Manages user session state
Application Layer#
Use Cases / Application Services#
InitiateOAuthSignInUseCase#
- Input: Handle (optional)
- Output: Authorization URL
- Behavior: Prepares OAuth flow initiation
CompleteOAuthSignInUseCase#
- Input: OAuth callback parameters (code, state)
- Output: User session information
- Behavior:
- Processes OAuth callback
- Creates or updates User entity
- Dispatches domain events
GetCurrentUserUseCase#
- Input: Session context
- Output: User information
- Behavior: Retrieves authenticated user information
DTOs (Data Transfer Objects)#
- UserDTO: Represents user data for external consumers
- OAuthCallbackDTO: Contains parameters from OAuth callback
- SessionDTO: Contains session information
Repository Interfaces#
- IUserRepository:
findById(did: string): Promise<User | null>save(user: User): Promise<void>
Service Interfaces#
- IOAuthProcessor:
processCallback(params: OAuthCallbackDTO): Promise<AuthResult>generateAuthUrl(handle?: string): Promise<string>
- ITokenService:
generateToken(did: string): Promise<TokenPair>validateToken(token: string): Promise<string | null>// Returns DID if validrefreshToken(refreshToken: string): Promise<TokenPair | null>revokeToken(refreshToken: string): Promise<void>
Infrastructure Layer#
Persistence#
- UserRepository: Implements
IUserRepositoryusing database - SessionStore: Implements session storage for OAuth client
- StateStore: Implements state parameter storage for OAuth flow
Authentication#
- AtProtoOAuthProcessor: Implements
IOAuthProcessor- Wraps
NodeOAuthClientfrom@atproto/oauth-client-node - Handles OAuth flow and token management
- Wraps
- TokenService: Implements
ITokenService- Generates and validates JWT tokens for API authentication
- Manages token lifecycle and refresh
API / Controllers#
- AuthController:
/login: Initiates OAuth flow/oauth/callback: Handles OAuth callback/oauth/token: Issues JWT tokens after successful OAuth/oauth/refresh: Refreshes access token using refresh token/oauth/revoke: Revokes refresh token
- UserController:
/me: Returns current user information
- AuthMiddleware:
ensureAuthenticated: Validates JWT token and attaches user to request- Used across all protected routes in different subdomains
- Acts as a gatekeeper at the infrastructure layer
Implementation Details#
Authentication Flow#
- User initiates login via
/loginendpoint - System redirects to PDS for authentication
- PDS redirects back to
/oauth/callbackwith authorization code - System exchanges code for PDS tokens via
NodeOAuthClient - System creates/updates User entity and generates JWT tokens
- Client receives access and refresh tokens in response
- Client includes access token in subsequent API requests
- AuthMiddleware validates tokens for protected routes
Token Management#
- JWT-based authentication with access and refresh tokens
- Access tokens are short-lived (e.g., 1 hour)
- Refresh tokens are longer-lived (e.g., 30 days)
- Tokens contain minimal claims (primarily user DID)
- Full ATProto session restored as needed using stored PDS refresh tokens
Token Refresh#
- Access tokens automatically refreshed via
NodeOAuthClient - Refresh tokens securely stored in
SessionStore - Token refresh transparent to application code
Database Schema#
Users Table#
CREATE TABLE users (
id TEXT PRIMARY KEY, -- DID
handle TEXT,
linked_at TIMESTAMP NOT NULL,
last_login_at TIMESTAMP NOT NULL
);
Auth Tokens Table#
CREATE TABLE auth_refresh_tokens (
token_id TEXT PRIMARY KEY,
user_did TEXT NOT NULL,
refresh_token TEXT NOT NULL,
issued_at TIMESTAMP NOT NULL,
expires_at TIMESTAMP NOT NULL,
revoked BOOLEAN DEFAULT FALSE,
FOREIGN KEY (user_did) REFERENCES users(id)
);
Auth State Table#
CREATE TABLE auth_state (
key TEXT PRIMARY KEY, -- State parameter
state TEXT NOT NULL, -- JSON containing state data
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
Conclusion#
This DDD approach to the User subdomain provides a clean separation of concerns while maintaining a minimal set of abstractions. The domain model focuses on the core concept of a User linked to an external ATProto identity, while the infrastructure layer handles the complexities of OAuth authentication and token management.
The AuthMiddleware component serves as a critical cross-cutting concern that enforces authentication across all protected routes in the application. By implementing this middleware at the infrastructure layer, we maintain a clean separation between authentication logic and business logic, allowing other subdomains to focus on their core responsibilities while still benefiting from a consistent authentication mechanism.
For more details on the authentication middleware implementation, see auth-middleware.md.