@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 +284 -0
- package/dist/index.d.ts +591 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1395 -0
- package/dist/index.js.map +1 -0
- package/package.json +52 -0
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
|