@atproto/oauth-client 0.3.21 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +25 -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/fetch-dpop.d.ts +1 -2
- package/dist/fetch-dpop.d.ts.map +1 -1
- package/dist/fetch-dpop.js +4 -5
- package/dist/fetch-dpop.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 +4 -4
- package/dist/oauth-client.d.ts.map +1 -1
- package/dist/oauth-client.js +26 -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 -51
- 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/oauth-session.d.ts.map +1 -1
- package/dist/oauth-session.js +0 -1
- package/dist/oauth-session.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 +4 -4
- package/src/errors/auth-method-unsatisfiable-error.ts +1 -0
- package/src/fetch-dpop.ts +2 -6
- package/src/oauth-client-auth.ts +182 -0
- package/src/oauth-client.ts +50 -12
- package/src/oauth-server-agent.ts +19 -72
- package/src/oauth-server-factory.ts +37 -2
- package/src/oauth-session.ts +0 -1
- 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
@@ -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,
|
package/src/oauth-session.ts
CHANGED
@@ -32,7 +32,6 @@ export class OAuthSession {
|
|
32
32
|
) {
|
33
33
|
this.dpopFetch = dpopFetchWrapper<void>({
|
34
34
|
fetch: bindFetch(fetch),
|
35
|
-
iss: server.clientMetadata.client_id,
|
36
35
|
key: server.dpopKey,
|
37
36
|
supportedAlgs: server.serverMetadata.dpop_signing_alg_values_supported,
|
38
37
|
sha256: async (v) => server.runtime.sha256(v),
|
package/src/session-getter.ts
CHANGED
@@ -5,9 +5,11 @@ import {
|
|
5
5
|
GetCachedOptions,
|
6
6
|
SimpleStore,
|
7
7
|
} from '@atproto-labs/simple-store'
|
8
|
+
import { AuthMethodUnsatisfiableError } from './errors/auth-method-unsatisfiable-error.js'
|
8
9
|
import { TokenInvalidError } from './errors/token-invalid-error.js'
|
9
10
|
import { TokenRefreshError } from './errors/token-refresh-error.js'
|
10
11
|
import { TokenRevokedError } from './errors/token-revoked-error.js'
|
12
|
+
import { ClientAuthMethod } from './oauth-client-auth.js'
|
11
13
|
import { OAuthResponseError } from './oauth-response-error.js'
|
12
14
|
import { TokenSet } from './oauth-server-agent.js'
|
13
15
|
import { OAuthServerFactory } from './oauth-server-factory.js'
|
@@ -16,6 +18,10 @@ import { CustomEventTarget, combineSignals, timeoutSignal } from './util.js'
|
|
16
18
|
|
17
19
|
export type Session = {
|
18
20
|
dpopKey: Key
|
21
|
+
/**
|
22
|
+
* Previous implementation of this lib did not define an `authMethod`
|
23
|
+
*/
|
24
|
+
authMethod?: ClientAuthMethod
|
19
25
|
tokenSet: TokenSet
|
20
26
|
}
|
21
27
|
|
@@ -51,7 +57,7 @@ export class SessionGetter extends CachedGetter<AtprotoDid, Session> {
|
|
51
57
|
private readonly runtime: Runtime,
|
52
58
|
) {
|
53
59
|
super(
|
54
|
-
async (sub, options, storedSession)
|
60
|
+
async (sub, options, storedSession) => {
|
55
61
|
// There needs to be a previous session to be able to refresh. If
|
56
62
|
// storedSession is undefined, it means that the store does not contain
|
57
63
|
// a session for the given sub.
|
@@ -73,7 +79,7 @@ export class SessionGetter extends CachedGetter<AtprotoDid, Session> {
|
|
73
79
|
// concurrent access (which, normally, should not happen if a proper
|
74
80
|
// runtime lock was provided).
|
75
81
|
|
76
|
-
const { dpopKey, tokenSet } = storedSession
|
82
|
+
const { dpopKey, authMethod = 'legacy', tokenSet } = storedSession
|
77
83
|
|
78
84
|
if (sub !== tokenSet.sub) {
|
79
85
|
// Fool-proofing (e.g. against invalid session storage)
|
@@ -94,7 +100,11 @@ export class SessionGetter extends CachedGetter<AtprotoDid, Session> {
|
|
94
100
|
// always possible. If no lock implementation is provided, we will use
|
95
101
|
// the store to check if a concurrent refresh occurred.
|
96
102
|
|
97
|
-
const server = await serverFactory.fromIssuer(
|
103
|
+
const server = await serverFactory.fromIssuer(
|
104
|
+
tokenSet.iss,
|
105
|
+
authMethod,
|
106
|
+
dpopKey,
|
107
|
+
)
|
98
108
|
|
99
109
|
// Because refresh tokens can only be used once, we must not use the
|
100
110
|
// "signal" to abort the refresh, or throw any abort error beyond this
|
@@ -111,7 +121,11 @@ export class SessionGetter extends CachedGetter<AtprotoDid, Session> {
|
|
111
121
|
throw new TokenRefreshError(sub, 'Token set sub mismatch')
|
112
122
|
}
|
113
123
|
|
114
|
-
return {
|
124
|
+
return {
|
125
|
+
dpopKey,
|
126
|
+
tokenSet: newTokenSet,
|
127
|
+
authMethod: server.authMethod,
|
128
|
+
}
|
115
129
|
} catch (cause) {
|
116
130
|
// If the refresh token is invalid, let's try to recover from
|
117
131
|
// concurrency issues, or make sure the session is deleted by throwing
|
@@ -173,17 +187,36 @@ export class SessionGetter extends CachedGetter<AtprotoDid, Session> {
|
|
173
187
|
30e3 * Math.random()
|
174
188
|
)
|
175
189
|
},
|
176
|
-
onStoreError: async (
|
177
|
-
|
178
|
-
|
179
|
-
|
190
|
+
onStoreError: async (
|
191
|
+
err,
|
192
|
+
sub,
|
193
|
+
{ tokenSet, dpopKey, authMethod = 'legacy' as const },
|
194
|
+
) => {
|
195
|
+
if (!(err instanceof AuthMethodUnsatisfiableError)) {
|
196
|
+
// If the error was an AuthMethodUnsatisfiableError, there is no
|
197
|
+
// point in trying to call `fromIssuer`.
|
198
|
+
try {
|
199
|
+
// If the token data cannot be stored, let's revoke it
|
200
|
+
const server = await serverFactory.fromIssuer(
|
201
|
+
tokenSet.iss,
|
202
|
+
authMethod,
|
203
|
+
dpopKey,
|
204
|
+
)
|
205
|
+
await server.revoke(
|
206
|
+
tokenSet.refresh_token ?? tokenSet.access_token,
|
207
|
+
)
|
208
|
+
} catch {
|
209
|
+
// Let the original error propagate
|
210
|
+
}
|
211
|
+
}
|
212
|
+
|
180
213
|
throw err
|
181
214
|
},
|
182
215
|
deleteOnError: async (err) =>
|
183
|
-
// Optimization: More likely to happen first
|
184
216
|
err instanceof TokenRefreshError ||
|
185
217
|
err instanceof TokenRevokedError ||
|
186
|
-
err instanceof TokenInvalidError
|
218
|
+
err instanceof TokenInvalidError ||
|
219
|
+
err instanceof AuthMethodUnsatisfiableError,
|
187
220
|
},
|
188
221
|
)
|
189
222
|
}
|
package/src/state-store.ts
CHANGED
@@ -1,9 +1,12 @@
|
|
1
1
|
import { Key } from '@atproto/jwk'
|
2
2
|
import { SimpleStore } from '@atproto-labs/simple-store'
|
3
|
+
import { ClientAuthMethod } from './oauth-client-auth.js'
|
3
4
|
|
4
5
|
export type InternalStateData = {
|
5
6
|
iss: string
|
6
7
|
dpopKey: Key
|
8
|
+
/** @note optional for legacy reasons */
|
9
|
+
authMethod?: ClientAuthMethod
|
7
10
|
verifier?: string
|
8
11
|
appState?: string
|
9
12
|
}
|
@@ -4,28 +4,13 @@ import {
|
|
4
4
|
assertOAuthDiscoverableClientId,
|
5
5
|
assertOAuthLoopbackClientId,
|
6
6
|
} from '@atproto/oauth-types'
|
7
|
+
import { FALLBACK_ALG } from './constants.js'
|
7
8
|
import { ClientMetadata, clientMetadataSchema } from './types.js'
|
8
9
|
|
9
|
-
const TOKEN_ENDPOINT_AUTH_METHOD = `token_endpoint_auth_method`
|
10
|
-
const TOKEN_ENDPOINT_AUTH_SIGNING_ALG = `token_endpoint_auth_signing_alg`
|
11
|
-
|
12
10
|
export function validateClientMetadata(
|
13
11
|
input: OAuthClientMetadataInput,
|
14
12
|
keyset?: Keyset,
|
15
13
|
): ClientMetadata {
|
16
|
-
if (input.jwks) {
|
17
|
-
if (!keyset) {
|
18
|
-
throw new TypeError(`Keyset must not be provided when jwks is provided`)
|
19
|
-
}
|
20
|
-
for (const key of input.jwks.keys) {
|
21
|
-
if (!key.kid) {
|
22
|
-
throw new TypeError(`Key must have a "kid" property`)
|
23
|
-
} else if (!keyset.has(key.kid)) {
|
24
|
-
throw new TypeError(`Key with kid "${key.kid}" not found in keyset`)
|
25
|
-
}
|
26
|
-
}
|
27
|
-
}
|
28
|
-
|
29
14
|
// Allow to pass a keyset and omit the jwks/jwks_uri properties
|
30
15
|
if (!input.jwks && !input.jwks_uri && keyset?.size) {
|
31
16
|
input = { ...input, jwks: keyset.toJSON() }
|
@@ -53,32 +38,60 @@ export function validateClientMetadata(
|
|
53
38
|
throw new TypeError(`"grant_types" must include "authorization_code"`)
|
54
39
|
}
|
55
40
|
|
56
|
-
const method = metadata
|
41
|
+
const method = metadata.token_endpoint_auth_method
|
42
|
+
const methodAlg = metadata.token_endpoint_auth_signing_alg
|
57
43
|
switch (method) {
|
58
|
-
case undefined:
|
59
|
-
throw new TypeError(`${TOKEN_ENDPOINT_AUTH_METHOD} must be provided`)
|
60
44
|
case 'none':
|
61
|
-
if (
|
45
|
+
if (methodAlg) {
|
62
46
|
throw new TypeError(
|
63
|
-
|
47
|
+
`"token_endpoint_auth_signing_alg" must not be provided when "token_endpoint_auth_method" is "${method}"`,
|
64
48
|
)
|
65
49
|
}
|
66
50
|
break
|
67
|
-
|
68
|
-
|
51
|
+
|
52
|
+
case 'private_key_jwt': {
|
53
|
+
if (!methodAlg) {
|
54
|
+
throw new TypeError(
|
55
|
+
`"token_endpoint_auth_signing_alg" must be provided when "token_endpoint_auth_method" is "${method}"`,
|
56
|
+
)
|
57
|
+
}
|
58
|
+
|
59
|
+
const signingKeys = keyset
|
60
|
+
? Array.from(keyset.list({ use: 'sig' })).filter(
|
61
|
+
(key) => key.isPrivate && key.kid,
|
62
|
+
)
|
63
|
+
: null
|
64
|
+
|
65
|
+
if (!signingKeys?.some((key) => key.algorithms.includes(FALLBACK_ALG))) {
|
69
66
|
throw new TypeError(
|
70
|
-
`
|
67
|
+
`Client authentication method "${method}" requires at least one "${FALLBACK_ALG}" signing key with a "kid" property`,
|
71
68
|
)
|
72
69
|
}
|
73
|
-
|
70
|
+
|
71
|
+
if (metadata.jwks) {
|
72
|
+
// Ensure that all the signing keys that could end-up being used are
|
73
|
+
// advertised in the JWKS.
|
74
|
+
for (const key of signingKeys) {
|
75
|
+
if (!metadata.jwks.keys.some((k) => k.kid === key.kid)) {
|
76
|
+
throw new TypeError(`Key with kid "${key.kid}" not found in jwks`)
|
77
|
+
}
|
78
|
+
}
|
79
|
+
} else if (metadata.jwks_uri) {
|
80
|
+
// @NOTE we only ensure that all the signing keys are referenced in JWKS
|
81
|
+
// when it is available (see previous "if") as we don't want to download
|
82
|
+
// that file here (for efficiency reasons).
|
83
|
+
} else {
|
74
84
|
throw new TypeError(
|
75
|
-
|
85
|
+
`Client authentication method "${method}" requires a JWKS`,
|
76
86
|
)
|
77
87
|
}
|
88
|
+
|
78
89
|
break
|
90
|
+
}
|
91
|
+
|
79
92
|
default:
|
80
93
|
throw new TypeError(
|
81
|
-
`
|
94
|
+
`Unsupported "token_endpoint_auth_method" value: ${method}`,
|
82
95
|
)
|
83
96
|
}
|
84
97
|
|
@@ -1 +1 @@
|
|
1
|
-
{"root":["./src/atproto-token-response.ts","./src/constants.ts","./src/fetch-dpop.ts","./src/index.ts","./src/lock.ts","./src/oauth-authorization-server-metadata-resolver.ts","./src/oauth-callback-error.ts","./src/oauth-client.ts","./src/oauth-protected-resource-metadata-resolver.ts","./src/oauth-resolver-error.ts","./src/oauth-resolver.ts","./src/oauth-response-error.ts","./src/oauth-server-agent.ts","./src/oauth-server-factory.ts","./src/oauth-session.ts","./src/runtime-implementation.ts","./src/runtime.ts","./src/session-getter.ts","./src/state-store.ts","./src/types.ts","./src/util.ts","./src/validate-client-metadata.ts","./src/errors/token-invalid-error.ts","./src/errors/token-refresh-error.ts","./src/errors/token-revoked-error.ts"],"version":"5.8.2"}
|
1
|
+
{"root":["./src/atproto-token-response.ts","./src/constants.ts","./src/fetch-dpop.ts","./src/index.ts","./src/lock.ts","./src/oauth-authorization-server-metadata-resolver.ts","./src/oauth-callback-error.ts","./src/oauth-client-auth.ts","./src/oauth-client.ts","./src/oauth-protected-resource-metadata-resolver.ts","./src/oauth-resolver-error.ts","./src/oauth-resolver.ts","./src/oauth-response-error.ts","./src/oauth-server-agent.ts","./src/oauth-server-factory.ts","./src/oauth-session.ts","./src/runtime-implementation.ts","./src/runtime.ts","./src/session-getter.ts","./src/state-store.ts","./src/types.ts","./src/util.ts","./src/validate-client-metadata.ts","./src/errors/auth-method-unsatisfiable-error.ts","./src/errors/token-invalid-error.ts","./src/errors/token-refresh-error.ts","./src/errors/token-revoked-error.ts"],"version":"5.8.2"}
|