@atproto/oauth-client 0.3.22 → 0.4.1

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 (48) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/dist/errors/auth-method-unsatisfiable-error.d.ts +3 -0
  3. package/dist/errors/auth-method-unsatisfiable-error.d.ts.map +1 -0
  4. package/dist/errors/auth-method-unsatisfiable-error.js +7 -0
  5. package/dist/errors/auth-method-unsatisfiable-error.js.map +1 -0
  6. package/dist/index.d.ts +2 -0
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +2 -0
  9. package/dist/index.js.map +1 -1
  10. package/dist/oauth-client-auth.d.ts +23 -0
  11. package/dist/oauth-client-auth.d.ts.map +1 -0
  12. package/dist/oauth-client-auth.js +131 -0
  13. package/dist/oauth-client-auth.js.map +1 -0
  14. package/dist/oauth-client.d.ts +13 -8
  15. package/dist/oauth-client.d.ts.map +1 -1
  16. package/dist/oauth-client.js +39 -13
  17. package/dist/oauth-client.js.map +1 -1
  18. package/dist/oauth-resolver.d.ts +1 -1
  19. package/dist/oauth-server-agent.d.ts +8 -6
  20. package/dist/oauth-server-agent.d.ts.map +1 -1
  21. package/dist/oauth-server-agent.js +19 -50
  22. package/dist/oauth-server-agent.js.map +1 -1
  23. package/dist/oauth-server-factory.d.ts +15 -2
  24. package/dist/oauth-server-factory.d.ts.map +1 -1
  25. package/dist/oauth-server-factory.js +23 -4
  26. package/dist/oauth-server-factory.js.map +1 -1
  27. package/dist/session-getter.d.ts +5 -0
  28. package/dist/session-getter.d.ts.map +1 -1
  29. package/dist/session-getter.js +24 -11
  30. package/dist/session-getter.js.map +1 -1
  31. package/dist/state-store.d.ts +3 -0
  32. package/dist/state-store.d.ts.map +1 -1
  33. package/dist/types.d.ts +8 -8
  34. package/dist/types.d.ts.map +1 -1
  35. package/dist/validate-client-metadata.d.ts.map +1 -1
  36. package/dist/validate-client-metadata.js +32 -26
  37. package/dist/validate-client-metadata.js.map +1 -1
  38. package/package.json +7 -7
  39. package/src/errors/auth-method-unsatisfiable-error.ts +1 -0
  40. package/src/index.ts +2 -0
  41. package/src/oauth-client-auth.ts +182 -0
  42. package/src/oauth-client.ts +89 -33
  43. package/src/oauth-server-agent.ts +19 -71
  44. package/src/oauth-server-factory.ts +37 -2
  45. package/src/session-getter.ts +43 -10
  46. package/src/state-store.ts +3 -0
  47. package/src/validate-client-metadata.ts +40 -27
  48. package/tsconfig.build.tsbuildinfo +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"validate-client-metadata.js","sourceRoot":"","sources":["../src/validate-client-metadata.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"validate-client-metadata.js","sourceRoot":"","sources":["../src/validate-client-metadata.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atproto/oauth-client",
3
- "version": "0.3.22",
3
+ "version": "0.4.1",
4
4
  "license": "MIT",
5
5
  "description": "OAuth client for ATPROTO PDS. This package serves as common base for environment-specific implementations (NodeJS, Browser, React-Native).",
6
6
  "keywords": [
@@ -27,16 +27,16 @@
27
27
  "dependencies": {
28
28
  "multiformats": "^9.9.0",
29
29
  "zod": "^3.23.8",
30
- "@atproto-labs/did-resolver": "0.1.13",
31
30
  "@atproto-labs/fetch": "0.2.3",
32
- "@atproto-labs/handle-resolver": "0.1.8",
33
- "@atproto-labs/identity-resolver": "0.1.18",
31
+ "@atproto-labs/handle-resolver": "0.2.0",
32
+ "@atproto-labs/identity-resolver": "0.1.19",
34
33
  "@atproto-labs/simple-store": "0.2.0",
35
34
  "@atproto-labs/simple-store-memory": "0.1.3",
36
35
  "@atproto/did": "0.1.5",
37
- "@atproto/jwk": "0.2.0",
38
- "@atproto/oauth-types": "0.2.8",
39
- "@atproto/xrpc": "0.7.0"
36
+ "@atproto/jwk": "0.4.0",
37
+ "@atproto/oauth-types": "0.3.1",
38
+ "@atproto/xrpc": "0.7.0",
39
+ "@atproto-labs/did-resolver": "0.2.0"
40
40
  },
41
41
  "devDependencies": {
42
42
  "typescript": "^5.6.3"
@@ -0,0 +1 @@
1
+ export class AuthMethodUnsatisfiableError extends Error {}
package/src/index.ts CHANGED
@@ -7,8 +7,10 @@ export {
7
7
  export * from '@atproto-labs/handle-resolver'
8
8
 
9
9
  export * from '@atproto/did'
10
+ export * from '@atproto/jwk'
10
11
  export * from '@atproto/oauth-types'
11
12
 
13
+ export * from './lock.js'
12
14
  export * from './oauth-authorization-server-metadata-resolver.js'
13
15
  export * from './oauth-callback-error.js'
14
16
  export * from './oauth-client.js'
@@ -0,0 +1,182 @@
1
+ import { Keyset } from '@atproto/jwk'
2
+ import {
3
+ CLIENT_ASSERTION_TYPE_JWT_BEARER,
4
+ OAuthAuthorizationServerMetadata,
5
+ OAuthClientCredentials,
6
+ } from '@atproto/oauth-types'
7
+ import { FALLBACK_ALG } from './constants.js'
8
+ import { AuthMethodUnsatisfiableError } from './errors/auth-method-unsatisfiable-error.js'
9
+ import { Runtime } from './runtime.js'
10
+ import { ClientMetadata } from './types.js'
11
+ import { Awaitable } from './util.js'
12
+
13
+ export type ClientAuthMethod =
14
+ | { method: 'none' }
15
+ | { method: 'private_key_jwt'; kid: string }
16
+
17
+ export function negotiateClientAuthMethod(
18
+ serverMetadata: OAuthAuthorizationServerMetadata,
19
+ clientMetadata: ClientMetadata,
20
+ keyset?: Keyset,
21
+ ): ClientAuthMethod {
22
+ const method = clientMetadata.token_endpoint_auth_method
23
+
24
+ // @NOTE ATproto spec requires that AS support both "none" and
25
+ // "private_key_jwt", and that clients use one of the other. The following
26
+ // check ensures that the AS is indeed compliant with this client's
27
+ // configuration.
28
+ const methods = supportedMethods(serverMetadata)
29
+ if (!methods.includes(method)) {
30
+ throw new Error(
31
+ `The server does not support "${method}" authentication. Supported methods are: ${methods.join(
32
+ ', ',
33
+ )}.`,
34
+ )
35
+ }
36
+
37
+ if (method === 'private_key_jwt') {
38
+ // Invalid client configuration. This should not happen as
39
+ // "validateClientMetadata" already check this.
40
+ if (!keyset) throw new Error('A keyset is required for private_key_jwt')
41
+
42
+ const alg = supportedAlgs(serverMetadata)
43
+
44
+ // @NOTE we can't use `keyset.findPrivateKey` here because we can't enforce
45
+ // that the returned key contains a "kid". The following implementation is
46
+ // more robust against keysets containing keys without a "kid" property.
47
+ for (const key of keyset.list({ use: 'sig', alg })) {
48
+ // Return the first key from the key set that matches the server's
49
+ // supported algorithms.
50
+ if (key.isPrivate && key.kid) {
51
+ return { method: 'private_key_jwt', kid: key.kid }
52
+ }
53
+ }
54
+
55
+ throw new Error(
56
+ alg.includes(FALLBACK_ALG)
57
+ ? `Client authentication method "${method}" requires at least one "${FALLBACK_ALG}" signing key with a "kid" property`
58
+ : // AS is not compliant with the ATproto OAuth spec.
59
+ `Authorization server requires "${method}" authentication method, but does not support "${FALLBACK_ALG}" algorithm.`,
60
+ )
61
+ }
62
+
63
+ if (method === 'none') {
64
+ return { method: 'none' }
65
+ }
66
+
67
+ throw new Error(
68
+ `The ATProto OAuth spec requires that client use either "none" or "private_key_jwt" authentication method.` +
69
+ (method === 'client_secret_basic'
70
+ ? ' You might want to explicitly set "token_endpoint_auth_method" to one of those values in the client metadata document.'
71
+ : ` You set "${method}" which is not allowed.`),
72
+ )
73
+ }
74
+
75
+ export type ClientCredentialsFactory = () => Awaitable<{
76
+ headers?: Record<string, string>
77
+ payload?: OAuthClientCredentials
78
+ }>
79
+
80
+ /**
81
+ * @throws {AuthMethodUnsatisfiableError} if the authentication method is no
82
+ * long usable (either because the AS changed, of because the key is no longer
83
+ * available in the keyset).
84
+ */
85
+ export function createClientCredentialsFactory(
86
+ authMethod: ClientAuthMethod,
87
+ serverMetadata: OAuthAuthorizationServerMetadata,
88
+ clientMetadata: ClientMetadata,
89
+ runtime: Runtime,
90
+ keyset?: Keyset,
91
+ ): ClientCredentialsFactory {
92
+ // Ensure the AS still supports the auth method.
93
+ if (!supportedMethods(serverMetadata).includes(authMethod.method)) {
94
+ throw new AuthMethodUnsatisfiableError(
95
+ `Client authentication method "${authMethod.method}" no longer supported`,
96
+ )
97
+ }
98
+
99
+ if (authMethod.method === 'none') {
100
+ return () => ({
101
+ payload: {
102
+ client_id: clientMetadata.client_id,
103
+ },
104
+ })
105
+ }
106
+
107
+ if (authMethod.method === 'private_key_jwt') {
108
+ try {
109
+ // The client used to be a confidential client but no longer has a keyset.
110
+ if (!keyset) throw new Error('A keyset is required for private_key_jwt')
111
+
112
+ // @NOTE throws if no matching key can be found
113
+ const { key, alg } = keyset.findPrivateKey({
114
+ use: 'sig',
115
+ kid: authMethod.kid,
116
+ alg: supportedAlgs(serverMetadata),
117
+ })
118
+
119
+ // https://www.rfc-editor.org/rfc/rfc7523.html#section-3
120
+ return async () => ({
121
+ payload: {
122
+ client_id: clientMetadata.client_id,
123
+ client_assertion_type: CLIENT_ASSERTION_TYPE_JWT_BEARER,
124
+ client_assertion: await key.createJwt(
125
+ { alg },
126
+ {
127
+ // > The JWT MUST contain an "iss" (issuer) claim that contains a
128
+ // > unique identifier for the entity that issued the JWT.
129
+ iss: clientMetadata.client_id,
130
+ // > For client authentication, the subject MUST be the
131
+ // > "client_id" of the OAuth client.
132
+ sub: clientMetadata.client_id,
133
+ // > The JWT MUST contain an "aud" (audience) claim containing a value
134
+ // > that identifies the authorization server as an intended audience.
135
+ // > The token endpoint URL of the authorization server MAY be used as a
136
+ // > value for an "aud" element to identify the authorization server as an
137
+ // > intended audience of the JWT.
138
+ aud: serverMetadata.issuer,
139
+ // > The JWT MAY contain a "jti" (JWT ID) claim that provides a
140
+ // > unique identifier for the token.
141
+ jti: await runtime.generateNonce(),
142
+ // > The JWT MAY contain an "iat" (issued at) claim that
143
+ // > identifies the time at which the JWT was issued.
144
+ iat: Math.floor(Date.now() / 1000),
145
+ // > The JWT MUST contain an "exp" (expiration time) claim that
146
+ // > limits the time window during which the JWT can be used.
147
+ exp: Math.floor(Date.now() / 1000) + 60, // 1 minute
148
+ },
149
+ ),
150
+ },
151
+ })
152
+ } catch (cause) {
153
+ throw new AuthMethodUnsatisfiableError('Failed to load private key', {
154
+ cause,
155
+ })
156
+ }
157
+ }
158
+
159
+ throw new AuthMethodUnsatisfiableError(
160
+ // @ts-expect-error
161
+ `Unsupported auth method ${authMethod.method}`,
162
+ )
163
+ }
164
+
165
+ function supportedMethods(serverMetadata: OAuthAuthorizationServerMetadata) {
166
+ return serverMetadata['token_endpoint_auth_methods_supported']
167
+ }
168
+
169
+ function supportedAlgs(serverMetadata: OAuthAuthorizationServerMetadata) {
170
+ return (
171
+ serverMetadata['token_endpoint_auth_signing_alg_values_supported'] ?? [
172
+ // @NOTE If not specified, assume that the server supports the ES256
173
+ // algorithm, as prescribed by the spec:
174
+ //
175
+ // > Clients and Authorization Servers currently must support the ES256
176
+ // > cryptographic system [for client authentication].
177
+ //
178
+ // https://atproto.com/specs/oauth#confidential-client-authentication
179
+ FALLBACK_ALG,
180
+ ]
181
+ )
182
+ }
@@ -10,6 +10,7 @@ import {
10
10
  import {
11
11
  AtprotoDid,
12
12
  DidCache,
13
+ DidResolver,
13
14
  DidResolverCached,
14
15
  DidResolverCommon,
15
16
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
@@ -18,20 +19,22 @@ import {
18
19
  } from '@atproto-labs/did-resolver'
19
20
  import { Fetch } from '@atproto-labs/fetch'
20
21
  import {
21
- AppViewHandleResolver,
22
22
  CachedHandleResolver,
23
23
  HandleCache,
24
24
  HandleResolver,
25
+ XrpcHandleResolver,
25
26
  } from '@atproto-labs/handle-resolver'
26
27
  import { IdentityResolver } from '@atproto-labs/identity-resolver'
27
28
  import { SimpleStoreMemory } from '@atproto-labs/simple-store-memory'
28
29
  import { FALLBACK_ALG } from './constants.js'
30
+ import { AuthMethodUnsatisfiableError } from './errors/auth-method-unsatisfiable-error.js'
29
31
  import { TokenRevokedError } from './errors/token-revoked-error.js'
30
32
  import {
31
33
  AuthorizationServerMetadataCache,
32
34
  OAuthAuthorizationServerMetadataResolver,
33
35
  } from './oauth-authorization-server-metadata-resolver.js'
34
36
  import { OAuthCallbackError } from './oauth-callback-error.js'
37
+ import { negotiateClientAuthMethod } from './oauth-client-auth.js'
35
38
  import {
36
39
  OAuthProtectedResourceMetadataResolver,
37
40
  ProtectedResourceMetadataCache,
@@ -53,23 +56,23 @@ import { CustomEventTarget } from './util.js'
53
56
  import { validateClientMetadata } from './validate-client-metadata.js'
54
57
 
55
58
  // Export all types needed to construct OAuthClientOptions
56
- export type {
57
- AuthorizationServerMetadataCache,
58
- DidCache,
59
- DpopNonceCache,
60
- Fetch,
61
- HandleCache,
62
- HandleResolver,
63
- InternalStateData,
59
+ export {
60
+ type AuthorizationServerMetadataCache,
61
+ type DidCache,
62
+ type DpopNonceCache,
63
+ type Fetch,
64
+ type HandleCache,
65
+ type HandleResolver,
66
+ type InternalStateData,
64
67
  Key,
65
68
  Keyset,
66
- OAuthClientMetadata,
67
- OAuthClientMetadataInput,
68
- OAuthResponseMode,
69
- ProtectedResourceMetadataCache,
70
- RuntimeImplementation,
71
- SessionStore,
72
- StateStore,
69
+ type OAuthClientMetadata,
70
+ type OAuthClientMetadataInput,
71
+ type OAuthResponseMode,
72
+ type ProtectedResourceMetadataCache,
73
+ type RuntimeImplementation,
74
+ type SessionStore,
75
+ type StateStore,
73
76
  }
74
77
 
75
78
  export type OAuthClientOptions = {
@@ -103,7 +106,12 @@ export type OAuthClientOptions = {
103
106
  dpopNonceCache?: DpopNonceCache
104
107
 
105
108
  // Services
109
+ didResolver?: DidResolver<'plc' | 'web'>
106
110
  handleResolver: HandleResolver | URL | string
111
+ /**
112
+ * Used to instantiate the {@link OAuthClientOptions.didResolver} if none
113
+ * is provided.
114
+ */
107
115
  plcDirectoryUrl?: URL | string
108
116
  runtimeImplementation: RuntimeImplementation
109
117
  fetch?: Fetch
@@ -186,6 +194,7 @@ export class OAuthClient extends CustomEventTarget<OAuthClientEventMap> {
186
194
 
187
195
  responseMode,
188
196
  clientMetadata,
197
+ didResolver,
189
198
  handleResolver,
190
199
  plcDirectoryUrl,
191
200
  runtimeImplementation,
@@ -205,14 +214,23 @@ export class OAuthClient extends CustomEventTarget<OAuthClientEventMap> {
205
214
  this.fetch = fetch
206
215
  this.oauthResolver = new OAuthResolver(
207
216
  new IdentityResolver(
208
- new DidResolverCached(
209
- new DidResolverCommon({ fetch, plcDirectoryUrl, allowHttp }),
210
- didCache,
211
- ),
212
- new CachedHandleResolver(
213
- AppViewHandleResolver.from(handleResolver, { fetch }),
214
- handleCache,
215
- ),
217
+ didResolver instanceof DidResolverCached && !didCache
218
+ ? didResolver
219
+ : new DidResolverCached(
220
+ didResolver != null
221
+ ? didResolver
222
+ : new DidResolverCommon({ fetch, plcDirectoryUrl, allowHttp }),
223
+ didCache,
224
+ ),
225
+ handleResolver instanceof CachedHandleResolver && !handleCache
226
+ ? handleResolver
227
+ : new CachedHandleResolver(
228
+ typeof handleResolver === 'string' ||
229
+ handleResolver instanceof URL
230
+ ? new XrpcHandleResolver(handleResolver, { fetch })
231
+ : handleResolver,
232
+ handleCache,
233
+ ),
216
234
  ),
217
235
  new OAuthProtectedResourceMetadataResolver(
218
236
  protectedResourceMetadataCache,
@@ -290,11 +308,17 @@ export class OAuthClient extends CustomEventTarget<OAuthClientEventMap> {
290
308
  metadata.dpop_signing_alg_values_supported || [FALLBACK_ALG],
291
309
  )
292
310
 
311
+ const authMethod = negotiateClientAuthMethod(
312
+ metadata,
313
+ this.clientMetadata,
314
+ this.keyset,
315
+ )
293
316
  const state = await this.runtime.generateNonce()
294
317
 
295
318
  await this.stateStore.set(state, {
296
319
  iss: metadata.issuer,
297
320
  dpopKey,
321
+ authMethod,
298
322
  verifier: pkce.verifier,
299
323
  appState: options?.state,
300
324
  })
@@ -327,7 +351,11 @@ export class OAuthClient extends CustomEventTarget<OAuthClientEventMap> {
327
351
  }
328
352
 
329
353
  if (metadata.pushed_authorization_request_endpoint) {
330
- const server = await this.serverFactory.fromMetadata(metadata, dpopKey)
354
+ const server = await this.serverFactory.fromMetadata(
355
+ metadata,
356
+ authMethod,
357
+ dpopKey,
358
+ )
331
359
  const parResponse = await server.request(
332
360
  'pushed_authorization_request',
333
361
  parameters,
@@ -423,6 +451,8 @@ export class OAuthClient extends CustomEventTarget<OAuthClientEventMap> {
423
451
 
424
452
  const server = await this.serverFactory.fromIssuer(
425
453
  stateData.iss,
454
+ // Using the literal 'legacy' if the authMethod is not defined (because stateData was created through an old version of this lib)
455
+ stateData.authMethod ?? 'legacy',
426
456
  stateData.dpopKey,
427
457
  )
428
458
 
@@ -455,6 +485,7 @@ export class OAuthClient extends CustomEventTarget<OAuthClientEventMap> {
455
485
  try {
456
486
  await this.sessionGetter.setStored(tokenSet.sub, {
457
487
  dpopKey: stateData.dpopKey,
488
+ authMethod: server.authMethod,
458
489
  tokenSet,
459
490
  })
460
491
 
@@ -486,24 +517,45 @@ export class OAuthClient extends CustomEventTarget<OAuthClientEventMap> {
486
517
  // sub arg is lightly typed for convenience of library user
487
518
  assertAtprotoDid(sub)
488
519
 
489
- const { dpopKey, tokenSet } = await this.sessionGetter.get(sub, {
520
+ const {
521
+ dpopKey,
522
+ authMethod = 'legacy',
523
+ tokenSet,
524
+ } = await this.sessionGetter.get(sub, {
490
525
  noCache: refresh === true,
491
526
  allowStale: refresh === false,
492
527
  })
493
528
 
494
- const server = await this.serverFactory.fromIssuer(tokenSet.iss, dpopKey, {
495
- noCache: refresh === true,
496
- allowStale: refresh === false,
497
- })
529
+ try {
530
+ const server = await this.serverFactory.fromIssuer(
531
+ tokenSet.iss,
532
+ authMethod,
533
+ dpopKey,
534
+ {
535
+ noCache: refresh === true,
536
+ allowStale: refresh === false,
537
+ },
538
+ )
498
539
 
499
- return this.createSession(server, sub)
540
+ return this.createSession(server, sub)
541
+ } catch (err) {
542
+ if (err instanceof AuthMethodUnsatisfiableError) {
543
+ await this.sessionGetter.delStored(sub, err)
544
+ }
545
+
546
+ throw err
547
+ }
500
548
  }
501
549
 
502
550
  async revoke(sub: string) {
503
551
  // sub arg is lightly typed for convenience of library user
504
552
  assertAtprotoDid(sub)
505
553
 
506
- const { dpopKey, tokenSet } = await this.sessionGetter.get(sub, {
554
+ const {
555
+ dpopKey,
556
+ authMethod = 'legacy',
557
+ tokenSet,
558
+ } = await this.sessionGetter.get(sub, {
507
559
  allowStale: true,
508
560
  })
509
561
 
@@ -511,7 +563,11 @@ export class OAuthClient extends CustomEventTarget<OAuthClientEventMap> {
511
563
  // the tokens to be deleted even if it was not possible to fetch the issuer
512
564
  // data.
513
565
  try {
514
- const server = await this.serverFactory.fromIssuer(tokenSet.iss, dpopKey)
566
+ const server = await this.serverFactory.fromIssuer(
567
+ tokenSet.iss,
568
+ authMethod,
569
+ dpopKey,
570
+ )
515
571
  await server.revoke(tokenSet.access_token)
516
572
  } finally {
517
573
  await this.sessionGetter.delStored(sub, new TokenRevokedError(sub))
@@ -1,10 +1,8 @@
1
1
  import { AtprotoDid } from '@atproto/did'
2
2
  import { Key, Keyset } from '@atproto/jwk'
3
3
  import {
4
- CLIENT_ASSERTION_TYPE_JWT_BEARER,
5
4
  OAuthAuthorizationRequestPar,
6
5
  OAuthAuthorizationServerMetadata,
7
- OAuthClientCredentials,
8
6
  OAuthEndpointName,
9
7
  OAuthParResponse,
10
8
  OAuthTokenRequest,
@@ -17,9 +15,13 @@ import {
17
15
  AtprotoTokenResponse,
18
16
  atprotoTokenResponseSchema,
19
17
  } from './atproto-token-response.js'
20
- import { FALLBACK_ALG } from './constants.js'
21
18
  import { TokenRefreshError } from './errors/token-refresh-error.js'
22
19
  import { dpopFetchWrapper } from './fetch-dpop.js'
20
+ import {
21
+ ClientAuthMethod,
22
+ ClientCredentialsFactory,
23
+ createClientCredentialsFactory,
24
+ } from './oauth-client-auth.js'
23
25
  import { OAuthResolver } from './oauth-resolver.js'
24
26
  import { OAuthResponseError } from './oauth-response-error.js'
25
27
  import { Runtime } from './runtime.js'
@@ -43,8 +45,13 @@ export type DpopNonceCache = SimpleStore<string, string>
43
45
 
44
46
  export class OAuthServerAgent {
45
47
  protected dpopFetch: Fetch<unknown>
48
+ protected clientCredentialsFactory: ClientCredentialsFactory
46
49
 
50
+ /**
51
+ * @throws see {@link createClientCredentialsFactory}
52
+ */
47
53
  constructor(
54
+ readonly authMethod: ClientAuthMethod,
48
55
  readonly dpopKey: Key,
49
56
  readonly serverMetadata: OAuthAuthorizationServerMetadata,
50
57
  readonly clientMetadata: ClientMetadata,
@@ -54,6 +61,14 @@ export class OAuthServerAgent {
54
61
  readonly keyset?: Keyset,
55
62
  fetch?: Fetch,
56
63
  ) {
64
+ this.clientCredentialsFactory = createClientCredentialsFactory(
65
+ authMethod,
66
+ serverMetadata,
67
+ clientMetadata,
68
+ runtime,
69
+ keyset,
70
+ )
71
+
57
72
  this.dpopFetch = dpopFetchWrapper<void>({
58
73
  fetch: bindFetch(fetch),
59
74
  key: dpopKey,
@@ -204,7 +219,7 @@ export class OAuthServerAgent {
204
219
  const url = this.serverMetadata[`${endpoint}_endpoint`]
205
220
  if (!url) throw new Error(`No ${endpoint} endpoint available`)
206
221
 
207
- const auth = await this.buildClientAuth(endpoint)
222
+ const auth = await this.clientCredentialsFactory()
208
223
 
209
224
  // https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-13#section-3.2.2
210
225
  // https://datatracker.ietf.org/doc/html/rfc7009#section-2.1
@@ -232,73 +247,6 @@ export class OAuthServerAgent {
232
247
  throw new OAuthResponseError(response, json)
233
248
  }
234
249
  }
235
-
236
- async buildClientAuth(endpoint: OAuthEndpointName): Promise<{
237
- headers?: Record<string, string>
238
- payload: OAuthClientCredentials
239
- }> {
240
- const methodSupported =
241
- this.serverMetadata[`token_endpoint_auth_methods_supported`]
242
-
243
- const method = this.clientMetadata[`token_endpoint_auth_method`]
244
-
245
- if (
246
- method === 'private_key_jwt' ||
247
- (this.keyset &&
248
- !method &&
249
- (methodSupported?.includes('private_key_jwt') ?? false))
250
- ) {
251
- if (!this.keyset) throw new Error('No keyset available')
252
-
253
- try {
254
- const alg =
255
- this.serverMetadata[
256
- `token_endpoint_auth_signing_alg_values_supported`
257
- ] ?? FALLBACK_ALG
258
-
259
- // If jwks is defined, make sure to only sign using a key that exists in
260
- // the jwks. If jwks_uri is defined, we can't be sure that the key we're
261
- // looking for is in there so we will just assume it is.
262
- const kid = this.clientMetadata.jwks?.keys
263
- .map(({ kid }) => kid)
264
- .filter((v): v is string => typeof v === 'string')
265
-
266
- return {
267
- payload: {
268
- client_id: this.clientMetadata.client_id,
269
- client_assertion_type: CLIENT_ASSERTION_TYPE_JWT_BEARER,
270
- client_assertion: await this.keyset.createJwt(
271
- { alg, kid },
272
- {
273
- iss: this.clientMetadata.client_id,
274
- sub: this.clientMetadata.client_id,
275
- aud: this.serverMetadata.issuer,
276
- jti: await this.runtime.generateNonce(),
277
- iat: Math.floor(Date.now() / 1000),
278
- },
279
- ),
280
- },
281
- }
282
- } catch (err) {
283
- if (method === 'private_key_jwt') throw err
284
-
285
- // Else try next method
286
- }
287
- }
288
-
289
- if (
290
- method === 'none' ||
291
- (!method && (methodSupported?.includes('none') ?? true))
292
- ) {
293
- return {
294
- payload: {
295
- client_id: this.clientMetadata.client_id,
296
- },
297
- }
298
- }
299
-
300
- throw new Error(`Unsupported ${endpoint} authentication method`)
301
- }
302
250
  }
303
251
 
304
252
  function wwwFormUrlEncode(payload: Record<string, undefined | unknown>) {
@@ -2,6 +2,10 @@ import { Key, Keyset } from '@atproto/jwk'
2
2
  import { OAuthAuthorizationServerMetadata } from '@atproto/oauth-types'
3
3
  import { Fetch } from '@atproto-labs/fetch'
4
4
  import { GetCachedOptions } from './oauth-authorization-server-metadata-resolver.js'
5
+ import {
6
+ ClientAuthMethod,
7
+ negotiateClientAuthMethod,
8
+ } from './oauth-client-auth.js'
5
9
  import { OAuthResolver } from './oauth-resolver.js'
6
10
  import { DpopNonceCache, OAuthServerAgent } from './oauth-server-agent.js'
7
11
  import { Runtime } from './runtime.js'
@@ -17,19 +21,50 @@ export class OAuthServerFactory {
17
21
  readonly dpopNonceCache: DpopNonceCache,
18
22
  ) {}
19
23
 
20
- async fromIssuer(issuer: string, dpopKey: Key, options?: GetCachedOptions) {
24
+ /**
25
+ * @param authMethod `undefined` means that we are restoring a session that
26
+ * was created before we started storing the `authMethod` in the session. In
27
+ * that case, we will use the first key from the keyset.
28
+ *
29
+ * Support for this might be removed in the future.
30
+ *
31
+ * @throws see {@link OAuthServerFactory.fromMetadata}
32
+ */
33
+ async fromIssuer(
34
+ issuer: string,
35
+ authMethod: 'legacy' | ClientAuthMethod,
36
+ dpopKey: Key,
37
+ options?: GetCachedOptions,
38
+ ) {
21
39
  const serverMetadata = await this.resolver.getAuthorizationServerMetadata(
22
40
  issuer,
23
41
  options,
24
42
  )
25
- return this.fromMetadata(serverMetadata, dpopKey)
43
+
44
+ if (authMethod === 'legacy') {
45
+ // @NOTE Because we were previously not storing the authMethod in the
46
+ // session data, we provide a backwards compatible implementation by
47
+ // computing it here.
48
+ authMethod = negotiateClientAuthMethod(
49
+ serverMetadata,
50
+ this.clientMetadata,
51
+ this.keyset,
52
+ )
53
+ }
54
+
55
+ return this.fromMetadata(serverMetadata, authMethod, dpopKey)
26
56
  }
27
57
 
58
+ /**
59
+ * @throws see {@link OAuthServerAgent}
60
+ */
28
61
  async fromMetadata(
29
62
  serverMetadata: OAuthAuthorizationServerMetadata,
63
+ authMethod: ClientAuthMethod,
30
64
  dpopKey: Key,
31
65
  ) {
32
66
  return new OAuthServerAgent(
67
+ authMethod,
33
68
  dpopKey,
34
69
  serverMetadata,
35
70
  this.clientMetadata,