Skip to main content

Authentication and Authorization Developer Guide

Version: 1.0.0 Phase: 10 - Authentication and Authorization Last Updated: December 2025


Overview

Chive implements a comprehensive authentication and authorization system built on ATProto's DID-based identity model. The system provides:

  1. DID-based Authentication: Identity verification via AT Protocol DIDs
  2. JWT Session Management: Stateless access tokens with Redis-backed sessions
  3. RBAC Authorization: Role-based access control via Casbin
  4. OAuth 2.0 + PKCE: Secure third-party application authorization
  5. Multi-factor Authentication: TOTP and backup codes
  6. WebAuthn/Passkeys: Passwordless authentication
  7. Zero Trust Architecture: NIST SP 800-207 compliant security model

Architecture

Directory Structure

src/auth/
├── index.ts # Barrel exports
├── errors.ts # Auth-specific error types
├── authentication-service.ts # Main authentication orchestration
├── did/
│ ├── did-resolver.ts # DID document resolution
│ ├── did-verifier.ts # Signature verification
│ └── index.ts
├── jwt/
│ ├── jwt-service.ts # Token issuance/verification
│ ├── key-manager.ts # ES256 key management
│ └── index.ts
├── session/
│ ├── session-manager.ts # Redis session store
│ └── index.ts
├── authorization/
│ ├── authorization-service.ts # Casbin RBAC
│ ├── policies/
│ │ └── model.conf # Casbin model
│ └── index.ts
├── oauth/
│ ├── oauth-service.ts # OAuth 2.0 flows
│ ├── pkce.ts # PKCE utilities
│ └── index.ts
├── webauthn/
│ ├── webauthn-service.ts # Passkey operations
│ └── index.ts
├── mfa/
│ ├── mfa-service.ts # MFA orchestration
│ └── index.ts
└── zero-trust/
├── zero-trust-service.ts # Trust evaluation
└── index.ts

Quick Start

Authenticating Users

import { AuthenticationService } from '@/auth/authentication-service.js';
import { DIDResolver } from '@/auth/did/did-resolver.js';
import { JWTService } from '@/auth/jwt/jwt-service.js';
import { SessionManager } from '@/auth/session/session-manager.js';

// Create services
const didResolver = new DIDResolver({ redis, logger });
const jwtService = new JWTService({ keyManager, redis, logger });
const sessionManager = new SessionManager({ redis, logger });

const authService = new AuthenticationService({
didResolver,
jwtService,
sessionManager,
logger,
});

// Authenticate with DID
const result = await authService.authenticate({
did: 'did:plc:abc123',
credential: {
type: 'app_password',
value: 'xxxx-xxxx-xxxx-xxxx',
},
metadata: {
ipAddress: '192.168.1.1',
userAgent: 'Mozilla/5.0...',
},
});

// Result includes session and tokens
console.log(result.session.id);
console.log(result.accessToken);
console.log(result.refreshToken);

Protecting Routes

import { authenticate, requirePermission } from '@/api/middleware/auth.js';

const app = new Hono<ChiveEnv>();

// Require authentication
app.use('/api/*', authenticate());

// Require specific permission
app.post('/api/v1/preprints', requirePermission('preprint', 'create'), async (c) => {
const user = c.get('user');
// User is authenticated and has preprint:create permission
});

DID Resolution

Overview

Chive resolves ATProto DIDs to retrieve identity documents containing public keys for signature verification.

Supported DID Methods

MethodFormatResolution
did:plcdid:plc:abc123...PLC Directory lookup
did:webdid:web:example.comHTTPS .well-known fetch

Usage

import { DIDResolver } from '@/auth/did/did-resolver.js';

const resolver = new DIDResolver({
redis,
logger,
config: {
plcDirectoryUrl: 'https://plc.directory',
cacheTtlSeconds: 300, // 5 minutes
},
});

// Resolve DID document
const didDocument = await resolver.resolve('did:plc:abc123');

// Get verification methods
const verificationMethods = didDocument.verificationMethod;

// Get PDS endpoint
const pdsEndpoint = await resolver.getPDSEndpoint('did:plc:abc123');

Caching

