@atcute/oauth-browser-client 1.0.3 → 1.0.5
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 +296 -6
- package/dist/agents/server-agent.js +7 -6
- package/dist/agents/server-agent.js.map +1 -1
- package/dist/store/db.js +1 -1
- package/dist/store/db.js.map +1 -1
- package/lib/agents/exchange.ts +115 -0
- package/lib/agents/server-agent.ts +149 -0
- package/lib/agents/sessions.ts +142 -0
- package/lib/agents/user-agent.ts +99 -0
- package/lib/constants.ts +1 -0
- package/lib/dpop.ts +154 -0
- package/lib/environment.ts +27 -0
- package/lib/errors.ts +76 -0
- package/lib/index.ts +17 -0
- package/lib/resolvers.ts +222 -0
- package/lib/store/db.ts +184 -0
- package/lib/types/client.ts +82 -0
- package/lib/types/dpop.ts +7 -0
- package/lib/types/identity.ts +7 -0
- package/lib/types/par.ts +4 -0
- package/lib/types/server.ts +67 -0
- package/lib/types/store.ts +6 -0
- package/lib/types/token.ts +46 -0
- package/lib/utils/misc.ts +14 -0
- package/lib/utils/response.ts +3 -0
- package/lib/utils/runtime.ts +55 -0
- package/lib/utils/strings.ts +5 -0
- package/package.json +9 -5
package/README.md
CHANGED
|
@@ -5,16 +5,12 @@ minimal OAuth browser client implementation for AT Protocol.
|
|
|
5
5
|
- **only the bare minimum**: enough code to get authentication reasonably working, with only one
|
|
6
6
|
happy path is supported (only ES256 keys for DPoP. PKCE and DPoP-bound PAR is required.)
|
|
7
7
|
- **does not use IndexedDB**: makes the library work under Safari's lockdown mode, and has less
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
- **no independent DNS/HTTP handle checks**: the default handle resolver makes use of Bluesky's
|
|
11
|
-
AppView to retrieve the correct DID identifier. you should be able to write your own resolver
|
|
12
|
-
function that'll resolve via DNS-over-HTTPS or via other PDSes.
|
|
8
|
+
maintenance headache overall, but it also means this is "less secure" (it won't be able to use
|
|
9
|
+
non-exportable keys as recommended by [DPoP specification][idb-dpop-spec].)
|
|
13
10
|
- **not well-tested**: it has been used in personal projects for quite some time, but hasn't seen
|
|
14
11
|
any use outside of that. using the [reference implementation][oauth-atproto-lib] is recommended if
|
|
15
12
|
you are unsure about the implications presented here.
|
|
16
13
|
|
|
17
|
-
[indexeddb-woes]: https://gist.github.com/pesterhazy/4de96193af89a6dd5ce682ce2adff49a
|
|
18
14
|
[idb-dpop-spec]: https://datatracker.ietf.org/doc/html/rfc9449#section-2-4
|
|
19
15
|
[oauth-atproto-lib]: https://npm.im/@atproto/oauth-client-browser
|
|
20
16
|
|
|
@@ -38,6 +34,14 @@ configureOAuth({
|
|
|
38
34
|
|
|
39
35
|
### starting an authorization flow
|
|
40
36
|
|
|
37
|
+
> [!CAUTION]
|
|
38
|
+
> the built-in handle resolution makes use of Bluesky-hosted services to return the intended DID,
|
|
39
|
+
> and this can mean sharing the user's IP address and the handle identifiers to Bluesky.
|
|
40
|
+
>
|
|
41
|
+
> while Bluesky has a declared privacy policy, both developers and users need to be informed and
|
|
42
|
+
> aware of the privacy implications of this arrangement. read [this guide](#doing-handle-resolution)
|
|
43
|
+
> on how you can implement your own resolution code.
|
|
44
|
+
|
|
41
45
|
if your application involves asking for the user's handle or DID, you can use `resolveFromIdentity`
|
|
42
46
|
which resolves the user's identity to get its PDS, and the metadata of its authorization server.
|
|
43
47
|
|
|
@@ -157,3 +161,289 @@ try {
|
|
|
157
161
|
deleteStoredSession(did);
|
|
158
162
|
}
|
|
159
163
|
```
|
|
164
|
+
|
|
165
|
+
## additional guide
|
|
166
|
+
|
|
167
|
+
### configuring your Vite project
|
|
168
|
+
|
|
169
|
+
you might want to configure the server options in your Vite config so you'll never end up visiting
|
|
170
|
+
your app in `localhost`, which is specifically forbidden by AT Protocol's OAuth, let's change it so
|
|
171
|
+
it'll always use `127.0.0.1`:
|
|
172
|
+
|
|
173
|
+
```ts
|
|
174
|
+
/// vite.config.ts
|
|
175
|
+
import { defineConfig } from 'vite';
|
|
176
|
+
|
|
177
|
+
const SERVER_HOST = '127.0.0.1';
|
|
178
|
+
const SERVER_PORT = 12520;
|
|
179
|
+
|
|
180
|
+
export default defineConfig({
|
|
181
|
+
server: {
|
|
182
|
+
host: SERVER_HOST,
|
|
183
|
+
port: SERVER_PORT,
|
|
184
|
+
},
|
|
185
|
+
});
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
additionally, to make it easier to develop locally and deploy to production, you should consider
|
|
189
|
+
adding a plugin that'll inject the necessary values for you through environment variables:
|
|
190
|
+
|
|
191
|
+
```ts
|
|
192
|
+
/// vite.config.ts
|
|
193
|
+
import metadata from './public/oauth/client-metadata.json' with { type: 'json' };
|
|
194
|
+
|
|
195
|
+
export default defineConfig({
|
|
196
|
+
// ...
|
|
197
|
+
|
|
198
|
+
plugins: [
|
|
199
|
+
// injects OAuth-related environment variables
|
|
200
|
+
{
|
|
201
|
+
config(_conf, { command }) {
|
|
202
|
+
if (command === 'build') {
|
|
203
|
+
process.env.VITE_OAUTH_CLIENT_ID = metadata.client_id;
|
|
204
|
+
process.env.VITE_OAUTH_REDIRECT_URI = metadata.redirect_uris[0];
|
|
205
|
+
} else {
|
|
206
|
+
const redirectUri = (() => {
|
|
207
|
+
const url = new URL(metadata.redirect_uris[0]);
|
|
208
|
+
return `http://${SERVER_HOST}:${SERVER_PORT}${url.pathname}`;
|
|
209
|
+
})();
|
|
210
|
+
|
|
211
|
+
const clientId =
|
|
212
|
+
`http://localhost` +
|
|
213
|
+
`?redirect_uri=${encodeURIComponent(redirectUri)}` +
|
|
214
|
+
`&scope=${encodeURIComponent(metadata.scope)}`;
|
|
215
|
+
|
|
216
|
+
process.env.VITE_DEV_SERVER_PORT = '' + SERVER_PORT;
|
|
217
|
+
process.env.VITE_OAUTH_CLIENT_ID = clientId;
|
|
218
|
+
process.env.VITE_OAUTH_REDIRECT_URI = redirectUri;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
process.env.VITE_CLIENT_URI = metadata.client_uri;
|
|
222
|
+
process.env.VITE_OAUTH_SCOPE = metadata.scope;
|
|
223
|
+
},
|
|
224
|
+
},
|
|
225
|
+
],
|
|
226
|
+
});
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
we'll augment the type declarations to get type-checking on it:
|
|
230
|
+
|
|
231
|
+
```ts
|
|
232
|
+
/// src/vite-env.d.ts
|
|
233
|
+
|
|
234
|
+
interface ImportMetaEnv {
|
|
235
|
+
readonly VITE_DEV_SERVER_PORT?: string;
|
|
236
|
+
readonly VITE_CLIENT_URI: string;
|
|
237
|
+
readonly VITE_OAUTH_CLIENT_ID: string;
|
|
238
|
+
readonly VITE_OAUTH_REDIRECT_URI: string;
|
|
239
|
+
readonly VITE_OAUTH_SCOPE: string;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
interface ImportMeta {
|
|
243
|
+
readonly env: ImportMetaEnv;
|
|
244
|
+
}
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
et voilà! you can now use this to configure the client.
|
|
248
|
+
|
|
249
|
+
```ts
|
|
250
|
+
configureOAuth({
|
|
251
|
+
metadata: {
|
|
252
|
+
client_id: import.meta.env.VITE_OAUTH_CLIENT_ID,
|
|
253
|
+
redirect_uri: import.meta.env.VITE_OAUTH_REDIRECT_URI,
|
|
254
|
+
},
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
// ... later during sign-in process
|
|
258
|
+
const authUrl = await createAuthorizationUrl({
|
|
259
|
+
metadata: metadata,
|
|
260
|
+
identity: identity,
|
|
261
|
+
scope: import.meta.env.VITE_OAUTH_SCOPE,
|
|
262
|
+
});
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
adjust the code here as necessary, the plugin adds more environment variables than what is actually
|
|
266
|
+
needed, you can remove them if you don't think you'd need it.
|
|
267
|
+
|
|
268
|
+
### doing handle resolution
|
|
269
|
+
|
|
270
|
+
there are two ways that a handle can be verified:
|
|
271
|
+
|
|
272
|
+
1. HTTP verification: there is a file at `/.well-known/atproto-did` containing your account's DID
|
|
273
|
+
2. DNS verification: there is an `_atproto` TXT record containing your account's DID
|
|
274
|
+
|
|
275
|
+
you'd want to resolve both of these. if both methods return a response but does not match each other
|
|
276
|
+
then it should ideally be thrown.
|
|
277
|
+
|
|
278
|
+
verify that the DID matches the intended format
|
|
279
|
+
|
|
280
|
+
```ts
|
|
281
|
+
const isDid = (did: string): did is At.DID => {
|
|
282
|
+
return /^did:([a-z]+):([a-zA-Z0-9._:%-]*[a-zA-Z0-9._-])$/.test(did);
|
|
283
|
+
};
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
pass this resolved DID to `resolveFromIdentity`, and carry on as per usual.
|
|
287
|
+
|
|
288
|
+
#### HTTP handle resolution
|
|
289
|
+
|
|
290
|
+
this is very straightforward, make a request to `https://<handle>/.well-known/atproto-did` without
|
|
291
|
+
following redirects. check if the response status is 200 and trim off any excess whitespaces.
|
|
292
|
+
|
|
293
|
+
some web servers might not set a permissible CORS header to access this resource, in which case
|
|
294
|
+
there is nothing that can be done, unless you'd want to proxy the requests.
|
|
295
|
+
|
|
296
|
+
```ts
|
|
297
|
+
const resolveHandleViaHttp = async (handle: string): Promise<At.DID> => {
|
|
298
|
+
const url = new URL('/.well-known/atproto-did', `https://${handle}`);
|
|
299
|
+
|
|
300
|
+
const response = await fetch(url, { redirect: 'error' });
|
|
301
|
+
if (!response.ok) {
|
|
302
|
+
throw new ResolverError(`domain is unreachable`);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const text = await response.text();
|
|
306
|
+
|
|
307
|
+
const did = text.split('\n')[0]!.trim();
|
|
308
|
+
if (isDid(did)) {
|
|
309
|
+
return did;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
throw new ResolverError(`failed to resolve ${handle}`);
|
|
313
|
+
};
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
#### DNS handle resolution
|
|
317
|
+
|
|
318
|
+
as websites can't do DNS resolution on their own, we'd have to rely on DNS-over-HTTPS (DoH)
|
|
319
|
+
services. it should be noted that this _can_ have privacy implications of its own, please read
|
|
320
|
+
through the privacy policy of whichever DoH service you end up using and make the user aware of it
|
|
321
|
+
as well.
|
|
322
|
+
|
|
323
|
+
for this example, we'll be using Cloudflare's DoH resolver for Firefox ([privacy
|
|
324
|
+
policy][cf-resolver-firefox-privacy]) as it has support for `application/dns-json` format which
|
|
325
|
+
allows us to query and see the responses in JSON.
|
|
326
|
+
|
|
327
|
+
```ts
|
|
328
|
+
const SUBDOMAIN = '_atproto';
|
|
329
|
+
const PREFIX = 'did=';
|
|
330
|
+
|
|
331
|
+
const resolveHandleViaDoH = async (handle: string): Promise<At.DID> => {
|
|
332
|
+
const url = new URL('https://mozilla.cloudflare-dns.com/dns-query');
|
|
333
|
+
url.searchParams.set('type', 'TXT');
|
|
334
|
+
url.searchParams.set('name', `${SUBDOMAIN}.${handle}`);
|
|
335
|
+
|
|
336
|
+
const response = await fetch(url, {
|
|
337
|
+
method: 'GET',
|
|
338
|
+
headers: { accept: 'application/dns-json' },
|
|
339
|
+
redirect: 'follow',
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
const type = response.headers.get('content-type')?.trim();
|
|
343
|
+
if (!response.ok) {
|
|
344
|
+
const message = type?.startsWith('text/plain')
|
|
345
|
+
? await response.text()
|
|
346
|
+
: `failed to resolve ${handle}`;
|
|
347
|
+
|
|
348
|
+
throw new ResolverError(message);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
if (type !== 'application/dns-json') {
|
|
352
|
+
throw new ResolverError(`unexpected response from DoH server`);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const result = asResult(await response.json());
|
|
356
|
+
const answers = result.Answer?.filter(isAnswerTxt).map(extractTxtData) ?? [];
|
|
357
|
+
|
|
358
|
+
for (let i = 0; i < answers.length; i++) {
|
|
359
|
+
// skip if the line does not start with "did="
|
|
360
|
+
if (!answers[i].startsWith(PREFIX)) {
|
|
361
|
+
continue;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// ensure there is no other entry starting with "did="
|
|
365
|
+
for (let j = i + 1; j < answers.length; j++) {
|
|
366
|
+
if (answers[j].startsWith(PREFIX)) {
|
|
367
|
+
break;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const did = answers[i].slice(PREFIX.length);
|
|
372
|
+
if (isDid(did)) {
|
|
373
|
+
return did;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
break;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
throw new ResolverError(`failed to resolve ${handle}`);
|
|
380
|
+
};
|
|
381
|
+
|
|
382
|
+
type Result = { Status: number; Answer?: Answer[] };
|
|
383
|
+
const isResult = (result: unknown): result is Result => {
|
|
384
|
+
if (result === null || typeof result !== 'object') {
|
|
385
|
+
return false;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
return (
|
|
389
|
+
'Status' in result &&
|
|
390
|
+
typeof result.Status === 'number' &&
|
|
391
|
+
(!('Answer' in result) || (Array.isArray(result.Answer) && result.Answer.every(isAnswer)))
|
|
392
|
+
);
|
|
393
|
+
};
|
|
394
|
+
const asResult = (result: unknown): Result => {
|
|
395
|
+
if (!isResult(result)) {
|
|
396
|
+
throw new TypeError(`unexpected DoH response`);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
return result;
|
|
400
|
+
};
|
|
401
|
+
|
|
402
|
+
type Answer = { name: string; type: number; data: string; TTL: number };
|
|
403
|
+
const isAnswer = (answer: unknown): answer is Answer => {
|
|
404
|
+
if (answer === null || typeof answer !== 'object') {
|
|
405
|
+
return false;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
return (
|
|
409
|
+
'name' in answer &&
|
|
410
|
+
typeof answer.name === 'string' &&
|
|
411
|
+
'type' in answer &&
|
|
412
|
+
typeof answer.type === 'number' &&
|
|
413
|
+
'data' in answer &&
|
|
414
|
+
typeof answer.data === 'string' &&
|
|
415
|
+
'TTL' in answer &&
|
|
416
|
+
typeof answer.TTL === 'number'
|
|
417
|
+
);
|
|
418
|
+
};
|
|
419
|
+
|
|
420
|
+
type AnswerTxt = Answer & { type: 16 };
|
|
421
|
+
const isAnswerTxt = (answer: Answer): answer is AnswerTxt => {
|
|
422
|
+
return answer.type === 16;
|
|
423
|
+
};
|
|
424
|
+
|
|
425
|
+
const extractTxtData = (answer: AnswerTxt): string => {
|
|
426
|
+
return answer.data.replace(/^"|"$/g, '').replace(/\\"/g, '"');
|
|
427
|
+
};
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
[cf-resolver-firefox-privacy]:
|
|
431
|
+
https://developers.cloudflare.com/1.1.1.1/privacy/cloudflare-resolver-firefox/
|
|
432
|
+
|
|
433
|
+
#### using your PDS for handle resolution
|
|
434
|
+
|
|
435
|
+
alternatively, if you operate your own PDS, you can make use of it as a handle resolver.
|
|
436
|
+
|
|
437
|
+
```ts
|
|
438
|
+
const resolveHandleViaPds = async (handle: string): Promise<At.DID> => {
|
|
439
|
+
const rpc = new XRPC({ handler: simpleFetchHandler({ service: `https://my-pds.example.com` }) });
|
|
440
|
+
|
|
441
|
+
const { data } = await rpc.get('com.atproto.identity.resolveHandle', {
|
|
442
|
+
params: {
|
|
443
|
+
handle: handle,
|
|
444
|
+
},
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
return data.did;
|
|
448
|
+
};
|
|
449
|
+
```
|
|
@@ -73,19 +73,20 @@ export class OAuthServerAgent {
|
|
|
73
73
|
}
|
|
74
74
|
}
|
|
75
75
|
#processTokenResponse(res) {
|
|
76
|
-
|
|
77
|
-
const scope = res.scope;
|
|
78
|
-
if (!sub) {
|
|
76
|
+
if (!res.sub) {
|
|
79
77
|
throw new TypeError(`missing sub field in token response`);
|
|
80
78
|
}
|
|
81
|
-
if (!scope) {
|
|
79
|
+
if (!res.scope) {
|
|
82
80
|
throw new TypeError(`missing scope field in token response`);
|
|
83
81
|
}
|
|
82
|
+
if (res.token_type !== 'DPoP') {
|
|
83
|
+
throw new TypeError(`token response returned a non-dpop token`);
|
|
84
|
+
}
|
|
84
85
|
return {
|
|
85
|
-
scope: scope,
|
|
86
|
+
scope: res.scope,
|
|
86
87
|
refresh: res.refresh_token,
|
|
87
88
|
access: res.access_token,
|
|
88
|
-
type: res.token_type
|
|
89
|
+
type: res.token_type,
|
|
89
90
|
expires_at: typeof res.expires_in === 'number' ? Date.now() + res.expires_in * 1_000 : undefined,
|
|
90
91
|
};
|
|
91
92
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server-agent.js","sourceRoot":"","sources":["../../lib/agents/server-agent.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"server-agent.js","sourceRoot":"","sources":["../../lib/agents/server-agent.ts"],"names":[],"mappings":"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"}
|
package/dist/store/db.js
CHANGED
package/dist/store/db.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"db.js","sourceRoot":"","sources":["../../lib/store/db.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,KAAK,EAAE,MAAM,qBAAqB,CAAC;AAkC5C,MAAM,KAAK,GAAG,CAAC,GAAkB,EAAE,EAAE;IACpC,IAAI,GAAG,IAAI,IAAI,EAAE,CAAC;QACjB,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QAC/B,IAAI,MAAM,IAAI,IAAI,EAAE,CAAC;YACpB,OAAO,MAAM,CAAC;QACf,CAAC;IACF,CAAC;IAED,OAAO,EAAE,CAAC;AACX,CAAC,CAAC;AAIF,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAAC,EAAE,IAAI,EAAwB,EAAE,EAAE;IACrE,MAAM,UAAU,GAAG,IAAI,eAAe,EAAE,CAAC;IACzC,MAAM,MAAM,GAAG,UAAU,CAAC,MAAM,CAAC;IAEjC,MAAM,WAAW,GAAG,CACnB,OAAU,EACV,SAAsD,EACF,EAAE;QACtD,IAAI,KAAU,CAAC;QAEf,MAAM,UAAU,GAAG,GAAG,IAAI,IAAI,OAAO,EAAE,CAAC;QAExC,MAAM,OAAO,GAAG,GAAG,EAAE,CAAC,KAAK,IAAI,YAAY,CAAC,OAAO,CAAC,UAAU,EAAE,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,CAAC;QACvF,MAAM,IAAI,GAAG,GAAG,EAAE;YACjB,IAAI,MAAM,CAAC,OAAO,EAAE,CAAC;gBACpB,MAAM,IAAI,KAAK,CAAC,cAAc,CAAC,CAAC;YACjC,CAAC;YAED,OAAO,CAAC,KAAK,KAAK,KAAK,CAAC,YAAY,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC;QAC5D,CAAC,CAAC;QAEF,CAAC;YACA,MAAM,QAAQ,GAAG,CAAC,EAAgB,EAAE,EAAE;gBACrC,IAAI,EAAE,CAAC,GAAG,KAAK,UAAU,EAAE,CAAC;oBAC3B,KAAK,GAAG,SAAS,CAAC;gBACnB,CAAC;YACF,CAAC,CAAC;YAEF,
|
|
1
|
+
{"version":3,"file":"db.js","sourceRoot":"","sources":["../../lib/store/db.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { createES256Key } from '../dpop.js';
|
|
2
|
+
import { CLIENT_ID, database, REDIRECT_URI } from '../environment.js';
|
|
3
|
+
import { AuthorizationError, LoginError } from '../errors.js';
|
|
4
|
+
import type { IdentityMetadata } from '../types/identity.js';
|
|
5
|
+
import type { AuthorizationServerMetadata } from '../types/server.js';
|
|
6
|
+
import type { Session } from '../types/token.js';
|
|
7
|
+
import { generatePKCE, generateState } from '../utils/runtime.js';
|
|
8
|
+
|
|
9
|
+
import { OAuthServerAgent } from './server-agent.js';
|
|
10
|
+
import { storeSession } from './sessions.js';
|
|
11
|
+
|
|
12
|
+
export interface AuthorizeOptions {
|
|
13
|
+
metadata: AuthorizationServerMetadata;
|
|
14
|
+
identity?: IdentityMetadata;
|
|
15
|
+
scope: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Create authentication URL for authorization
|
|
20
|
+
* @param options
|
|
21
|
+
* @returns URL to redirect the user for authorization
|
|
22
|
+
*/
|
|
23
|
+
export const createAuthorizationUrl = async ({
|
|
24
|
+
metadata,
|
|
25
|
+
identity,
|
|
26
|
+
scope,
|
|
27
|
+
}: AuthorizeOptions): Promise<URL> => {
|
|
28
|
+
const state = generateState();
|
|
29
|
+
|
|
30
|
+
const pkce = await generatePKCE();
|
|
31
|
+
const dpopKey = await createES256Key();
|
|
32
|
+
|
|
33
|
+
const params = {
|
|
34
|
+
redirect_uri: REDIRECT_URI,
|
|
35
|
+
code_challenge: pkce.challenge,
|
|
36
|
+
code_challenge_method: pkce.method,
|
|
37
|
+
state: state,
|
|
38
|
+
login_hint: identity?.raw,
|
|
39
|
+
response_mode: 'fragment',
|
|
40
|
+
response_type: 'code',
|
|
41
|
+
display: 'page',
|
|
42
|
+
// id_token_hint: undefined,
|
|
43
|
+
// max_age: undefined,
|
|
44
|
+
// prompt: undefined,
|
|
45
|
+
scope: scope,
|
|
46
|
+
// ui_locales: undefined,
|
|
47
|
+
} satisfies Record<string, string | undefined>;
|
|
48
|
+
|
|
49
|
+
database.states.set(state, {
|
|
50
|
+
dpopKey: dpopKey,
|
|
51
|
+
metadata: metadata,
|
|
52
|
+
verifier: pkce.verifier,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const server = new OAuthServerAgent(metadata, dpopKey);
|
|
56
|
+
const response = await server.request('pushed_authorization_request', params);
|
|
57
|
+
|
|
58
|
+
const authUrl = new URL(metadata.authorization_endpoint);
|
|
59
|
+
authUrl.searchParams.set('client_id', CLIENT_ID);
|
|
60
|
+
authUrl.searchParams.set('request_uri', response.request_uri);
|
|
61
|
+
|
|
62
|
+
return authUrl;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Finalize authorization
|
|
67
|
+
* @param params Search params
|
|
68
|
+
* @returns Session object, which you can use to instantiate user agents
|
|
69
|
+
*/
|
|
70
|
+
export const finalizeAuthorization = async (params: URLSearchParams) => {
|
|
71
|
+
const issuer = params.get('iss');
|
|
72
|
+
const state = params.get('state');
|
|
73
|
+
const code = params.get('code');
|
|
74
|
+
const error = params.get('error');
|
|
75
|
+
|
|
76
|
+
if (!state || !(code || error)) {
|
|
77
|
+
throw new LoginError(`missing parameters`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const stored = database.states.get(state);
|
|
81
|
+
if (stored) {
|
|
82
|
+
// Delete now that we've caught it
|
|
83
|
+
database.states.delete(state);
|
|
84
|
+
} else {
|
|
85
|
+
throw new LoginError(`unknown state provided`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const dpopKey = stored.dpopKey;
|
|
89
|
+
const metadata = stored.metadata;
|
|
90
|
+
|
|
91
|
+
if (error) {
|
|
92
|
+
throw new AuthorizationError(params.get('error_description') || error);
|
|
93
|
+
}
|
|
94
|
+
if (!code) {
|
|
95
|
+
throw new LoginError(`missing code parameter`);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (issuer === null) {
|
|
99
|
+
throw new LoginError(`missing issuer parameter`);
|
|
100
|
+
} else if (issuer !== metadata.issuer) {
|
|
101
|
+
throw new LoginError(`issuer mismatch`);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Retrieve authentication tokens
|
|
105
|
+
const server = new OAuthServerAgent(metadata, dpopKey);
|
|
106
|
+
const { info, token } = await server.exchangeCode(code, stored.verifier);
|
|
107
|
+
|
|
108
|
+
// We're finished!
|
|
109
|
+
const sub = info.sub;
|
|
110
|
+
const session: Session = { dpopKey, info, token };
|
|
111
|
+
|
|
112
|
+
await storeSession(sub, session);
|
|
113
|
+
|
|
114
|
+
return session;
|
|
115
|
+
};
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import type { At } from '@atcute/client/lexicons';
|
|
2
|
+
|
|
3
|
+
import { createDPoPFetch } from '../dpop.js';
|
|
4
|
+
import { CLIENT_ID, REDIRECT_URI } from '../environment.js';
|
|
5
|
+
import { FetchResponseError, OAuthResponseError, TokenRefreshError } from '../errors.js';
|
|
6
|
+
import { resolveFromIdentity } from '../resolvers.js';
|
|
7
|
+
import type { DPoPKey } from '../types/dpop.js';
|
|
8
|
+
import type { OAuthParResponse } from '../types/par.js';
|
|
9
|
+
import type { PersistedAuthorizationServerMetadata } from '../types/server.js';
|
|
10
|
+
import type { ExchangeInfo, OAuthTokenResponse, TokenInfo } from '../types/token.js';
|
|
11
|
+
import { pick } from '../utils/misc.js';
|
|
12
|
+
import { extractContentType } from '../utils/response.js';
|
|
13
|
+
|
|
14
|
+
export class OAuthServerAgent {
|
|
15
|
+
#fetch: typeof fetch;
|
|
16
|
+
#metadata: PersistedAuthorizationServerMetadata;
|
|
17
|
+
|
|
18
|
+
constructor(metadata: PersistedAuthorizationServerMetadata, dpopKey: DPoPKey) {
|
|
19
|
+
this.#metadata = metadata;
|
|
20
|
+
this.#fetch = createDPoPFetch(CLIENT_ID, dpopKey, true);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async request(
|
|
24
|
+
endpoint: 'pushed_authorization_request',
|
|
25
|
+
payload: Record<string, unknown>,
|
|
26
|
+
): Promise<OAuthParResponse>;
|
|
27
|
+
async request(endpoint: 'token', payload: Record<string, unknown>): Promise<OAuthTokenResponse>;
|
|
28
|
+
async request(endpoint: 'revocation', payload: Record<string, unknown>): Promise<any>;
|
|
29
|
+
async request(endpoint: 'introspection', payload: Record<string, unknown>): Promise<any>;
|
|
30
|
+
async request(endpoint: string, payload: Record<string, unknown>): Promise<any> {
|
|
31
|
+
const url: string | undefined = (this.#metadata as any)[`${endpoint}_endpoint`];
|
|
32
|
+
if (!url) {
|
|
33
|
+
throw new Error(`no endpoint for ${endpoint}`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const response = await this.#fetch(url, {
|
|
37
|
+
method: 'post',
|
|
38
|
+
headers: { 'content-type': 'application/json' },
|
|
39
|
+
body: JSON.stringify({ ...payload, client_id: CLIENT_ID }),
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
if (extractContentType(response.headers) !== 'application/json') {
|
|
43
|
+
throw new FetchResponseError(response, 2, `unexpected content-type`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const json = await response.json();
|
|
47
|
+
|
|
48
|
+
if (response.ok) {
|
|
49
|
+
return json;
|
|
50
|
+
} else {
|
|
51
|
+
throw new OAuthResponseError(response, json);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async revoke(token: string): Promise<void> {
|
|
56
|
+
try {
|
|
57
|
+
await this.request('revocation', { token: token });
|
|
58
|
+
} catch {}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async exchangeCode(code: string, verifier?: string): Promise<{ info: ExchangeInfo; token: TokenInfo }> {
|
|
62
|
+
const response = await this.request('token', {
|
|
63
|
+
grant_type: 'authorization_code',
|
|
64
|
+
redirect_uri: REDIRECT_URI,
|
|
65
|
+
code: code,
|
|
66
|
+
code_verifier: verifier,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
return await this.#processExchangeResponse(response);
|
|
71
|
+
} catch (err) {
|
|
72
|
+
await this.revoke(response.access_token);
|
|
73
|
+
throw err;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async refresh({ sub, token }: { sub: At.DID; token: TokenInfo }): Promise<TokenInfo> {
|
|
78
|
+
if (!token.refresh) {
|
|
79
|
+
throw new TokenRefreshError(sub, 'no refresh token available');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const response = await this.request('token', {
|
|
83
|
+
grant_type: 'refresh_token',
|
|
84
|
+
refresh_token: token.refresh,
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
if (sub !== response.sub) {
|
|
89
|
+
throw new TokenRefreshError(sub, `sub mismatch in token response; got ${response.sub}`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return this.#processTokenResponse(response);
|
|
93
|
+
} catch (err) {
|
|
94
|
+
await this.revoke(response.access_token);
|
|
95
|
+
|
|
96
|
+
throw err;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
#processTokenResponse(res: OAuthTokenResponse): TokenInfo {
|
|
101
|
+
if (!res.sub) {
|
|
102
|
+
throw new TypeError(`missing sub field in token response`);
|
|
103
|
+
}
|
|
104
|
+
if (!res.scope) {
|
|
105
|
+
throw new TypeError(`missing scope field in token response`);
|
|
106
|
+
}
|
|
107
|
+
if (res.token_type !== 'DPoP') {
|
|
108
|
+
throw new TypeError(`token response returned a non-dpop token`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
scope: res.scope,
|
|
113
|
+
refresh: res.refresh_token,
|
|
114
|
+
access: res.access_token,
|
|
115
|
+
type: res.token_type,
|
|
116
|
+
expires_at: typeof res.expires_in === 'number' ? Date.now() + res.expires_in * 1_000 : undefined,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async #processExchangeResponse(res: OAuthTokenResponse): Promise<{ info: ExchangeInfo; token: TokenInfo }> {
|
|
121
|
+
const sub = res.sub;
|
|
122
|
+
if (!sub) {
|
|
123
|
+
throw new TypeError(`missing sub field in token response`);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const token = this.#processTokenResponse(res);
|
|
127
|
+
const resolved = await resolveFromIdentity(sub);
|
|
128
|
+
|
|
129
|
+
if (resolved.metadata.issuer !== this.#metadata.issuer) {
|
|
130
|
+
throw new TypeError(`issuer mismatch; got ${resolved.metadata.issuer}`);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
token: token,
|
|
135
|
+
info: {
|
|
136
|
+
sub: sub as At.DID,
|
|
137
|
+
aud: resolved.identity.pds.href,
|
|
138
|
+
server: pick(resolved.metadata, [
|
|
139
|
+
'issuer',
|
|
140
|
+
'authorization_endpoint',
|
|
141
|
+
'introspection_endpoint',
|
|
142
|
+
'pushed_authorization_request_endpoint',
|
|
143
|
+
'revocation_endpoint',
|
|
144
|
+
'token_endpoint',
|
|
145
|
+
]),
|
|
146
|
+
},
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
}
|