@atcute/oauth-browser-client 2.0.3 → 3.0.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.
Files changed (82) hide show
  1. package/README.md +18 -12
  2. package/dist/agents/exchange.d.ts +2 -1
  3. package/dist/agents/exchange.d.ts.map +1 -1
  4. package/dist/agents/exchange.js +3 -4
  5. package/dist/agents/exchange.js.map +1 -1
  6. package/dist/agents/server-agent.d.ts +5 -5
  7. package/dist/agents/server-agent.d.ts.map +1 -1
  8. package/dist/agents/server-agent.js +5 -9
  9. package/dist/agents/server-agent.js.map +1 -1
  10. package/dist/agents/sessions.d.ts.map +1 -1
  11. package/dist/agents/sessions.js +16 -1
  12. package/dist/agents/sessions.js.map +1 -1
  13. package/dist/dpop.d.ts +2 -4
  14. package/dist/dpop.d.ts.map +1 -1
  15. package/dist/dpop.js +6 -79
  16. package/dist/dpop.js.map +1 -1
  17. package/dist/environment.d.ts +3 -3
  18. package/dist/environment.d.ts.map +1 -1
  19. package/dist/environment.js.map +1 -1
  20. package/dist/index.d.ts +3 -11
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +1 -11
  23. package/dist/index.js.map +1 -1
  24. package/dist/resolvers.d.ts +92 -4
  25. package/dist/resolvers.d.ts.map +1 -1
  26. package/dist/resolvers.js +5 -5
  27. package/dist/resolvers.js.map +1 -1
  28. package/dist/store/db.d.ts +49 -6
  29. package/dist/store/db.d.ts.map +1 -1
  30. package/dist/types/client-assertion.d.ts +2 -3
  31. package/dist/types/client-assertion.d.ts.map +1 -1
  32. package/dist/types/server.d.ts +2 -56
  33. package/dist/types/server.d.ts.map +1 -1
  34. package/dist/types/token.d.ts +8 -20
  35. package/dist/types/token.d.ts.map +1 -1
  36. package/dist/utils/dpop-key.d.ts +10 -0
  37. package/dist/utils/dpop-key.d.ts.map +1 -0
  38. package/dist/utils/dpop-key.js +13 -0
  39. package/dist/utils/dpop-key.js.map +1 -0
  40. package/dist/utils/runtime.d.ts +0 -6
  41. package/dist/utils/runtime.d.ts.map +1 -1
  42. package/dist/utils/runtime.js +0 -16
  43. package/dist/utils/runtime.js.map +1 -1
  44. package/lib/agents/exchange.ts +10 -11
  45. package/lib/agents/server-agent.ts +14 -17
  46. package/lib/agents/sessions.ts +23 -2
  47. package/lib/dpop.ts +7 -108
  48. package/lib/environment.ts +3 -3
  49. package/lib/index.ts +12 -12
  50. package/lib/resolvers.ts +13 -11
  51. package/lib/store/db.ts +6 -6
  52. package/lib/types/client-assertion.ts +2 -4
  53. package/lib/types/server.ts +2 -57
  54. package/lib/types/token.ts +10 -24
  55. package/lib/utils/dpop-key.ts +24 -0
  56. package/lib/utils/runtime.ts +0 -22
  57. package/package.json +12 -8
  58. package/dist/types/client.d.ts +0 -38
  59. package/dist/types/client.d.ts.map +0 -1
  60. package/dist/types/client.js +0 -2
  61. package/dist/types/client.js.map +0 -1
  62. package/dist/types/dpop.d.ts +0 -10
  63. package/dist/types/dpop.d.ts.map +0 -1
  64. package/dist/types/dpop.js +0 -2
  65. package/dist/types/dpop.js.map +0 -1
  66. package/dist/types/identity.d.ts +0 -6
  67. package/dist/types/identity.d.ts.map +0 -1
  68. package/dist/types/identity.js +0 -2
  69. package/dist/types/identity.js.map +0 -1
  70. package/dist/types/par.d.ts +0 -5
  71. package/dist/types/par.d.ts.map +0 -1
  72. package/dist/types/par.js +0 -2
  73. package/dist/types/par.js.map +0 -1
  74. package/dist/utils/identity-resolver.d.ts +0 -7
  75. package/dist/utils/identity-resolver.d.ts.map +0 -1
  76. package/dist/utils/identity-resolver.js +0 -8
  77. package/dist/utils/identity-resolver.js.map +0 -1
  78. package/lib/types/client.ts +0 -82
  79. package/lib/types/dpop.ts +0 -9
  80. package/lib/types/identity.ts +0 -12
  81. package/lib/types/par.ts +0 -4
  82. package/lib/utils/identity-resolver.ts +0 -12
package/lib/dpop.ts CHANGED
@@ -1,82 +1,20 @@
1
- import { fromBase64Url, toBase64Url } from '@atcute/multibase';
2
- import { encodeUtf8 } from '@atcute/uint8array';
3
-
4
- import { nanoid } from 'nanoid';
1
+ import { createDpopProofSigner, sha256Base64Url, type DpopPrivateJwk } from '@atcute/oauth-crypto';
5
2
 