DID documents are cached in Redis with configurable TTL to reduce resolution latency and external API calls.

Key format: chive:did:document:{did}
Default TTL: 300 seconds (5 minutes)

JWT Service

Token Structure

Chive issues ES256-signed JWTs with the following claims:

ClaimDescription
subUser DID
issToken issuer (https://chive.pub)
audToken audience (https://chive.pub)
iatIssued at timestamp
expExpiration timestamp
jtiUnique token ID
sessionIdAssociated session ID
scopeSpace-separated scopes

Issuing Tokens

import { JWTService } from '@/auth/jwt/jwt-service.js';
import { KeyManager } from '@/auth/jwt/key-manager.js';

const keyManager = new KeyManager({ redis, logger });
const jwtService = new JWTService({
keyManager,
redis,
logger,
config: {
issuer: 'https://chive.pub',
audience: 'https://chive.pub',
accessTokenExpirationSeconds: 3600, // 1 hour
},
});

const { token, jti, expiresAt } = await jwtService.issueToken({
subject: 'did:plc:abc123',
sessionId: 'sess_xyz',
scopes: ['read:preprints', 'write:reviews'],
});

Verifying Tokens

try {
const { claims } = await jwtService.verifyToken(token);
console.log(claims.sub); // did:plc:abc123
console.log(claims.sessionId); // sess_xyz
console.log(claims.scope); // 'read:preprints write:reviews'
} catch (error) {
if (error instanceof TokenExpiredError) {
// Token has expired
} else if (error instanceof TokenValidationError) {
// Token is invalid
}
}

Token Revocation

// Revoke a specific token
await jwtService.revokeToken(jti, expiresAt);

// Token is now blacklisted until expiration
await jwtService.verifyToken(token); // Throws TokenRevokedError

Key Rotation

Keys are automatically rotated based on configuration:

const keyManager = new KeyManager({
redis,
logger,
config: {
rotationIntervalSeconds: 7776000, // 90 days
overlapPeriodSeconds: 86400, // 24 hours
},
});

During the overlap period, both old and new keys are valid for verification, ensuring seamless rotation.


Session Management

Session Lifecycle

Create → Active → (Update) → Revoke

Sliding Expiration

Creating Sessions

import { SessionManager } from '@/auth/session/session-manager.js';

const sessionManager = new SessionManager({
redis,
logger,
config: {
sessionExpirationSeconds: 2592000, // 30 days
maxSessionsPerUser: 10,
},
});

const session = await sessionManager.createSession('did:plc:abc123', {
ipAddress: '192.168.1.1',
userAgent: 'Mozilla/5.0...',
deviceId: 'device_xyz',
scope: ['read:preprints', 'write:reviews'],
});

Session Operations

// Get session
const session = await sessionManager.getSession(sessionId);

// Update session (sliding expiration)
await sessionManager.updateSession(sessionId, {
lastActivity: new Date(),
});

// Revoke single session
await sessionManager.revokeSession(sessionId);

// Revoke all sessions (logout everywhere)
await sessionManager.revokeAllSessions('did:plc:abc123');

// List active sessions
const sessions = await sessionManager.listSessions('did:plc:abc123');

Redis Storage

Session key:     chive:session:{sessionId}
User index: chive:user:sessions:{did}
Token blacklist: chive:token:revoked:{jti}

Authorization (RBAC)

Role Hierarchy

admin

moderator

authority-editor

author

reader

Permission Model

Permissions follow the format {resource}:{action}:

ResourceActions
preprintcreate, read, update, delete, admin
reviewcreate, read, update, delete
graphpropose, vote, approve, admin
userread, update, admin

Usage

import { AuthorizationService } from '@/auth/authorization/authorization-service.js';

const authzService = new AuthorizationService({ redis, logger });
await authzService.initialize();

// Assign role
await authzService.assignRole('did:plc:abc123', 'author');

// Check authorization
const result = await authzService.authorize({
subject: { did: 'did:plc:abc123', roles: ['author'] },
action: 'create',
resource: { type: 'preprint' },
});

if (result.allowed) {
// Proceed with action
} else {
// Access denied: result.reason
}

// Resource owner check
const ownerResult = await authzService.authorize({
subject: { did: 'did:plc:abc123', roles: ['author'] },
action: 'update',
resource: {
type: 'preprint',
ownerDid: 'did:plc:abc123', // Same as subject
},
});
// ownerResult.allowed === true
// ownerResult.reason === 'resource_owner'

Casbin Policy Model

[request_definition]
r = sub, obj, act

[policy_definition]
p = sub, obj, act

[role_definition]
g = _, _

[policy_effect]
e = some(where (p.eft == allow))

[matchers]
m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act

OAuth 2.0 + PKCE

Supported Flows

  • Authorization Code with PKCE (recommended)
  • Client Credentials (service-to-service)

Authorization Flow

import { OAuthService } from '@/auth/oauth/oauth-service.js';

const oauthService = new OAuthService({ redis, logger });

// 1. Generate authorization URL
const { authorizationUrl, state, codeVerifier } = await oauthService.startAuthorization({
clientId: 'client_abc123',
redirectUri: 'https://app.example.com/callback',
scope: ['read:preprints'],
codeChallengeMethod: 'S256',
});

// 2. User authorizes, receives code at callback

// 3. Exchange code for tokens
const tokens = await oauthService.exchangeCode({
code: 'auth_code_xyz',
clientId: 'client_abc123',
redirectUri: 'https://app.example.com/callback',
codeVerifier,
});

Client Registration

const client = await oauthService.registerClient({
name: 'My Research App',
redirectUris: ['https://app.example.com/callback'],
clientType: 'public',
scopes: ['read:preprints', 'write:reviews'],
ownerDid: 'did:plc:abc123',
});

WebAuthn (Passkeys)

Registration Flow

import { WebAuthnService } from '@/auth/webauthn/webauthn-service.js';

const webauthnService = new WebAuthnService({
redis,
logger,
config: {
rpId: 'chive.pub',
rpName: 'Chive',
origin: 'https://chive.pub',
},
});

// 1. Generate registration challenge
const challenge = await webauthnService.generateRegistrationChallenge('did:plc:abc123');

// 2. Client creates credential (browser WebAuthn API)

// 3. Verify registration
const credential = await webauthnService.verifyRegistration('did:plc:abc123', {
challenge,
credential: clientResponse,
});

Authentication Flow

// 1. Generate authentication challenge
const challenge = await webauthnService.generateAuthenticationChallenge('did:plc:abc123');

// 2. Client authenticates (browser WebAuthn API)

// 3. Verify authentication
const result = await webauthnService.verifyAuthentication('did:plc:abc123', {
challenge,
credential: clientResponse,
});

Multi-Factor Authentication

TOTP Enrollment

import { MFAService } from '@/auth/mfa/mfa-service.js';

const mfaService = new MFAService({ redis, logger });

// 1. Start enrollment
const { secret, qrCodeUri } = await mfaService.enrollTOTP('did:plc:abc123');

// 2. User scans QR code in authenticator app

// 3. Verify enrollment with code
await mfaService.verifyTOTPEnrollment('did:plc:abc123', '123456');

Verification

// Verify TOTP or backup code
const result = await mfaService.verifyMFA('did:plc:abc123', {
type: 'totp',
code: '123456',
});

if (!result.success) {
console.log(result.error); // 'invalid_code' | 'expired_code' | etc.
}

Backup Codes

// Generate backup codes
const codes = await mfaService.regenerateBackupCodes('did:plc:abc123');
// Returns array of 10 single-use codes

// Verify backup code
const result = await mfaService.verifyMFA('did:plc:abc123', {
type: 'backup_code',
code: 'XXXX-XXXX-XXXX',
});

Zero Trust Architecture

Trust Evaluation

import { ZeroTrustService } from '@/auth/zero-trust/zero-trust-service.js';

const zeroTrustService = new ZeroTrustService({ redis, logger });

const decision = await zeroTrustService.evaluate({
subject: {
did: 'did:plc:abc123',
sessionId: 'sess_xyz',
authMethod: 'webauthn',
authTime: new Date(),
mfaVerified: true,
roles: ['author'],
},
resource: {
type: 'preprint',
sensitivity: 'high',
},
context: {
ipAddress: '192.168.1.1',
userAgent: 'Mozilla/5.0...',
deviceId: 'device_xyz',
requestTime: new Date(),
geoLocation: 'US',
},
});

if (decision.allow) {
// Proceed with request
} else {
// decision.reason explains why access was denied
// decision.requiredActions lists remediation steps
}

Trust Score Components

ComponentWeightDescription
Authentication0.3Auth method strength, MFA status
Device Posture0.25Known device, security state
Behavior Analysis0.25Anomaly detection, risk signals
Network Context0.2IP reputation, geolocation

Error Handling

Error Types

import {
AuthenticationError,
TokenValidationError,
TokenExpiredError,
SessionRevokedError,
MFARequiredError,
WebAuthnError,
} from '@/auth/errors.js';

Error Codes

CodeDescription
invalid_credentialsCredentials verification failed
invalid_signatureJWT signature verification failed
token_expiredJWT has expired
token_revokedJWT has been revoked
session_revokedSession has been revoked
mfa_requiredMFA verification required
invalid_codeMFA code is invalid

Database Schema

PostgreSQL Tables

-- User roles (RBAC)
CREATE TABLE user_roles (
did TEXT NOT NULL,
role TEXT NOT NULL,
assigned_at TIMESTAMPTZ DEFAULT NOW(),
assigned_by TEXT,
expires_at TIMESTAMPTZ,
PRIMARY KEY (did, role)
);

-- OAuth clients
CREATE TABLE oauth_clients (
client_id TEXT PRIMARY KEY,
client_secret_hash TEXT,
client_type TEXT NOT NULL,
name TEXT NOT NULL,
redirect_uris TEXT[] NOT NULL,
scopes TEXT[] NOT NULL,
owner_did TEXT NOT NULL,
is_active BOOLEAN DEFAULT true,
created_at TIMESTAMPTZ DEFAULT NOW()
);

-- WebAuthn credentials
CREATE TABLE webauthn_credentials (
credential_id TEXT PRIMARY KEY,
did TEXT NOT NULL,
public_key TEXT NOT NULL,
counter BIGINT DEFAULT 0,
transports TEXT[],
nickname TEXT,
created_at TIMESTAMPTZ DEFAULT NOW()
);

-- MFA enrollments
CREATE TABLE mfa_enrollments (
did TEXT PRIMARY KEY,
totp_secret_encrypted TEXT,
totp_enabled BOOLEAN DEFAULT false,
backup_codes_hash TEXT[],
created_at TIMESTAMPTZ DEFAULT NOW()
);

-- Audit log
CREATE TABLE audit_log (
id BIGSERIAL PRIMARY KEY,
event_type TEXT NOT NULL,
actor_did TEXT,
action TEXT NOT NULL,
result TEXT NOT NULL,
ip_address INET,
details JSONB,
created_at TIMESTAMPTZ DEFAULT NOW()
);

Testing

Unit Tests

# Run auth unit tests
npm run test:unit -- tests/unit/auth/

# Test specific service
npm run test:unit -- tests/unit/auth/jwt-service.test.ts

Integration Tests

# Run auth integration tests
npm run test:integration -- tests/integration/auth/

Test Fixtures

import { createMockLogger, createMockRedis } from '@tests/fixtures/mocks.js';

const logger = createMockLogger();
const redis = createMockRedis();

const service = new JWTService({ keyManager, redis, logger });

Security Considerations

Token Security

  • Access tokens expire after 1 hour
  • Refresh tokens are single-use
  • Token revocation is immediate via Redis blacklist
  • ES256 algorithm with 256-bit keys

Session Security

  • Sessions expire after 30 days of inactivity
  • Maximum 10 sessions per user
  • IP and user agent tracked for anomaly detection
  • Session revocation cascades to all tokens

MFA Security

  • TOTP secrets encrypted at rest (AES-256-GCM)
  • Backup codes hashed with SHA-256
  • Rate limiting on verification attempts
  • Lockout after repeated failures

WebAuthn Security

  • Credential counter tracking for replay detection
  • Attestation verification (none, direct)
  • Challenge expiration (5 minutes)