@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.
- package/CHANGELOG.md +29 -0
- package/dist/errors/auth-method-unsatisfiable-error.d.ts +3 -0
- package/dist/errors/auth-method-unsatisfiable-error.d.ts.map +1 -0
- package/dist/errors/auth-method-unsatisfiable-error.js +7 -0
- package/dist/errors/auth-method-unsatisfiable-error.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/oauth-client-auth.d.ts +23 -0
- package/dist/oauth-client-auth.d.ts.map +1 -0
- package/dist/oauth-client-auth.js +131 -0
- package/dist/oauth-client-auth.js.map +1 -0
- package/dist/oauth-client.d.ts +13 -8
- package/dist/oauth-client.d.ts.map +1 -1
- package/dist/oauth-client.js +39 -13
- package/dist/oauth-client.js.map +1 -1
- package/dist/oauth-resolver.d.ts +1 -1
- package/dist/oauth-server-agent.d.ts +8 -6
- package/dist/oauth-server-agent.d.ts.map +1 -1
- package/dist/oauth-server-agent.js +19 -50
- package/dist/oauth-server-agent.js.map +1 -1
- package/dist/oauth-server-factory.d.ts +15 -2
- package/dist/oauth-server-factory.d.ts.map +1 -1
- package/dist/oauth-server-factory.js +23 -4
- package/dist/oauth-server-factory.js.map +1 -1
- package/dist/session-getter.d.ts +5 -0
- package/dist/session-getter.d.ts.map +1 -1
- package/dist/session-getter.js +24 -11
- package/dist/session-getter.js.map +1 -1
- package/dist/state-store.d.ts +3 -0
- package/dist/state-store.d.ts.map +1 -1
- package/dist/types.d.ts +8 -8
- package/dist/types.d.ts.map +1 -1
- package/dist/validate-client-metadata.d.ts.map +1 -1
- package/dist/validate-client-metadata.js +32 -26
- package/dist/validate-client-metadata.js.map +1 -1
- package/package.json +7 -7
- package/src/errors/auth-method-unsatisfiable-error.ts +1 -0
- package/src/index.ts +2 -0
- package/src/oauth-client-auth.ts +182 -0
- package/src/oauth-client.ts +89 -33
- package/src/oauth-server-agent.ts +19 -71
- package/src/oauth-server-factory.ts +37 -2
- package/src/session-getter.ts +43 -10
- package/src/state-store.ts +3 -0
- package/src/validate-client-metadata.ts +40 -27
- 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":";;
|
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
|
+
"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.
|
33
|
-
"@atproto-labs/identity-resolver": "0.1.
|
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.
|
38
|
-
"@atproto/oauth-types": "0.
|
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
|
+
}
|
package/src/oauth-client.ts
CHANGED
@@ -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
|
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
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
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(
|
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 {
|
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
|
-
|
495
|
-
|
496
|
-
|
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
|
-
|
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 {
|
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(
|
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.
|
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
|
-
|
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
|
-
|
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,
|