6
3
  import { database } from './environment.js';
7
- import type { DPoPKey } from './types/dpop.js';
8
4
  import { extractContentType } from './utils/response.js';
9
- import { stringToSha256 } from './utils/runtime.js';
10
-
11
- const ES256_ALG = { name: 'ECDSA', namedCurve: 'P-256' } as const;
12
-
13
- export const createES256Key = async (): Promise<DPoPKey> => {
14
- const pair = await crypto.subtle.generateKey(ES256_ALG, true, ['sign', 'verify']);
15
-
16
- const key = await crypto.subtle.exportKey('pkcs8', pair.privateKey);
17
- const { ext: _ext, key_ops: _key_opts, ...jwk } = await crypto.subtle.exportKey('jwk', pair.publicKey);
18
-
19
- const canonicalJwk = JSON.stringify({ crv: jwk.crv, kty: jwk.kty, x: jwk.x, y: jwk.y });
20
- const jkt = await stringToSha256(canonicalJwk);
21
-
22
- return {
23
- typ: 'ES256',
24
- key: toBase64Url(new Uint8Array(key)),
25
- jwt: toBase64Url(encodeUtf8(JSON.stringify({ typ: 'dpop+jwt', alg: 'ES256', jwk: jwk }))),
26
- jkt: jkt,
27
- };
28
- };
29
-
30
- export const createDPoPSignage = (dpopKey: DPoPKey) => {
31
- const headerString = dpopKey.jwt;
32
- const keyPromise = crypto.subtle.importKey(
33
- 'pkcs8',
34
- fromBase64Url(dpopKey.key) as Uint8Array<ArrayBuffer>,
35
- ES256_ALG,
36
- true,
37
- ['sign'],
38
- );
39
-
40
- const constructPayload = (htm: string, htu: string, nonce: string | undefined, ath: string | undefined) => {
41
- const payload = {
42
- ath: ath,
43
- htm: htm,
44
- htu: htu,
45
- iat: Math.floor(Date.now() / 1_000),
46
- jti: nanoid(24),
47
- nonce: nonce,
48
- };
49
-
50
- return toBase64Url(encodeUtf8(JSON.stringify(payload)));
51
- };
52
5
 
53
- return async (method: string, htu: string, nonce: string | undefined, ath: string | undefined) => {
54
- const payloadString = constructPayload(method, htu, nonce, ath);
55
-
56
- const signed = await crypto.subtle.sign(
57
- { name: 'ECDSA', hash: { name: 'SHA-256' } },
58
- await keyPromise,
59
- encodeUtf8(headerString + '.' + payloadString) as Uint8Array<ArrayBuffer>,
60
- );
61
-
62
- const signatureString = toBase64Url(new Uint8Array(signed));
63
-
64
- return headerString + '.' + payloadString + '.' + signatureString;
65
- };
66
- };
67
-
68
- export const createDPoPFetch = (dpopKey: DPoPKey, isAuthServer?: boolean): typeof fetch => {
6
+ export const createDPoPFetch = (dpopKey: DpopPrivateJwk, isAuthServer?: boolean): typeof fetch => {
69
7
  const nonces = database.dpopNonces;
70
8
  const pending = database.inflightDpop;
71
9
 
72
- const sign = createDPoPSignage(dpopKey);
10
+ const sign = createDpopProofSigner(dpopKey);
73
11
 
74
12
  return async (input, init) => {
75
13
  const request = new Request(input, init);
76
14
 
77
15
  const authorizationHeader = request.headers.get('authorization');
78
16
  const ath = authorizationHeader?.startsWith('DPoP ')
79
- ? await stringToSha256(authorizationHeader.slice(5))
17
+ ? await sha256Base64Url(authorizationHeader.slice(5))
80
18
  : undefined;
81
19
 
82
20
  const { method, url } = request;
@@ -84,44 +22,24 @@ export const createDPoPFetch = (dpopKey: DPoPKey, isAuthServer?: boolean): typeo
84
22
 
85
23
  const htu = origin + pathname;
86
24
 
87
- // See if we have a pending promise for this origin, we'll await before
88
- // proceeding with this request, next comment describes what the promise
89
- // is meant to be.
90
25
  let deferred = pending.get(origin);
91
26
  if (deferred) {
92
27
  await deferred.promise;
93
28
  deferred = undefined;
94
29
  }
95
30
 
96
- // Get our persisted nonce value for this origin
97
31
  let initNonce: string | undefined;
98
32
  let expiredOrMissing = false;
99
33
  try {
100
34
  const [nonce, lapsed] = nonces.getWithLapsed(origin);
101
35
 
102
36
  initNonce = nonce;
103
-
104
- // The problem with DPoP nonces is that we don't have insight as to when
105
- // they'll expire, either we have a nonce value or we don't.
106
- //
107
- // Which is very unfortunate, if the client makes multiple requests at the
108
- // same time, there's a chance that all of them will fail due to the nonce
109
- // value having expired.
110
- //
111
- // To make this less painful, if it's been over 3 minutes since we last
112
- // had a nonce value, or we never had one to begin with, we'll let this
113
- // request through and defer everyone else until we get a possibly fresh
114
- // nonce value.
115
- //
116
- // 3 minutes being the DPoP nonce expiration time set by the reference PDS
117
- // implementation.
118
37
  expiredOrMissing = lapsed > 3 * 60 * 1_000;
119
38
  } catch {
120
- // Ignore read errors, we'll just act like we're missing a nonce.
39
+ // ignore read errors
121
40
  }
122
41
 
123
42
  if (expiredOrMissing) {
124
- // Defer everyone else until this request finishes.
125
43
  pending.set(origin, (deferred = Promise.withResolvers()));
126
44
  }
127
45
 
@@ -134,44 +52,30 @@ export const createDPoPFetch = (dpopKey: DPoPKey, isAuthServer?: boolean): typeo
134
52
 
135
53
  nextNonce = initResponse.headers.get('dpop-nonce');
136
54
  if (nextNonce === null || nextNonce === initNonce) {
137
- // No nonce was returned or it is the same as the one we sent. No need to
138
- // update the nonce store, or retry the request.
139
-
140
55
  return initResponse;
141
56
  }
142
57
 
143
- // Store the fresh nonce for future requests
144
58
  try {
145
59
  nonces.set(origin, nextNonce);
146
60
  } catch {
147
- // Ignore write errors
61
+ // ignore write errors
148
62
  }
149
63
 
150
64
  const shouldRetry = await isUseDpopNonceError(initResponse, isAuthServer);
151
65
  if (!shouldRetry) {
152
- // Not a "use_dpop_nonce" error, so there is no need to retry
153
-
154
66
  return initResponse;
155
67
  }
156
68
 
157
69
  if (input === request || init?.body instanceof ReadableStream) {
158
- // If the input stream was already consumed, we cannot retry the request. A
159
- // solution would be to clone() the request but that would bufferize the
160
- // entire stream in memory which can lead to memory starvation. Instead, we
161
- // will return the original response and let the calling code handle retries.
162
-
163
70
  return initResponse;
164
71
  }
165
72
  } finally {
166
- // Now everyone can have their turn.
167
73
  if (deferred) {
168
74
  pending.delete(origin);
169
75
  deferred.resolve();
170
76
  }
171
77
  }
172
78
 
173
- // We got here because we were asked to retry the request (due to missing
174
- // nonce value in the first request), let's do just that.
175
79
  {
176
80
  const nextProof = await sign(method, htu, nextNonce, ath);
177
81
  const nextRequest = new Request(input, init);
@@ -179,13 +83,12 @@ export const createDPoPFetch = (dpopKey: DPoPKey, isAuthServer?: boolean): typeo
179
83
 
180
84
  const retryResponse = await fetch(nextRequest);
181
85
 
182
- // Check if the server returned another new nonce in the retry response
183
86
  const retryNonce = retryResponse.headers.get('dpop-nonce');
184
87
  if (retryNonce !== null && retryNonce !== nextNonce) {
185
88
  try {
186
89
  nonces.set(origin, retryNonce);
187
90
  } catch {
188
- // Ignore write errors
91
+ // ignore write errors
189
92
  }
190
93
  }
191
94
 
@@ -195,8 +98,6 @@ export const createDPoPFetch = (dpopKey: DPoPKey, isAuthServer?: boolean): typeo
195
98
  };
196
99
 
197
100
  const isUseDpopNonceError = async (response: Response, isAuthServer?: boolean): Promise<boolean> => {
198
- // https://datatracker.ietf.org/doc/html/rfc6750#section-3
199
- // https://datatracker.ietf.org/doc/html/rfc9449#name-resource-server-provided-no
200
101
  if (isAuthServer === undefined || isAuthServer === false) {
201
102
  if (response.status === 401) {
202
103
  const wwwAuth = response.headers.get('www-authenticate');
@@ -206,14 +107,12 @@ const isUseDpopNonceError = async (response: Response, isAuthServer?: boolean):
206
107
  }
207
108
  }
208
109
 
209
- // https://datatracker.ietf.org/doc/html/rfc9449#name-authorization-server-provid
210
110
  if (isAuthServer === undefined || isAuthServer === true) {
211
111
  if (response.status === 400 && extractContentType(response.headers) === 'application/json') {
212
112
  try {
213
113
  const json = await response.clone().json();
214
114
  return typeof json === 'object' && json?.['error'] === 'use_dpop_nonce';
215
115
  } catch {
216
- // Response too big (to be "use_dpop_nonce" error) or invalid JSON
217
116
  return false;
218
117
  }
219
118
  }
@@ -1,4 +1,4 @@
1
- import type { IdentityResolver } from './types/identity.js';
1
+ import type { ActorResolver } from '@atcute/identity-resolver';
2
2
 
3
3
  import { createOAuthDatabase, type OAuthDatabase } from './store/db.js';
4
4
  import type { ClientAssertionFetcher } from './types/client-assertion.js';
@@ -10,7 +10,7 @@ export let fetchClientAssertion: ClientAssertionFetcher | undefined;
10
10
 
11
11
  export let database: OAuthDatabase;
12
12
 
13
- export let identityResolver: IdentityResolver;
13
+ export let identityResolver: ActorResolver;
14
14
 
15
15
  export interface ConfigureOAuthOptions {
16
16
  /**
@@ -22,7 +22,7 @@ export interface ConfigureOAuthOptions {
22
22
  };
23
23
 
24
24
  /** resolves actor identifiers into identity metadata */
25
- identityResolver: IdentityResolver;
25
+ identityResolver: ActorResolver;
26
26
 
27
27
  /**
28
28
  * optional function to fetch DPoP-bound client assertions from your backend.
package/lib/index.ts CHANGED
@@ -3,17 +3,17 @@ export { configureOAuth, type ConfigureOAuthOptions } from './environment.js';
3
3
  export * from './errors.js';
4
4
 
5
5
  export * from './agents/exchange.js';
6
- export * from './agents/server-agent.js';
7
- export * from './agents/sessions.js';
6
+ export {
7
+ getSession,
8
+ deleteStoredSession,
9
+ listStoredSessions,
10
+ type SessionGetOptions,
11
+ } from './agents/sessions.js';
8
12
  export * from './agents/user-agent.js';
9
13
 
10
- export * from './types/client-assertion.js';
11
- export * from './types/client.js';
12
- export * from './types/dpop.js';
13
- export * from './types/identity.js';
14
- export * from './types/par.js';
15
- export * from './types/server.js';
16
- export * from './types/store.js';
17
- export * from './types/token.js';
18
-
19
- export * from './utils/identity-resolver.js';
14
+ export type {
15
+ ClientAssertionCredentials,
16
+ ClientAssertionFetcher,
17
+ FetchClientAssertionParams,
18
+ } from './types/client-assertion.js';
19
+ export type { TokenInfo, ExchangeInfo, Session } from './types/token.js';
package/lib/resolvers.ts CHANGED
@@ -1,15 +1,15 @@
1
+ import type { ResolvedActor } from '@atcute/identity-resolver';
1
2
  import type { ActorIdentifier } from '@atcute/lexicons';
3
+ import type { OAuthAuthorizationServerMetadata, OAuthProtectedResourceMetadata } from '@atcute/oauth-types';
2
4
 
3
5
  import { identityResolver } from './environment.js';
4
6
  import { ResolverError } from './errors.js';
5
- import type { ResolvedIdentity } from './types/identity.js';
6
- import type { AuthorizationServerMetadata, ProtectedResourceMetadata } from './types/server.js';
7
7
  import { extractContentType } from './utils/response.js';
8
8
  import { isValidUrl } from './utils/strings.js';
9
9
 
10
10
  export const resolveFromIdentifier = async (
11
11
  ident: ActorIdentifier,
12
- ): Promise<{ identity: ResolvedIdentity; metadata: AuthorizationServerMetadata }> => {
12
+ ): Promise<{ identity: ResolvedActor; metadata: OAuthAuthorizationServerMetadata }> => {
13
13
  const identity = await identityResolver.resolve(ident);
14
14
 
15
15
  return {
@@ -20,14 +20,14 @@ export const resolveFromIdentifier = async (
20
20
 
21
21
  export const resolveFromService = async (
22
22
  host: string,
23
- ): Promise<{ metadata: AuthorizationServerMetadata }> => {
23
+ ): Promise<{ metadata: OAuthAuthorizationServerMetadata }> => {
24
24
  try {
25
25
  const metadata = await getMetadataFromResourceServer(host);
26
26
  return { metadata };
27
27
  } catch (err) {
28
28
  if (err instanceof ResolverError) {
29
29
  try {
30
- const metadata = await getAuthorizationServerMetadata(host);
30
+ const metadata = await getOAuthAuthorizationServerMetadata(host);
31
31
  return { metadata };
32
32
  } catch {}
33
33
  }
@@ -36,7 +36,7 @@ export const resolveFromService = async (
36
36
  }
37
37
  };
38
38
 
39
- const getProtectedResourceMetadata = async (host: string): Promise<ProtectedResourceMetadata> => {
39
+ const getOAuthProtectedResourceMetadata = async (host: string): Promise<OAuthProtectedResourceMetadata> => {
40
40
  const url = new URL(`/.well-known/oauth-protected-resource`, host);
41
41
  const response = await fetch(url.href, {
42
42
  redirect: 'manual',
@@ -49,7 +49,7 @@ const getProtectedResourceMetadata = async (host: string): Promise<ProtectedReso
49
49
  throw new ResolverError(`unexpected response`);
50
50
  }
51
51
 
52
- const metadata = (await response.json()) as ProtectedResourceMetadata;
52
+ const metadata = (await response.json()) as OAuthProtectedResourceMetadata;
53
53
  if (metadata.resource !== url.origin) {
54
54
  throw new ResolverError(`unexpected issuer`);
55
55
  }
@@ -57,7 +57,9 @@ const getProtectedResourceMetadata = async (host: string): Promise<ProtectedReso
57
57
  return metadata;
58
58
  };
59
59
 
60
- const getAuthorizationServerMetadata = async (host: string): Promise<AuthorizationServerMetadata> => {
60
+ const getOAuthAuthorizationServerMetadata = async (
61
+ host: string,
62
+ ): Promise<OAuthAuthorizationServerMetadata> => {
61
63
  const url = new URL(`/.well-known/oauth-authorization-server`, host);
62
64
  const response = await fetch(url.href, {
63
65
  redirect: 'manual',
@@ -70,7 +72,7 @@ const getAuthorizationServerMetadata = async (host: string): Promise<Authorizati
70
72
  throw new ResolverError(`unexpected response`);
71
73
  }
72
74
 
73
- const metadata = (await response.json()) as AuthorizationServerMetadata;
75
+ const metadata = (await response.json()) as OAuthAuthorizationServerMetadata;
74
76
  if (metadata.issuer !== url.origin) {
75
77
  throw new ResolverError(`unexpected issuer`);
76
78
  }
@@ -93,7 +95,7 @@ const getAuthorizationServerMetadata = async (host: string): Promise<Authorizati
93
95
  };
94
96
 
95
97
  const getMetadataFromResourceServer = async (input: string) => {
96
- const rs_metadata = await getProtectedResourceMetadata(input);
98
+ const rs_metadata = await getOAuthProtectedResourceMetadata(input);
97
99
 
98
100
  if (rs_metadata.authorization_servers?.length !== 1) {
99
101
  throw new ResolverError(`expected exactly one authorization server in the listing`);
@@ -101,7 +103,7 @@ const getMetadataFromResourceServer = async (input: string) => {
101
103
 
102
104
  const issuer = rs_metadata.authorization_servers[0];
103
105
 
104
- const as_metadata = await getAuthorizationServerMetadata(issuer);
106
+ const as_metadata = await getOAuthAuthorizationServerMetadata(issuer);
105
107
 
106
108
  if (as_metadata.protected_resources) {
107
109
  if (!as_metadata.protected_resources.includes(rs_metadata.resource)) {
package/lib/store/db.ts CHANGED
@@ -1,9 +1,9 @@
1
1
  import type { Did } from '@atcute/lexicons';
2
+ import type { DpopPrivateJwk } from '@atcute/oauth-crypto';
3
+ import type { OAuthAuthorizationServerMetadata } from '@atcute/oauth-types';
2
4
 
3
- import type { DPoPKey } from '../types/dpop.js';
4
- import type { AuthorizationServerMetadata } from '../types/server.js';
5
5
  import type { SimpleStore } from '../types/store.js';
6
- import type { Session } from '../types/token.js';
6
+ import type { RawSession } from '../types/token.js';
7
7
  import { locks } from '../utils/runtime.js';
8
8
 
9
9
  export interface OAuthDatabaseOptions {
@@ -19,7 +19,7 @@ interface SchemaItem<T> {
19
19
  interface Schema {
20
20
  sessions: {
21
21
  key: Did;
22
- value: Session;
22
+ value: RawSession;
23
23
  indexes: {
24
24
  expiresAt: number;
25
25
  };
@@ -27,8 +27,8 @@ interface Schema {
27
27
  states: {
28
28
  key: string;
29
29
  value: {
30
- dpopKey: DPoPKey;
31
- metadata: AuthorizationServerMetadata;
30
+ dpopKey: DpopPrivateJwk;
31
+ metadata: OAuthAuthorizationServerMetadata;
32
32
  verifier?: string;
33
33
  state?: unknown;
34
34
  };
@@ -6,18 +6,16 @@ export interface ClientAssertionCredentials {
6
6
  }
7
7
 
8
8
  export interface FetchClientAssertionParams {
9
- /** JWK thumbprint of the DPoP key to bind the assertion to */
10
- jkt: string;
11
9
  /** authorization server issuer (audience for the assertion) */
12
10
  aud: string;
13
-
14
11
  /**
15
12
  * create a DPoP proof to prove you possess the key for the claimed jkt.
16
13
  *
17
14
  * @param htu origin and pathname to your backend
15
+ * @param nonce optional DPoP nonce from the server
18
16
  * @returns DPoP proof that can be included in the assertion
19
17
  */
20
- createDpopProof: (htu: string) => Promise<string>;
18
+ createDpopProof: (htu: string, nonce?: string) => Promise<string>;
21
19
  }
22
20
 
23
21
  export type ClientAssertionFetcher = (
@@ -1,62 +1,7 @@
1
- export interface ProtectedResourceMetadata {
2
- resource: string;
3
- jwks_uri?: string;
4
- authorization_servers?: string[];
5
- scopes_supported?: string[];
6
- bearer_methods_supported?: ('header' | 'body' | 'query')[];
7
- resource_signing_alg_values_supported?: string[];
8
- resource_documentation?: string;
9
- resource_policy_uri?: string;
10
- resource_tos_uri?: string;
11
- }
12
-
13
- export interface AuthorizationServerMetadata {
14
- issuer: string;
15
- authorization_endpoint: string;
16
- token_endpoint: string;
17
- jwks_uri?: string;
18
- scopes_supported?: string[];
19
- claims_supported?: string[];
20
- claims_locales_supported?: string[];
21
- claims_parameter_supported?: boolean;
22
- request_parameter_supported?: boolean;
23
- request_uri_parameter_supported?: boolean;
24
- require_request_uri_registration?: boolean;
25
- subject_types_supported?: string[];
26
- response_types_supported?: string[];
27
- response_modes_supported?: string[];
28
- grant_types_supported?: string[];
29
- code_challenge_methods_supported?: string[];
30
- ui_locales_supported?: string[];
31
- id_token_signing_alg_values_supported?: string[];
32
- display_values_supported?: string[];
33
- request_object_signing_alg_values_supported?: string[];
34
- authorization_response_iss_parameter_supported?: boolean;
35
- authorization_details_types_supported?: string[];
36
- request_object_encryption_alg_values_supported?: string[];
37
- request_object_encryption_enc_values_supported?: string[];
38
- token_endpoint_auth_methods_supported?: string[];
39
- token_endpoint_auth_signing_alg_values_supported?: string[];
40
- revocation_endpoint?: string;
41
- revocation_endpoint_auth_methods_supported?: string[];
42
- revocation_endpoint_auth_signing_alg_values_supported?: string[];
43
- introspection_endpoint?: string;
44
- introspection_endpoint_auth_methods_supported?: string[];
45
- introspection_endpoint_auth_signing_alg_values_supported?: string[];
46
- pushed_authorization_request_endpoint?: string;
47
- pushed_authorization_request_endpoint_auth_methods_supported?: string[];
48
- pushed_authorization_request_endpoint_auth_signing_alg_values_supported?: string[];
49
- require_pushed_authorization_requests?: boolean;
50
- userinfo_endpoint?: string;
51
- end_session_endpoint?: string;
52
- registration_endpoint?: string;
53
- dpop_signing_alg_values_supported?: string[];
54
- protected_resources?: string[];
55
- client_id_metadata_document_supported?: boolean;
56
- }
1
+ import type { OAuthAuthorizationServerMetadata } from '@atcute/oauth-types';
57
2
 
58
3
  export interface PersistedAuthorizationServerMetadata extends Pick<
59
- AuthorizationServerMetadata,
4
+ OAuthAuthorizationServerMetadata,
60
5
  | 'issuer'
61
6
  | 'authorization_endpoint'
62
7
  | 'introspection_endpoint'
@@ -1,29 +1,9 @@
1
1
  import type { Did } from '@atcute/lexicons';
2
+ import type { DpopPrivateJwk } from '@atcute/oauth-crypto';
2
3
 
3
- import type { DPoPKey } from './dpop.js';
4
- import type { PersistedAuthorizationServerMetadata } from './server.js';
4
+ import type { LegacyDpopKey } from '../utils/dpop-key.js';
5
5
 
6
- export interface OAuthTokenResponse {
7
- access_token: string;
8
- // Can be DPoP or Bearer, normalize casing.
9
- token_type: string;
10
- issuer?: string;
11
- sub?: string;
12
- scope?: string;
13
- id_token?: `${string}.${string}.${string}`;
14
- refresh_token?: string;
15
- expires_in?: number;
16
- authorization_details?:
17
- | {
18
- type: string;
19
- locations?: string[];
20
- actions?: string[];
21
- datatypes?: string[];
22
- identifier?: string;
23
- privileges?: string[];
24
- }[]
25
- | undefined;
26
- }
6
+ import type { PersistedAuthorizationServerMetadata } from './server.js';
27
7
 
28
8
  export interface TokenInfo {
29
9
  scope: string;
@@ -39,8 +19,14 @@ export interface ExchangeInfo {
39
19
  server: PersistedAuthorizationServerMetadata;
40
20
  }
41
21
 
22
+ export interface RawSession {
23
+ dpopKey: DpopPrivateJwk | LegacyDpopKey;
24
+ info: ExchangeInfo;
25
+ token: TokenInfo;
26
+ }
27
+
42
28
  export interface Session {
43
- dpopKey: DPoPKey;
29
+ dpopKey: DpopPrivateJwk;
44
30
  info: ExchangeInfo;
45
31
  token: TokenInfo;
46
32
  }
@@ -0,0 +1,24 @@
1
+ import { fromBase64Url } from '@atcute/multibase';
2
+ import type { DpopPrivateJwk } from '@atcute/oauth-crypto';
3
+
4
+ export interface LegacyDpopKey {
5
+ typ: 'ES256';
6
+ key: string;
7
+ jwt: string;
8
+ jkt?: string;
9
+ }
10
+
11
+ const ES256_ALG = { name: 'ECDSA', namedCurve: 'P-256' } as const;
12
+
13
+ export const isLegacyDpopKey = (key: DpopPrivateJwk | LegacyDpopKey): key is LegacyDpopKey => {
14
+ return typeof (key as LegacyDpopKey).key === 'string' && typeof (key as LegacyDpopKey).jwt === 'string';
15
+ };
16
+
17
+ export const migrateLegacyDpopKey = async (key: LegacyDpopKey): Promise<DpopPrivateJwk> => {
18
+ const pkcs8 = fromBase64Url(key.key);
19
+ const cryptoKey = await crypto.subtle.importKey('pkcs8', pkcs8, ES256_ALG, true, ['sign']);
20
+ const jwk = (await crypto.subtle.exportKey('jwk', cryptoKey)) as DpopPrivateJwk;
21
+ jwk.alg = 'ES256';
22
+
23
+ return jwk;
24
+ };
@@ -1,23 +1 @@
1
- import { nanoid } from 'nanoid';
2
-
3
- import { toBase64Url } from '@atcute/multibase';
4
- import { encodeUtf8, toSha256 } from '@atcute/uint8array';
5
-
6
1
  export const locks: LockManager | undefined = typeof navigator !== 'undefined' ? navigator.locks : undefined;
7
-
8
- export const stringToSha256 = async (input: string): Promise<string> => {
9
- const bytes = encodeUtf8(input);
10
- const digest = await toSha256(bytes);
11
-
12
- return toBase64Url(digest);
13
- };
14
-
15
- export const generatePKCE = async (): Promise<{ verifier: string; challenge: string; method: string }> => {
16
- const verifier = nanoid(64);
17
-
18
- return {
19
- verifier: verifier,
20
- challenge: await stringToSha256(verifier),
21
- method: 'S256',
22
- };
23
- };
package/package.json CHANGED
@@ -1,7 +1,6 @@
1
1
  {
2
- "type": "module",
3
2
  "name": "@atcute/oauth-browser-client",
4
- "version": "2.0.3",
3
+ "version": "3.0.0",
5
4
  "description": "minimal OAuth browser client implementation for AT Protocol",
6
5
  "license": "0BSD",
7
6
  "repository": {
@@ -14,17 +13,22 @@
14
13
  "!lib/**/*.bench.ts",
15
14
  "!lib/**/*.test.ts"
16
15
  ],
16
+ "type": "module",
17
+ "sideEffects": false,
17
18
  "exports": {
18
19
  ".": "./dist/index.js"
19
20
  },
20
- "sideEffects": false,
21
+ "publishConfig": {
22
+ "access": "public"
23
+ },
21
24
  "dependencies": {
22
25
  "nanoid": "^5.1.6",
23
- "@atcute/client": "^4.1.1",
24
- "@atcute/identity-resolver": "^1.2.0",
25
- "@atcute/lexicons": "^1.2.5",
26
- "@atcute/uint8array": "^1.0.6",
27
- "@atcute/multibase": "^1.1.6"
26
+ "@atcute/client": "^4.2.1",
27
+ "@atcute/identity-resolver": "^1.2.2",
28
+ "@atcute/lexicons": "^1.2.7",
29
+ "@atcute/oauth-crypto": "^0.1.0",
30
+ "@atcute/multibase": "^1.1.7",
31
+ "@atcute/oauth-types": "^0.1.0"
28
32
  },
29
33
  "scripts": {
30
34
  "build": "tsgo --project tsconfig.build.json",
@@ -1,38 +0,0 @@
1
- export interface ClientMetadata {
2
- redirect_uris: string[];
3
- response_types: ('code' | 'token' | 'none' | 'code id_token token' | 'code id_token' | 'code token' | 'id_token token' | 'id_token')[];
4
- grant_types: ('authorization_code' | 'implicit' | 'refresh_token' | 'password' | 'client_credentials' | 'urn:ietf:params:oauth:grant-type:jwt-bearer' | 'urn:ietf:params:oauth:grant-type:saml2-bearer')[];
5
- scope?: string;
6
- token_endpoint_auth_method?: 'none' | 'client_secret_basic' | 'client_secret_jwt' | 'client_secret_post' | 'private_key_jwt' | 'self_signed_tls_client_auth' | 'tls_client_auth';
7
- token_endpoint_auth_signing_alg?: string;
8
- introspection_endpoint_auth_method?: 'none' | 'client_secret_basic' | 'client_secret_jwt' | 'client_secret_post' | 'private_key_jwt' | 'self_signed_tls_client_auth' | 'tls_client_auth';
9
- introspection_endpoint_auth_signing_alg?: string;
10
- revocation_endpoint_auth_method?: 'none' | 'client_secret_basic' | 'client_secret_jwt' | 'client_secret_post' | 'private_key_jwt' | 'self_signed_tls_client_auth' | 'tls_client_auth';
11
- revocation_endpoint_auth_signing_alg?: string;
12
- pushed_authorization_request_endpoint_auth_method?: 'none' | 'client_secret_basic' | 'client_secret_jwt' | 'client_secret_post' | 'private_key_jwt' | 'self_signed_tls_client_auth' | 'tls_client_auth';
13
- pushed_authorization_request_endpoint_auth_signing_alg?: string;
14
- userinfo_signed_response_alg?: string;
15
- userinfo_encrypted_response_alg?: string;
16
- jwks_uri?: string;
17
- jwks?: unknown;
18
- application_type?: 'web' | 'native';
19
- subject_type?: 'public' | 'pairwise';
20
- request_object_signing_alg?: string;
21
- id_token_signed_response_alg?: string;
22
- authorization_signed_response_alg?: string;
23
- authorization_encrypted_response_enc?: 'A128CBC-HS256';
24
- authorization_encrypted_response_alg?: string;
25
- client_id?: string;
26
- client_name?: string;
27
- client_uri?: string;
28
- policy_uri?: string;
29
- tos_uri?: string;
30
- logo_uri?: string;
31
- default_max_age?: number;
32
- require_auth_time?: boolean;
33
- contacts?: string[];
34
- tls_client_certificate_bound_access_tokens?: boolean;
35
- dpop_bound_access_tokens?: boolean;
36
- authorization_details_types?: string[];
37
- }
38
- //# sourceMappingURL=client.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../lib/types/client.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,cAAc;IAC9B,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,cAAc,EAAE,CACb,MAAM,GACN,OAAO,GACP,MAAM,GACN,qBAAqB,GACrB,eAAe,GACf,YAAY,GACZ,gBAAgB,GAChB,UAAU,CACZ,EAAE,CAAC;IACJ,WAAW,EAAE,CACV,oBAAoB,GACpB,UAAU,GACV,eAAe,GACf,UAAU,GACV,oBAAoB,GACpB,6CAA6C,GAC7C,+CAA+C,CACjD,EAAE,CAAC;IACJ,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,0BAA0B,CAAC,EACxB,MAAM,GACN,qBAAqB,GACrB,mBAAmB,GACnB,oBAAoB,GACpB,iBAAiB,GACjB,6BAA6B,GAC7B,iBAAiB,CAAC;IACrB,+BAA+B,CAAC,EAAE,MAAM,CAAC;IACzC,kCAAkC,CAAC,EAChC,MAAM,GACN,qBAAqB,GACrB,mBAAmB,GACnB,oBAAoB,GACpB,iBAAiB,GACjB,6BAA6B,GAC7B,iBAAiB,CAAC;IACrB,uCAAuC,CAAC,EAAE,MAAM,CAAC;IACjD,+BAA+B,CAAC,EAC7B,MAAM,GACN,qBAAqB,GACrB,mBAAmB,GACnB,oBAAoB,GACpB,iBAAiB,GACjB,6BAA6B,GAC7B,iBAAiB,CAAC;IACrB,oCAAoC,CAAC,EAAE,MAAM,CAAC;IAC9C,iDAAiD,CAAC,EAC/C,MAAM,GACN,qBAAqB,GACrB,mBAAmB,GACnB,oBAAoB,GACpB,iBAAiB,GACjB,6BAA6B,GAC7B,iBAAiB,CAAC;IACrB,sDAAsD,CAAC,EAAE,MAAM,CAAC;IAChE,4BAA4B,CAAC,EAAE,MAAM,CAAC;IACtC,+BAA+B,CAAC,EAAE,MAAM,CAAC;IACzC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,gBAAgB,CAAC,EAAE,KAAK,GAAG,QAAQ,CAAC;IACpC,YAAY,CAAC,EAAE,QAAQ,GAAG,UAAU,CAAC;IACrC,0BAA0B,CAAC,EAAE,MAAM,CAAC;IACpC,4BAA4B,CAAC,EAAE,MAAM,CAAC;IACtC,iCAAiC,CAAC,EAAE,MAAM,CAAC;IAC3C,oCAAoC,CAAC,EAAE,eAAe,CAAC;IACvD,oCAAoC,CAAC,EAAE,MAAM,CAAC;IAC9C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,0CAA0C,CAAC,EAAE,OAAO,CAAC;IACrD,wBAAwB,CAAC,EAAE,OAAO,CAAC;IACnC,2BAA2B,CAAC,EAAE,MAAM,EAAE,CAAC;CACvC"}
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=client.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"client.js","sourceRoot":"","sources":["../../lib/types/client.ts"],"names":[],"mappings":""}