@atproto/oauth-client 0.7.5 → 0.7.7

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.
@@ -1,180 +0,0 @@
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({ alg, usage: 'sign' })) {
48
- // Return the first key from the key set that matches the server's
49
- // supported algorithms.
50
- if (key.kid) return { method: 'private_key_jwt', kid: key.kid }
51
- }
52
-
53
- throw new Error(
54
- alg.includes(FALLBACK_ALG)
55
- ? `Client authentication method "${method}" requires at least one "${FALLBACK_ALG}" signing key with a "kid" property`
56
- : // AS is not compliant with the ATproto OAuth spec.
57
- `Authorization server requires "${method}" authentication method, but does not support "${FALLBACK_ALG}" algorithm.`,
58
- )
59
- }
60
-
61
- if (method === 'none') {
62
- return { method: 'none' }
63
- }
64
-
65
- throw new Error(
66
- `The ATProto OAuth spec requires that client use either "none" or "private_key_jwt" authentication method.` +
67
- (method === 'client_secret_basic'
68
- ? ' You might want to explicitly set "token_endpoint_auth_method" to one of those values in the client metadata document.'
69
- : ` You set "${method}" which is not allowed.`),
70
- )
71
- }
72
-
73
- export type ClientCredentialsFactory = () => Awaitable<{
74
- headers?: Record<string, string>
75
- payload?: OAuthClientCredentials
76
- }>
77
-
78
- /**
79
- * @throws {AuthMethodUnsatisfiableError} if the authentication method is no
80
- * long usable (either because the AS changed, of because the key is no longer
81
- * available in the keyset).
82
- */
83
- export function createClientCredentialsFactory(
84
- authMethod: ClientAuthMethod,
85
- serverMetadata: OAuthAuthorizationServerMetadata,
86
- clientMetadata: ClientMetadata,
87
- runtime: Runtime,
88
- keyset?: Keyset,
89
- ): ClientCredentialsFactory {
90
- // Ensure the AS still supports the auth method.
91
- if (!supportedMethods(serverMetadata).includes(authMethod.method)) {
92
- throw new AuthMethodUnsatisfiableError(
93
- `Client authentication method "${authMethod.method}" no longer supported`,
94
- )
95
- }
96
-
97
- if (authMethod.method === 'none') {
98
- return () => ({
99
- payload: {
100
- client_id: clientMetadata.client_id,
101
- },
102
- })
103
- }
104
-
105
- if (authMethod.method === 'private_key_jwt') {
106
- try {
107
- // The client used to be a confidential client but no longer has a keyset.
108
- if (!keyset) throw new Error('A keyset is required for private_key_jwt')
109
-
110
- // @NOTE throws if no matching key can be found
111
- const { key, alg } = keyset.findPrivateKey({
112
- usage: 'sign',
113
- kid: authMethod.kid,
114
- alg: supportedAlgs(serverMetadata),
115
- })
116
-
117
- // https://www.rfc-editor.org/rfc/rfc7523.html#section-3
118
- return async () => ({
119
- payload: {
120
- client_id: clientMetadata.client_id,
121
- client_assertion_type: CLIENT_ASSERTION_TYPE_JWT_BEARER,
122
- client_assertion: await key.createJwt(
123
- { alg },
124
- {
125
- // > The JWT MUST contain an "iss" (issuer) claim that contains a
126
- // > unique identifier for the entity that issued the JWT.
127
- iss: clientMetadata.client_id,
128
- // > For client authentication, the subject MUST be the
129
- // > "client_id" of the OAuth client.
130
- sub: clientMetadata.client_id,
131
- // > The JWT MUST contain an "aud" (audience) claim containing a value
132
- // > that identifies the authorization server as an intended audience.
133
- // > The token endpoint URL of the authorization server MAY be used as a
134
- // > value for an "aud" element to identify the authorization server as an
135
- // > intended audience of the JWT.
136
- aud: serverMetadata.issuer,
137
- // > The JWT MAY contain a "jti" (JWT ID) claim that provides a
138
- // > unique identifier for the token.
139
- jti: await runtime.generateNonce(),
140
- // > The JWT MAY contain an "iat" (issued at) claim that
141
- // > identifies the time at which the JWT was issued.
142
- iat: Math.floor(Date.now() / 1000),
143
- // > The JWT MUST contain an "exp" (expiration time) claim that
144
- // > limits the time window during which the JWT can be used.
145
- exp: Math.floor(Date.now() / 1000) + 60, // 1 minute
146
- },
147
- ),
148
- },
149
- })
150
- } catch (cause) {
151
- throw new AuthMethodUnsatisfiableError('Failed to load private key', {
152
- cause,
153
- })
154
- }
155
- }
156
-
157
- throw new AuthMethodUnsatisfiableError(
158
- // @ts-expect-error
159
- `Unsupported auth method ${authMethod.method}`,
160
- )
161
- }
162
-
163
- function supportedMethods(serverMetadata: OAuthAuthorizationServerMetadata) {
164
- return serverMetadata['token_endpoint_auth_methods_supported']
165
- }
166
-
167
- function supportedAlgs(serverMetadata: OAuthAuthorizationServerMetadata) {
168
- return (
169
- serverMetadata['token_endpoint_auth_signing_alg_values_supported'] ?? [
170
- // @NOTE If not specified, assume that the server supports the ES256
171
- // algorithm, as prescribed by the spec:
172
- //
173
- // > Clients and Authorization Servers currently must support the ES256
174
- // > cryptographic system [for client authentication].
175
- //
176
- // https://atproto.com/specs/oauth#confidential-client-authentication
177
- FALLBACK_ALG,
178
- ]
179
- )
180
- }