@tijs/oauth-client-deno#
A Deno-compatible AT Protocol OAuth client built specifically for Deno environments using Web Crypto API. Built to solve crypto compatibility issues between Node.js-specific implementations and Deno runtime environments.
🎯 Opinionated Design#
This client makes specific design choices that may or may not fit your use case:
- Handle-focused: Accepts AT Protocol handles (
alice.bsky.social) only, not DIDs or URLs - Slingshot resolver: Uses Slingshot as the default handle resolver with fallbacks
- Deno-first: Built for Deno runtime, not a drop-in replacement for
@atproto/oauth-client-node - Web Crypto API: Uses modern web standards instead of Node.js crypto
This is perfect if you're building Deno applications with handle-based authentication. For more flexible input types (DIDs, URLs) or Node.js environments, use @atproto/oauth-client-node instead.
✨ Key Features#
- 🔒 Complete OAuth 2.0 + DPoP: Full AT Protocol authentication implementation
- 🛠️ Configurable Storage: Memory, LocalStorage, SQLite, or custom backends
- 🔄 Multiple Resolvers: Slingshot, Bluesky API, direct resolution with fallbacks
- 🚀 Production Ready: Comprehensive error handling, session management, and testing
- 📦 Zero Dependencies: Pure Web Standards implementation
🚀 Installation#
# npm
npm install @tijs/oauth-client-deno
# Deno
deno add jsr:@tijs/oauth-client-deno
🔄 vs @atproto/oauth-client-node#
| Feature | @tijs/oauth-client-deno | @atproto/oauth-client-node |
|---|---|---|
| Input Types | AT Protocol handles only | Handles, DIDs, PDS URLs, Entryway URLs |
| Runtime | Deno, Web Crypto API | Node.js, Node crypto |
| Return Types | URL objects, URLSearchParams |
URL objects, URLSearchParams |
| Storage | Memory, LocalStorage, SQLite, custom | Configurable |
📖 Quick Start#
Basic Usage#
import { MemoryStorage, OAuthClient } from "jsr:@tijs/oauth-client-deno";
// Initialize client
const client = new OAuthClient({
clientId: "https://yourapp.com/client-metadata.json",
redirectUri: "https://yourapp.com/oauth/callback",
storage: new MemoryStorage(),
});
// Start OAuth flow
const authUrl = await client.authorize("alice.bsky.social");
console.log("Redirect user to:", authUrl);
// Handle OAuth callback
const { session } = await client.callback({
code: "authorization_code_from_callback",
state: "state_parameter_from_callback",
});
// Make authenticated API requests
const response = await session.makeRequest(
"GET",
"https://bsky.social/xrpc/com.atproto.repo.listRecords",
);
const data = await response.json();
console.log("User records:", data);
Session Management#
// Store session for later use
const sessionId = "user-123";
await client.store(sessionId, session);
// Restore session (with automatic token refresh if needed)
try {
const restoredSession = await client.restore(sessionId);
console.log("Welcome back,", restoredSession.handle);
} catch (error) {
if (error instanceof SessionNotFoundError) {
console.log("Please log in again");
} else if (error instanceof RefreshTokenExpiredError) {
console.log("Session expired, please re-authenticate");
} else {
throw error; // Unexpected error
}
}
// Manual token refresh
if (session.isExpired) {
const refreshedSession = await client.refresh(session);
await client.store(sessionId, refreshedSession);
}
// Clean logout
await client.signOut(sessionId, session);
🔧 Configuration Options#
Storage Backends#
Choose from built-in storage options or implement your own:
// In-memory storage (development)
import { MemoryStorage } from "jsr:@tijs/oauth-client-deno";
const storage = new MemoryStorage();
// SQLite storage (for Deno CLI apps)
import { SQLiteStorage } from "jsr:@tijs/oauth-client-deno";
const storage = new SQLiteStorage(sqlite);
// localStorage (for browsers)
import { LocalStorage } from "jsr:@tijs/oauth-client-deno";
const storage = new LocalStorage();
// Custom storage implementation
const customStorage = {
async get(key) {/* your logic */},
async set(key, value, options) {/* your logic */},
async delete(key) {/* your logic */},
};
Handle Resolution#
Configure how AT Protocol handles are resolved to DIDs and PDS URLs. By default, this client uses Slingshot (https://slingshot.microcosm.blue) as the primary resolver with automatic fallbacks.
Default Behavior (Slingshot-first)#
import { CustomResolver, DirectoryResolver, SlingshotResolver } from "jsr:@tijs/oauth-client-deno";
// Default: Slingshot with fallbacks to directory and direct resolution
const client = new OAuthClient({
// ... other config
// Uses Slingshot resolver automatically with fallbacks
});
// Resolution order:
// 1. Slingshot resolveMiniDoc (https://slingshot.microcosm.blue/xrpc/com.bad-example.identity.resolveMiniDoc)
// 2. Slingshot standard (https://slingshot.microcosm.blue/xrpc/com.atproto.identity.resolveHandle)
// 3. Bluesky API (https://bsky.social/xrpc/com.atproto.identity.resolveHandle)
// 4. Direct handle lookup (https://handle/.well-known/atproto-did)
Alternative Resolution Strategies#
If Slingshot doesn't fit your use case, you can configure alternative resolvers:
// Custom Slingshot URL
const client = new OAuthClient({
// ... other config
slingshotUrl: "https://my-custom-slingshot.example.com",
});
// Use Bluesky API-first resolution (avoids Slingshot)
const client = new OAuthClient({
// ... other config
handleResolver: new DirectoryResolver(), // Only uses bsky.social API
});
// Completely custom resolution logic (full control)
const client = new OAuthClient({
// ... other config
handleResolver: new CustomResolver(async (handle) => {
// Your custom handle resolution logic
const did = await customResolveHandleToDid(handle);
const pdsUrl = await customResolvePdsUrl(did);
return { did, pdsUrl };
}),
});
Why Slingshot? Slingshot is a production-grade cache of AT Protocol data that provides faster handle resolution and better reliability, especially during high-traffic periods. It uses the
resolveMiniDocendpoint which returns both DID and PDS URL in a single request, reducing the need for multiple lookups. However, it does introduce a dependency on a third-party service. The fallback mechanisms ensure your application continues to work even if Slingshot is unavailable.
Logging#
Control client logging output by providing a custom logger:
import { ConsoleLogger, type Logger } from "jsr:@tijs/oauth-client-deno";
// Use built-in console logger for development
const client = new OAuthClient({
// ... other config
logger: new ConsoleLogger(),
});
// Or implement custom logger for production
class ProductionLogger implements Logger {
debug(message: string, ...args: unknown[]): void {
// Send to your logging service
}
info(message: string, ...args: unknown[]): void {
logger.log(message, ...args);
}
warn(message: string, ...args: unknown[]): void {
logger.warn(message, ...args);
}
error(message: string, ...args: unknown[]): void {
logger.error(message, ...args);
}
}
const client = new OAuthClient({
// ... other config
logger: new ProductionLogger(),
});
Note: By default, the client uses a no-op logger that produces no output.
🏗️ Advanced Usage#
Error Handling#
import {
HandleResolutionError,
InvalidHandleError,
OAuthError,
SessionError,
TokenExchangeError,
} from "jsr:@tijs/oauth-client-deno";
try {
const authUrl = await client.authorize("invalid.handle");
} catch (error) {
if (error instanceof InvalidHandleError) {
console.error("Handle format is invalid");
} else if (error instanceof HandleResolutionError) {
console.error("Could not resolve handle to DID/PDS");
} else {
console.error("Unexpected error:", error);
}
}
Mobile App Integration#
The client works seamlessly with mobile WebView implementations:
// Mobile-friendly configuration
const client = new OAuthClient({
clientId: "https://myapp.com/client-metadata.json",
redirectUri: "myapp://oauth/callback", // Custom URL scheme
storage: new MemoryStorage(), // or secure storage implementation
});
// Handle custom redirect in mobile app
const { session } = await client.callback(parsedCallbackParams);
🔍 API Reference#
OAuthClient#
Main OAuth client class for AT Protocol authentication.
Constructor Options#
interface OAuthClientConfig {
clientId: string; // Your OAuth client identifier
redirectUri: string; // Where users return after auth
storage: Storage; // Session storage implementation
handleResolver?: HandleResolver; // Custom handle resolution
slingshotUrl?: string; // Custom Slingshot URL
}
Methods#
authorize(handle: string, options?: AuthorizationUrlOptions): Promise<string>callback(params: CallbackParams): Promise<{ session: Session }>store(sessionId: string, session: Session): Promise<void>restore(sessionId: string): Promise<Session | null>refresh(session: Session): Promise<Session>signOut(sessionId: string, session: Session): Promise<void>
Session#
Authenticated user session with automatic token management.
Properties#
did: string- User's decentralized identifierhandle: string- User's AT Protocol handlepdsUrl: string- User's Personal Data Server URLaccessToken: string- Current OAuth access tokenrefreshToken: string- OAuth refresh tokenisExpired: boolean- Whether token needs refresh
Methods#
makeRequest(method: string, url: string, options?): Promise<Response>toJSON(): SessionData- Serialize for storageupdateTokens(tokens): void- Update with refreshed tokens
Storage Interface#
Implement this interface for custom storage backends:
interface Storage {
get<T>(key: string): Promise<T | null>;
set<T>(key: string, value: T, options?: { ttl?: number }): Promise<void>;
delete(key: string): Promise<void>;
}
🆚 Differences from @atproto/oauth-client-node#
| Feature | @atproto/oauth-client-node | @tijs/oauth-client-deno |
|---|---|---|
| Runtime | Node.js only | Deno, Browser, Web Standards |
| Crypto | Node.js crypto APIs | Web Crypto API (cross-platform) |
| Primary Use Case | Server-side Node.js apps | Deno apps, edge workers, browsers |
| Dependencies | Node.js built-ins + jose | Web Standards + jose (JSR) |
| Handle Resolution | Configurable resolvers | Slingshot-first with fallbacks |
| Storage | Flexible sessionStore interface | Simple Storage interface + built-ins |
Note: Both clients provide full AT Protocol OAuth + DPoP support and maintain API compatibility. The main difference is runtime compatibility - choose based on your deployment environment.
🔧 Development#
# Check code
deno check mod.ts
# Format code
deno fmt
# Lint code
deno lint
# Run tests
deno test --allow-net --allow-read
🤝 Contributing#
- Fork the repository
- Create a feature branch:
git checkout -b my-feature - Make changes and add tests
- Ensure all checks pass:
deno task check && deno task fmt && deno task lint - Commit changes:
git commit -am 'Add my feature' - Push to branch:
git push origin my-feature - Create a Pull Request
📄 License#
MIT License - see LICENSE file for details.
🧩 Why This Package Exists#
The official @atproto/oauth-client-node package has fundamental compatibility issues with Deno runtime environments. This package was created to solve these specific problems:
Root Cause Analysis#
-
Node.js-Specific Dependencies: The official package relies on Node.js-specific crypto dependencies (
@atproto/jwk-jose,@atproto/jwk-webcrypto) that don't work in Deno. -
Jose Library Compatibility: The underlying jose library (when used through Node.js-specific packages) throws
JOSENotSupported: Unsupported key curve for this operationerrors in Deno, specifically when generating ECDSA P-256 keys for DPoP (Demonstrating Proof of Possession) JWT operations. -
DPoP Implementation Problem: AT Protocol OAuth requires DPoP proofs using ES256 signatures with ECDSA P-256 curves. The Node.js crypto implementations in the official client don't translate to Deno's Web Crypto API properly.
Our Solution#
This package solves these issues by:
- Using Web Crypto API directly (
crypto.subtle.generateKey) instead of Node.js crypto - JSR-native jose imports (
jsr:@panva/jose) instead of Node.js-specific versions - Manual ECDSA P-256 key generation with explicit curve specification (
namedCurve: "P-256") - Direct DPoP JWT creation using Web Crypto compatible
SignJWToperations - Cross-platform compatibility that works in Deno, browsers, and other Web Standards environments
The implementation maintains full API compatibility with the original Node.js client while providing a native Web Standards foundation.
☕ Support Development#
If this package helps your app development, consider supporting on Ko-fi. Your support helps maintain and improve this package.
Acknowledgments#
This package implements the AT Protocol OAuth specification for Deno environments using Web Crypto APIs.
Based on specifications and patterns from:
- @atproto/oauth-client and @atproto/oauth-client-node (Copyright 2022-2025 Bluesky Social PBC, MIT and Apache 2.0)
- Bookhive OAuth implementation
Thanks to the Bluesky team for the AT Protocol ecosystem.
Related Packages#
- @tijs/atproto-oauth - Framework-agnostic OAuth integration for AT Protocol applications
- @tijs/atproto-storage - Storage implementations
- @tijs/atproto-sessions - Session cookie management