@getcirrus/oauth-provider 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,284 @@
1
+ # @getcirrus/oauth-provider
2
+
3
+ AT Protocol OAuth 2.1 Authorization Server for Cloudflare Workers.
4
+
5
+ A complete OAuth 2.1 provider implementation that enables "Login with Bluesky" functionality for your PDS. Built specifically for Cloudflare Workers with Durable Objects.
6
+
7
+ ## Features
8
+
9
+ - **OAuth 2.1 Authorization Code Flow** with PKCE (Proof Key for Code Exchange)
10
+ - **DPoP (Demonstrating Proof of Possession)** for token binding and enhanced security
11
+ - **PAR (Pushed Authorization Requests)** for secure authorization request initiation
12
+ - **Client Metadata Discovery** via `client_id` URL resolution
13
+ - **Token Management** - generation, rotation, and revocation
14
+ - **Storage Interface** - pluggable storage backend (SQLite adapter included)
15
+
16
+ ## Installation
17
+
18
+ ```bash
19
+ npm install @getcirrus/oauth-provider
20
+ # or
21
+ pnpm add @getcirrus/oauth-provider
22
+ ```
23
+
24
+ ## Quick Start
25
+
26
+ ```typescript
27
+ import { OAuthProvider } from "@getcirrus/oauth-provider";
28
+ import { OAuthStorage } from "./your-storage-implementation";
29
+
30
+ // Initialize the provider
31
+ const provider = new OAuthProvider({
32
+ issuer: "https://your-pds.example.com",
33
+ storage: new OAuthStorage(),
34
+ });
35
+
36
+ // Handle OAuth endpoints in your Worker
37
+ app.post("/oauth/par", async (c) => {
38
+ const result = await provider.handlePAR(await c.req.formData());
39
+ return c.json(result);
40
+ });
41
+
42
+ app.get("/oauth/authorize", async (c) => {
43
+ const result = await provider.handleAuthorize(c.req.url);
44
+ // Show authorization UI to user
45
+ return c.html(renderAuthUI(result));
46
+ });
47
+
48
+ app.post("/oauth/token", async (c) => {
49
+ const result = await provider.handleToken(
50
+ await c.req.formData(),
51
+ c.req.header("DPoP"),
52
+ );
53
+ return c.json(result);
54
+ });
55
+ ```
56
+
57
+ ## Architecture
58
+
59
+ ### Provider
60
+
61
+ The `OAuthProvider` class is the main entry point. It handles:
62
+
63
+ - Client metadata validation and discovery
64
+ - Authorization request processing (with PAR support)
65
+ - Token generation and validation
66
+ - DPoP proof verification
67
+ - PKCE challenge verification
68
+
69
+ ### Storage Interface
70
+
71
+ The provider uses a storage interface that you implement for your backend:
72
+
73
+ ```typescript
74
+ export interface OAuthProviderStorage {
75
+ // Authorization codes
76
+ saveAuthCode(code: string, data: AuthCodeData): Promise<void>;
77
+ getAuthCode(code: string): Promise<AuthCodeData | null>;
78
+ deleteAuthCode(code: string): Promise<void>;
79
+
80
+ // Access/refresh tokens
81
+ saveTokens(data: TokenData): Promise<void>;
82
+ getTokenByAccess(accessToken: string): Promise<TokenData | null>;
83
+ getTokenByRefresh(refreshToken: string): Promise<TokenData | null>;
84
+ revokeToken(accessToken: string): Promise<void>;
85
+ revokeAllTokens(sub: string): Promise<void>;
86
+
87
+ // Client metadata cache
88
+ saveClient(clientId: string, metadata: ClientMetadata): Promise<void>;
89
+ getClient(clientId: string): Promise<ClientMetadata | null>;
90
+
91
+ // PAR (Pushed Authorization Requests)
92
+ savePAR(requestUri: string, data: PARData): Promise<void>;
93
+ getPAR(requestUri: string): Promise<PARData | null>;
94
+ deletePAR(requestUri: string): Promise<void>;
95
+
96
+ // DPoP nonce tracking
97
+ checkAndSaveNonce(nonce: string): Promise<boolean>;
98
+ }
99
+ ```
100
+
101
+ A SQLite implementation for Durable Objects is included in the `@getcirrus/pds` package.
102
+
103
+ ## OAuth 2.1 Flow
104
+
105
+ ### 1. Pushed Authorization Request (PAR)
106
+
107
+ Client initiates the flow by pushing authorization parameters to the server:
108
+
109
+ ```http
110
+ POST /oauth/par
111
+ Content-Type: application/x-www-form-urlencoded
112
+
113
+ client_id=https://client.example.com/client-metadata.json
114
+ &code_challenge=XXXXXX
115
+ &code_challenge_method=S256
116
+ &redirect_uri=https://client.example.com/callback
117
+ &scope=atproto
118
+ &state=random-state
119
+ ```
120
+
121
+ Response:
122
+
123
+ ```json
124
+ {
125
+ "request_uri": "urn:ietf:params:oauth:request_uri:XXXXXX",
126
+ "expires_in": 90
127
+ }
128
+ ```
129
+
130
+ ### 2. Authorization
131
+
132
+ User is redirected to authorize the client:
133
+
134
+ ```http
135
+ GET /oauth/authorize?request_uri=urn:ietf:params:oauth:request_uri:XXXXXX
136
+ ```
137
+
138
+ After user approves, they're redirected back with an authorization code:
139
+
140
+ ```http
141
+ HTTP/1.1 302 Found
142
+ Location: https://client.example.com/callback?code=XXXXXX&state=random-state
143
+ ```
144
+
145
+ ### 3. Token Exchange
146
+
147
+ Client exchanges the authorization code for tokens:
148
+
149
+ ```http
150
+ POST /oauth/token
151
+ Content-Type: application/x-www-form-urlencoded
152
+ DPoP: <dpop-proof-jwt>
153
+
154
+ grant_type=authorization_code
155
+ &code=XXXXXX
156
+ &redirect_uri=https://client.example.com/callback
157
+ &code_verifier=YYYYYY
158
+ &client_id=https://client.example.com/client-metadata.json
159
+ ```
160
+
161
+ Response:
162
+
163
+ ```json
164
+ {
165
+ "access_token": "XXXXXX",
166
+ "token_type": "DPoP",
167
+ "expires_in": 3600,
168
+ "refresh_token": "YYYYYY",
169
+ "scope": "atproto",
170
+ "sub": "did:plc:abc123"
171
+ }
172
+ ```
173
+
174
+ ## Security Features
175
+
176
+ ### PKCE (Proof Key for Code Exchange)
177
+
178
+ All authorization flows require PKCE to prevent authorization code interception attacks:
179
+
180
+ - Client generates `code_verifier` (random string)
181
+ - Client sends SHA-256 hash as `code_challenge`
182
+ - Server verifies `code_verifier` matches during token exchange
183
+
184
+ ### DPoP (Demonstrating Proof of Possession)
185
+
186
+ Binds tokens to specific clients using cryptographic proofs:
187
+
188
+ - Client generates a key pair
189
+ - Client includes DPoP proof JWT with each token request
190
+ - Tokens are bound to the client's public key
191
+ - Prevents token theft and replay attacks
192
+
193
+ ### Replay Protection
194
+
195
+ - DPoP nonces are tracked to prevent replay attacks
196
+ - Authorization codes are single-use
197
+ - Refresh tokens can be rotated on each use
198
+
199
+ ## Client Metadata Discovery
200
+
201
+ Clients are identified by a URL pointing to their metadata document:
202
+
203
+ ```json
204
+ {
205
+ "client_id": "https://client.example.com/client-metadata.json",
206
+ "client_name": "Example App",
207
+ "redirect_uris": ["https://client.example.com/callback"],
208
+ "grant_types": ["authorization_code", "refresh_token"],
209
+ "response_types": ["code"],
210
+ "scope": "atproto",
211
+ "token_endpoint_auth_method": "none",
212
+ "application_type": "web"
213
+ }
214
+ ```
215
+
216
+ The provider automatically fetches and validates client metadata from the `client_id` URL.
217
+
218
+ ## Integration with @atproto/oauth-client
219
+
220
+ This provider is designed to work seamlessly with `@atproto/oauth-client`:
221
+
222
+ ```typescript
223
+ // Client side
224
+ import { OAuthClient } from "@atproto/oauth-client";
225
+
226
+ const client = new OAuthClient({
227
+ clientMetadata: {
228
+ client_id: "https://my-app.example.com/client-metadata.json",
229
+ redirect_uris: ["https://my-app.example.com/callback"],
230
+ },
231
+ });
232
+
233
+ // Initiate login
234
+ const authUrl = await client.authorize("https://user-pds.example.com", {
235
+ scope: "atproto",
236
+ });
237
+
238
+ // Handle callback
239
+ const { session } = await client.callback(callbackParams);
240
+ ```
241
+
242
+ ## Error Handling
243
+
244
+ The provider returns standard OAuth 2.1 error responses:
245
+
246
+ ```json
247
+ {
248
+ "error": "invalid_request",
249
+ "error_description": "Missing required parameter: code_challenge"
250
+ }
251
+ ```
252
+
253
+ Common error codes:
254
+
255
+ - `invalid_request` - Malformed request
256
+ - `invalid_client` - Client authentication failed
257
+ - `invalid_grant` - Invalid authorization code or refresh token
258
+ - `unauthorized_client` - Client not authorized for this grant type
259
+ - `unsupported_grant_type` - Grant type not supported
260
+ - `invalid_scope` - Requested scope is invalid
261
+
262
+ ## Testing
263
+
264
+ ```bash
265
+ pnpm test
266
+ ```
267
+
268
+ The package includes comprehensive tests for:
269
+
270
+ - Complete OAuth flows (PAR → authorize → token → refresh)
271
+ - PKCE verification
272
+ - DPoP proof validation
273
+ - Client metadata discovery
274
+ - Token rotation and revocation
275
+
276
+ ## License
277
+
278
+ MIT
279
+
280
+ ## Related Packages
281
+
282
+ - `@getcirrus/pds` - AT Protocol PDS implementation using this OAuth provider
283
+ - `@atproto/oauth-client` - Official AT Protocol OAuth client
284
+ - `@atproto/oauth-types` - TypeScript types for AT Protocol OAuth