@atproto/oauth-provider 0.1.2 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (136) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/dist/account/account.d.ts +6 -2
  3. package/dist/account/account.d.ts.map +1 -1
  4. package/dist/assets/app/bundle-manifest.json +3 -3
  5. package/dist/assets/app/main.css +1 -1
  6. package/dist/assets/app/main.js +3 -3
  7. package/dist/assets/app/main.js.map +1 -1
  8. package/dist/assets/assets-middleware.d.ts +2 -1
  9. package/dist/assets/assets-middleware.d.ts.map +1 -1
  10. package/dist/assets/assets-middleware.js +7 -0
  11. package/dist/assets/assets-middleware.js.map +1 -1
  12. package/dist/client/client-manager.d.ts +4 -3
  13. package/dist/client/client-manager.d.ts.map +1 -1
  14. package/dist/client/client-manager.js +91 -77
  15. package/dist/client/client-manager.js.map +1 -1
  16. package/dist/client/client.d.ts +2 -3
  17. package/dist/client/client.d.ts.map +1 -1
  18. package/dist/client/client.js +6 -12
  19. package/dist/client/client.js.map +1 -1
  20. package/dist/constants.d.ts +2 -0
  21. package/dist/constants.d.ts.map +1 -1
  22. package/dist/constants.js +3 -1
  23. package/dist/constants.js.map +1 -1
  24. package/dist/device/device-manager.d.ts +1 -1
  25. package/dist/device/device-manager.d.ts.map +1 -1
  26. package/dist/device/device-manager.js +2 -2
  27. package/dist/device/device-manager.js.map +1 -1
  28. package/dist/dpop/dpop-manager.d.ts +0 -1
  29. package/dist/dpop/dpop-manager.d.ts.map +1 -1
  30. package/dist/dpop/dpop-manager.js +1 -4
  31. package/dist/dpop/dpop-manager.js.map +1 -1
  32. package/dist/errors/invalid-authorization-details-error.d.ts +4 -3
  33. package/dist/errors/invalid-authorization-details-error.d.ts.map +1 -1
  34. package/dist/errors/invalid-authorization-details-error.js +4 -4
  35. package/dist/errors/invalid-authorization-details-error.js.map +1 -1
  36. package/dist/lib/http/parser.d.ts +13 -7
  37. package/dist/lib/http/parser.d.ts.map +1 -1
  38. package/dist/lib/http/parser.js +29 -9
  39. package/dist/lib/http/parser.js.map +1 -1
  40. package/dist/lib/http/request.d.ts +8 -5
  41. package/dist/lib/http/request.d.ts.map +1 -1
  42. package/dist/lib/http/request.js +24 -12
  43. package/dist/lib/http/request.js.map +1 -1
  44. package/dist/lib/http/stream.d.ts.map +1 -1
  45. package/dist/lib/http/stream.js +3 -2
  46. package/dist/lib/http/stream.js.map +1 -1
  47. package/dist/metadata/build-metadata.d.ts +0 -1
  48. package/dist/metadata/build-metadata.d.ts.map +1 -1
  49. package/dist/metadata/build-metadata.js +9 -49
  50. package/dist/metadata/build-metadata.js.map +1 -1
  51. package/dist/oauth-hooks.d.ts +3 -10
  52. package/dist/oauth-hooks.d.ts.map +1 -1
  53. package/dist/oauth-provider.d.ts +10 -15
  54. package/dist/oauth-provider.d.ts.map +1 -1
  55. package/dist/oauth-provider.js +176 -114
  56. package/dist/oauth-provider.js.map +1 -1
  57. package/dist/oauth-verifier.d.ts +1 -2
  58. package/dist/oauth-verifier.d.ts.map +1 -1
  59. package/dist/oauth-verifier.js.map +1 -1
  60. package/dist/output/build-authorize-data.d.ts +6 -0
  61. package/dist/output/build-authorize-data.d.ts.map +1 -1
  62. package/dist/output/build-authorize-data.js +1 -0
  63. package/dist/output/build-authorize-data.js.map +1 -1
  64. package/dist/replay/replay-manager.d.ts +1 -0
  65. package/dist/replay/replay-manager.d.ts.map +1 -1
  66. package/dist/replay/replay-manager.js +3 -0
  67. package/dist/replay/replay-manager.js.map +1 -1
  68. package/dist/replay/replay-store.d.ts +1 -1
  69. package/dist/request/request-info.d.ts +2 -0
  70. package/dist/request/request-info.d.ts.map +1 -1
  71. package/dist/request/request-manager.d.ts +3 -9
  72. package/dist/request/request-manager.d.ts.map +1 -1
  73. package/dist/request/request-manager.js +52 -77
  74. package/dist/request/request-manager.js.map +1 -1
  75. package/dist/request/types.d.ts +10 -10
  76. package/dist/signer/signed-token-payload.d.ts +88 -88
  77. package/dist/signer/signer.d.ts +24 -31
  78. package/dist/signer/signer.d.ts.map +1 -1
  79. package/dist/signer/signer.js +0 -40
  80. package/dist/signer/signer.js.map +1 -1
  81. package/dist/token/token-claims.d.ts +84 -84
  82. package/dist/token/token-manager.d.ts +1 -2
  83. package/dist/token/token-manager.d.ts.map +1 -1
  84. package/dist/token/token-manager.js +10 -37
  85. package/dist/token/token-manager.js.map +1 -1
  86. package/dist/token/types.d.ts +10 -10
  87. package/package.json +3 -3
  88. package/src/account/account.ts +11 -7
  89. package/src/assets/app/backend-data.ts +9 -2
  90. package/src/assets/app/components/accept-form.tsx +65 -51
  91. package/src/assets/app/components/client-name.tsx +24 -16
  92. package/src/assets/app/components/url-viewer.tsx +3 -3
  93. package/src/assets/app/views/accept-view.tsx +7 -4
  94. package/src/assets/app/views/authorize-view.tsx +2 -1
  95. package/src/assets/assets-middleware.ts +14 -2
  96. package/src/client/client-manager.ts +124 -120
  97. package/src/client/client.ts +5 -17
  98. package/src/constants.ts +3 -0
  99. package/src/device/device-manager.ts +7 -1
  100. package/src/dpop/dpop-manager.ts +1 -6
  101. package/src/errors/invalid-authorization-details-error.ts +9 -4
  102. package/src/lib/http/parser.ts +37 -13
  103. package/src/lib/http/request.ts +61 -15
  104. package/src/lib/http/stream.ts +5 -2
  105. package/src/metadata/build-metadata.ts +9 -56
  106. package/src/oauth-hooks.ts +3 -13
  107. package/src/oauth-provider.ts +187 -177
  108. package/src/oauth-verifier.ts +1 -2
  109. package/src/output/build-authorize-data.ts +8 -0
  110. package/src/replay/replay-manager.ts +9 -0
  111. package/src/replay/replay-store.ts +1 -1
  112. package/src/request/request-info.ts +2 -0
  113. package/src/request/request-manager.ts +81 -107
  114. package/src/signer/signer.ts +0 -63
  115. package/src/token/token-manager.ts +8 -41
  116. package/dist/oidc/claims.d.ts +0 -16
  117. package/dist/oidc/claims.d.ts.map +0 -1
  118. package/dist/oidc/claims.js +0 -29
  119. package/dist/oidc/claims.js.map +0 -1
  120. package/dist/oidc/userinfo.d.ts +0 -7
  121. package/dist/oidc/userinfo.d.ts.map +0 -1
  122. package/dist/oidc/userinfo.js +0 -3
  123. package/dist/oidc/userinfo.js.map +0 -1
  124. package/dist/parameters/claims-requested.d.ts +0 -3
  125. package/dist/parameters/claims-requested.d.ts.map +0 -1
  126. package/dist/parameters/claims-requested.js +0 -77
  127. package/dist/parameters/claims-requested.js.map +0 -1
  128. package/dist/parameters/oidc-payload.d.ts +0 -31
  129. package/dist/parameters/oidc-payload.d.ts.map +0 -1
  130. package/dist/parameters/oidc-payload.js +0 -25
  131. package/dist/parameters/oidc-payload.js.map +0 -1
  132. package/src/assets/app/components/client-identifier.tsx +0 -31
  133. package/src/oidc/claims.ts +0 -35
  134. package/src/oidc/userinfo.ts +0 -11
  135. package/src/parameters/claims-requested.ts +0 -106
  136. package/src/parameters/oidc-payload.ts +0 -28
@@ -8,12 +8,18 @@ import { Account } from '../account/account.js'
8
8
  import { Client } from '../client/client.js'
9
9
  import { RequestUri } from '../request/request-uri.js'
10
10
 
11
+ export type ScopeDetail = {
12
+ scope: string
13
+ description?: string
14
+ }
15
+
11
16
  export type AuthorizationResultAuthorize = {
12
17
  issuer: string
13
18
  client: Client
14
19
  parameters: OAuthAuthenticationRequestParameters
15
20
  authorize: {
16
21
  uri: RequestUri
22
+ scopeDetails?: ScopeDetail[]
17
23
  sessions: readonly {
18
24
  account: Account
19
25
  info: DeviceAccountInfo
@@ -44,6 +50,7 @@ export type AuthorizeData = {
44
50
  requestUri: string
45
51
  csrfCookie: string
46
52
  loginHint?: string
53
+ scopeDetails?: ScopeDetail[]
47
54
  newSessionsRequireConsent: boolean
48
55
  sessions: Session[]
49
56
  }
@@ -59,6 +66,7 @@ export function buildAuthorizeData(
59
66
  csrfCookie: `csrf-${data.authorize.uri}`,
60
67
  loginHint: data.parameters.login_hint,
61
68
  newSessionsRequireConsent: data.parameters.prompt === 'consent',
69
+ scopeDetails: data.authorize.scopeDetails,
62
70
  sessions: data.authorize.sessions.map(
63
71
  (session): Session => ({
64
72
  account: session.account,
@@ -2,6 +2,7 @@ import { ClientId } from '../client/client-id.js'
2
2
  import {
3
3
  CLIENT_ASSERTION_MAX_AGE,
4
4
  DPOP_NONCE_MAX_AGE,
5
+ CODE_CHALLENGE_REPLAY_TIMEFRAME,
5
6
  JAR_MAX_AGE,
6
7
  } from '../constants.js'
7
8
  import { ReplayStore } from './replay-store.js'
@@ -35,4 +36,12 @@ export class ReplayManager {
35
36
  asTimeFrame(DPOP_NONCE_MAX_AGE),
36
37
  )
37
38
  }
39
+
40
+ async uniqueCodeChallenge(challenge: string): Promise<boolean> {
41
+ return this.replayStore.unique(
42
+ 'CodeChallenge',
43
+ challenge,
44
+ asTimeFrame(CODE_CHALLENGE_REPLAY_TIMEFRAME),
45
+ )
46
+ }
38
47
  }
@@ -9,7 +9,7 @@ export interface ReplayStore {
9
9
  * strictly necessary for security purposes, the namespace should be used to
10
10
  * mitigate denial of service attacks from one client to the other.
11
11
  *
12
- * @param timeFrame expressed in milliseconds. Will never exceed 24 hours.
12
+ * @param timeFrame expressed in milliseconds.
13
13
  */
14
14
  unique(
15
15
  namespace: string,
@@ -1,4 +1,5 @@
1
1
  import { OAuthAuthenticationRequestParameters } from '@atproto/oauth-types'
2
+ import { ClientId } from '../client/client-id.js'
2
3
  import { ClientAuth } from '../client/client-auth.js'
3
4
  import { RequestId } from './request-id.js'
4
5
  import { RequestUri } from './request-uri.js'
@@ -8,5 +9,6 @@ export type RequestInfo = {
8
9
  uri: RequestUri
9
10
  parameters: Readonly<OAuthAuthenticationRequestParameters>
10
11
  expiresAt: Date
12
+ clientId: ClientId
11
13
  clientAuth: ClientAuth
12
14
  }
@@ -4,7 +4,6 @@ import {
4
4
  OAuthAuthorizationServerMetadata,
5
5
  } from '@atproto/oauth-types'
6
6
 
7
- import { DeviceAccountInfo } from '../account/account-store.js'
8
7
  import { Account } from '../account/account.js'
9
8
  import { ClientAuth } from '../client/client-auth.js'
10
9
  import { ClientId } from '../client/client-id.js'
@@ -17,12 +16,12 @@ import {
17
16
  import { DeviceId } from '../device/device-id.js'
18
17
  import { AccessDeniedError } from '../errors/access-denied-error.js'
19
18
  import { ConsentRequiredError } from '../errors/consent-required-error.js'
19
+ import { InvalidAuthorizationDetailsError } from '../errors/invalid-authorization-details-error.js'
20
20
  import { InvalidGrantError } from '../errors/invalid-grant-error.js'
21
21
  import { InvalidParametersError } from '../errors/invalid-parameters-error.js'
22
22
  import { InvalidRequestError } from '../errors/invalid-request-error.js'
23
23
  import { compareRedirectUri } from '../lib/util/redirect-uri.js'
24
24
  import { OAuthHooks } from '../oauth-hooks.js'
25
- import { OIDC_SCOPE_CLAIMS } from '../oidc/claims.js'
26
25
  import { Signer } from '../signer/signer.js'
27
26
  import { Code, generateCode } from './code.js'
28
27
  import {
@@ -44,7 +43,6 @@ export class RequestManager {
44
43
  protected readonly signer: Signer,
45
44
  protected readonly metadata: OAuthAuthorizationServerMetadata,
46
45
  protected readonly hooks: OAuthHooks,
47
- protected readonly pkceRequired = true,
48
46
  protected readonly tokenMaxAge = TOKEN_MAX_AGE,
49
47
  ) {}
50
48
 
@@ -83,7 +81,7 @@ export class RequestManager {
83
81
  })
84
82
 
85
83
  const uri = encodeRequestUri(id)
86
- return { id, uri, expiresAt, parameters, clientAuth }
84
+ return { id, uri, expiresAt, parameters, clientId: client.id, clientAuth }
87
85
  }
88
86
 
89
87
  async validate(
@@ -91,8 +89,28 @@ export class RequestManager {
91
89
  clientAuth: ClientAuth,
92
90
  parameters: Readonly<OAuthAuthenticationRequestParameters>,
93
91
  dpopJkt: null | string,
94
- pkceRequired = this.pkceRequired,
95
92
  ): Promise<Readonly<OAuthAuthenticationRequestParameters>> {
93
+ for (const k of [
94
+ // Known unsupported OIDC parameters
95
+ 'claims',
96
+ 'id_token_hint',
97
+ 'nonce', // note that OIDC "nonce" is redundant with PKCE
98
+ ] as const) {
99
+ if (parameters[k]) {
100
+ throw new InvalidParametersError(
101
+ parameters,
102
+ `Unsupported "${k}" parameter`,
103
+ )
104
+ }
105
+ }
106
+
107
+ if (parameters.response_type !== 'code') {
108
+ throw new InvalidParametersError(
109
+ parameters,
110
+ 'Only "code" response type is allowed',
111
+ )
112
+ }
113
+
96
114
  // https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-10#section-1.4.1
97
115
  // > The authorization server MAY fully or partially ignore the scope
98
116
  // > requested by the client, based on the authorization server policy or
@@ -101,59 +119,75 @@ export class RequestManager {
101
119
  // > server MUST include the scope response parameter in the token response
102
120
  // > (Section 3.2.3) to inform the client of the actual scope granted.
103
121
 
104
- const cScopes = client.metadata.scope?.split(' ')
122
+ const cScopes = client.metadata.scope?.split(' ').filter(Boolean)
105
123
  const sScopes = this.metadata.scopes_supported
106
124
 
107
- const scopes =
108
- (parameters.scope || client.metadata.scope)
109
- ?.split(' ')
110
- .filter((scope) => !!scope && (sScopes?.includes(scope) ?? true)) ?? []
125
+ const scopes = new Set(
126
+ parameters.scope?.split(' ').filter(Boolean) || cScopes,
127
+ )
128
+
129
+ if (scopes.has('openid')) {
130
+ throw new InvalidParametersError(
131
+ parameters,
132
+ 'OpenID Connect is not supported',
133
+ )
134
+ }
135
+
136
+ if (!scopes.has('atproto')) {
137
+ throw new InvalidParametersError(
138
+ parameters,
139
+ 'The "atproto" scope is required',
140
+ )
141
+ }
111
142
 
112
143
  for (const scope of scopes) {
113
- if (!cScopes?.includes(scope)) {
144
+ // Loopback clients do not define any scope in their metadata
145
+ if (cScopes && !cScopes.includes(scope)) {
114
146
  throw new InvalidParametersError(
115
147
  parameters,
116
148
  `Scope "${scope}" is not registered for this client`,
117
149
  )
118
150
  }
119
- }
120
151
 
121
- for (const [scope, claims] of Object.entries(OIDC_SCOPE_CLAIMS)) {
122
- for (const claim of claims) {
123
- if (
124
- parameters?.claims?.id_token?.[claim]?.essential === true ||
125
- parameters?.claims?.userinfo?.[claim]?.essential === true
126
- ) {
127
- if (!scopes?.includes(scope)) {
128
- throw new InvalidParametersError(
129
- parameters,
130
- `Essential ${claim} claim requires "${scope}" scope`,
131
- )
132
- }
133
- }
152
+ // Currently, the implementation requires all the scopes to be statically
153
+ // defined in the server metadata. In the future, we might add support
154
+ // for dynamic scopes.
155
+ if (!sScopes?.includes(scope)) {
156
+ throw new InvalidParametersError(
157
+ parameters,
158
+ `Scope "${scope}" is not supported by this server`,
159
+ )
134
160
  }
135
161
  }
136
162
 
137
- parameters = { ...parameters, scope: scopes.join(' ') }
138
-
139
- const responseTypes = parameters.response_type.split(' ')
163
+ parameters = { ...parameters, scope: [...scopes].join(' ') || undefined }
140
164
 
141
165
  if (parameters.authorization_details) {
142
166
  const clientAuthDetailsTypes = client.metadata.authorization_details_types
143
167
  if (!clientAuthDetailsTypes) {
144
- throw new InvalidParametersError(
168
+ throw new InvalidAuthorizationDetailsError(
145
169
  parameters,
146
170
  'Client Metadata does not declare any "authorization_details"',
147
171
  )
148
172
  }
149
173
 
150
174
  for (const detail of parameters.authorization_details) {
151
- if (!clientAuthDetailsTypes?.includes(detail.type)) {
152
- throw new InvalidParametersError(
175
+ if (
176
+ !this.metadata.authorization_details_types_supported?.includes(
177
+ detail.type,
178
+ )
179
+ ) {
180
+ throw new InvalidAuthorizationDetailsError(
153
181
  parameters,
154
182
  `Unsupported "authorization_details" type "${detail.type}"`,
155
183
  )
156
184
  }
185
+ if (!clientAuthDetailsTypes?.includes(detail.type)) {
186
+ throw new InvalidAuthorizationDetailsError(
187
+ parameters,
188
+ `Client Metadata does not declare any "authorization_details" of type "${detail.type}"`,
189
+ )
190
+ }
157
191
  }
158
192
  }
159
193
 
@@ -197,16 +231,9 @@ export class RequestManager {
197
231
  )
198
232
  }
199
233
 
200
- if (pkceRequired && responseTypes.includes('token')) {
201
- throw new InvalidParametersError(
202
- parameters,
203
- `Response type "${parameters.response_type}" is incompatible with PKCE`,
204
- 'unsupported_response_type',
205
- )
206
- }
207
-
208
234
  // https://datatracker.ietf.org/doc/html/rfc7636#section-4.4.1
209
- if (pkceRequired && !parameters.code_challenge) {
235
+ // PKCE is mandatory
236
+ if (!parameters.code_challenge) {
210
237
  throw new InvalidParametersError(parameters, 'code_challenge is required')
211
238
  }
212
239
 
@@ -229,50 +256,15 @@ export class RequestManager {
229
256
  )
230
257
  }
231
258
 
232
- // https://openid.net/specs/openid-connect-core-1_0.html#HybridAuthRequest
233
- //
234
- // > nonce: REQUIRED if the Response Type of the request is "code id_token" or
235
- // > "code id_token token" and OPTIONAL when the Response Type of the
236
- // > request is "code token". It is a string value used to associate a
237
- // > Client session with an ID Token, and to mitigate replay attacks. The
238
- // > value is passed through unmodified from the Authentication Request to
239
- // > the ID Token. Sufficient entropy MUST be present in the nonce values
240
- // > used to prevent attackers from guessing values. For implementation
241
- // > notes, see Section 15.5.2.
242
- if (responseTypes.includes('id_token') && !parameters.nonce) {
243
- throw new InvalidParametersError(
244
- parameters,
245
- 'nonce is required for implicit and hybrid flows',
246
- )
247
- }
248
-
249
- // Make "expensive" checks after the "cheaper" checks
250
-
251
- if (parameters.id_token_hint != null) {
252
- const { payload } = await this.signer.verify(parameters.id_token_hint, {
253
- // these are meant to be outdated when used as a hint
254
- clockTolerance: Infinity,
255
- })
256
-
257
- if (!payload.sub) {
258
- throw new InvalidParametersError(
259
- parameters,
260
- `Unexpected empty id_token_hint "sub"`,
261
- )
262
- } else if (parameters.login_hint == null) {
263
- parameters = { ...parameters, login_hint: payload.sub }
264
- } else if (parameters.login_hint !== payload.sub) {
265
- throw new InvalidParametersError(
266
- parameters,
267
- 'login_hint does not match "sub" of id_token_hint',
268
- )
269
- }
270
- }
271
-
272
- // ATPROTO extension: if the client is not trusted, force users to consent
273
- // to authorization requests. We do this to avoid unauthenticated clients
274
- // from being able to silently re-authenticate users.
275
- if (clientAuth.method === 'none' && !client.info.isFirstParty) {
259
+ // ATPROTO extension: if the client is not trusted, and not authenticated,
260
+ // force users to consent to authorization requests. We do this to avoid
261
+ // unauthenticated clients from being able to silently re-authenticate
262
+ // users.
263
+ if (
264
+ !client.info.isTrusted &&
265
+ !client.info.isFirstParty &&
266
+ clientAuth.method === 'none'
267
+ ) {
276
268
  if (parameters.prompt === 'none') {
277
269
  throw new ConsentRequiredError(
278
270
  parameters,
@@ -346,6 +338,7 @@ export class RequestManager {
346
338
  uri,
347
339
  expiresAt: updates.expiresAt || data.expiresAt,
348
340
  parameters: data.parameters,
341
+ clientId: data.clientId,
349
342
  clientAuth: data.clientAuth,
350
343
  }
351
344
  }
@@ -355,8 +348,7 @@ export class RequestManager {
355
348
  uri: RequestUri,
356
349
  deviceId: DeviceId,
357
350
  account: Account,
358
- info: DeviceAccountInfo,
359
- ): Promise<{ code?: Code; token?: string; id_token?: string }> {
351
+ ): Promise<Code> {
360
352
  const id = decodeRequestUri(uri)
361
353
 
362
354
  const data = await this.store.readRequest(id)
@@ -385,18 +377,8 @@ export class RequestManager {
385
377
  )
386
378
  }
387
379
 
388
- const responseType = data.parameters.response_type.split(' ')
389
-
390
- if (responseType.includes('token')) {
391
- throw new AccessDeniedError(
392
- data.parameters,
393
- 'Implicit "token" forbidden (use "code" with PKCE instead)',
394
- )
395
- }
396
-
397
- const code = responseType.includes('code')
398
- ? await generateCode()
399
- : undefined
380
+ // Only response_type=code is supported
381
+ const code = await generateCode()
400
382
 
401
383
  // Bind the request to the account, preventing it from being used again.
402
384
  await this.store.updateRequest(id, {
@@ -406,15 +388,7 @@ export class RequestManager {
406
388
  expiresAt: new Date(Date.now() + AUTHORIZATION_INACTIVITY_TIMEOUT),
407
389
  })
408
390
 
409
- const id_token = responseType.includes('id_token')
410
- ? await this.signer.idToken(client, data.parameters, account, {
411
- auth_time: info.authenticatedAt,
412
- exp: this.createTokenExpiry(),
413
- code,
414
- })
415
- : undefined
416
-
417
- return { code, id_token }
391
+ return code
418
392
  } catch (err) {
419
393
  await this.store.deleteRequest(id)
420
394
  throw err
@@ -1,5 +1,3 @@
1
- import { randomBytes } from 'node:crypto'
2
-
3
1
  import {
4
2
  JwtPayload,
5
3
  JwtPayloadGetter,
@@ -12,14 +10,10 @@ import {
12
10
  OAuthAuthenticationRequestParameters,
13
11
  OAuthAuthorizationDetails,
14
12
  } from '@atproto/oauth-types'
15
- import { generate as hash } from 'oidc-token-hash'
16
13
 
17
14
  import { Account } from '../account/account.js'
18
15
  import { Client } from '../client/client.js'
19
- import { InvalidClientMetadataError } from '../errors/invalid-client-metadata-error.js'
20
16
  import { dateToEpoch } from '../lib/util/date.js'
21
- import { claimRequested } from '../parameters/claims-requested.js'
22
- import { oidcPayload } from '../parameters/oidc-payload.js'
23
17
  import { TokenId } from '../token/token-id.js'
24
18
  import {
25
19
  SignedTokenPayload,
@@ -105,61 +99,4 @@ export class Signer {
105
99
 
106
100
  return result
107
101
  }
108
-
109
- async idToken(
110
- client: Client,
111
- params: OAuthAuthenticationRequestParameters,
112
- account: Account,
113
- extra: {
114
- exp: Date
115
- iat?: Date
116
- auth_time?: Date
117
- code?: string
118
- access_token?: string
119
- },
120
- ): Promise<SignedJwt> {
121
- // This can happen when a client is using password_grant. If a client is
122
- // using password_grant, it should not set "require_auth_time" to true.
123
- if (client.metadata.require_auth_time && extra.auth_time == null) {
124
- throw new InvalidClientMetadataError(
125
- '"require_auth_time" metadata is not compatible with "password_grant" flow',
126
- )
127
- }
128
-
129
- return this.sign(
130
- {
131
- alg: client.metadata.id_token_signed_response_alg,
132
- typ: 'JWT',
133
- },
134
- async ({ alg }, key) => ({
135
- ...oidcPayload(params, account),
136
-
137
- aud: client.id,
138
- iat: dateToEpoch(extra.iat),
139
- exp: dateToEpoch(extra.exp),
140
- sub: account.sub,
141
- jti: randomBytes(16).toString('hex'),
142
- scope: params.scope,
143
- nonce: params.nonce,
144
-
145
- s_hash: params.state //
146
- ? await hash(params.state, alg, key.crv)
147
- : undefined,
148
- c_hash: extra.code //
149
- ? await hash(extra.code, alg, key.crv)
150
- : undefined,
151
- at_hash: extra.access_token //
152
- ? await hash(extra.access_token, alg, key.crv)
153
- : undefined,
154
-
155
- // https://openid.net/specs/openid-provider-authentication-policy-extension-1_0.html#rfc.section.5.2
156
- auth_time:
157
- client.metadata.require_auth_time ||
158
- (extra.auth_time != null && params.max_age != null) ||
159
- claimRequested(params, 'id_token', 'auth_time', extra.auth_time)
160
- ? dateToEpoch(extra.auth_time!)
161
- : undefined,
162
- }),
163
- )
164
- }
165
102
  }
@@ -1,4 +1,4 @@
1
- import { isSignedJwt, SignedJwt } from '@atproto/jwk'
1
+ import { isSignedJwt } from '@atproto/jwk'
2
2
  import {
3
3
  AccessToken,
4
4
  CLIENT_ASSERTION_TYPE_JWT_BEARER,
@@ -140,6 +140,10 @@ export class TokenManager {
140
140
  if (!('code_verifier' in input) || !input.code_verifier) {
141
141
  throw new InvalidGrantError('code_verifier is required')
142
142
  }
143
+ // Prevent client from generating too short code_verifiers
144
+ if (input.code_verifier.length < 43) {
145
+ throw new InvalidGrantError('code_verifier too short')
146
+ }
143
147
  switch (parameters.code_challenge_method) {
144
148
  case undefined: // Default is "plain" (per spec)
145
149
  case 'plain': {
@@ -181,8 +185,7 @@ export class TokenManager {
181
185
  }
182
186
 
183
187
  const tokenId = await generateTokenId()
184
- const scopes = parameters.scope?.split(' ')
185
- const refreshToken = scopes?.includes('offline_access')
188
+ const refreshToken = client.metadata.grant_types.includes('refresh_token')
186
189
  ? await generateRefreshToken()
187
190
  : undefined
188
191
 
@@ -222,22 +225,10 @@ export class TokenManager {
222
225
  authorization_details: authorizationDetails,
223
226
  })
224
227
 
225
- const idToken = scopes?.includes('openid')
226
- ? await this.signer.idToken(client, parameters, account, {
227
- exp: expiresAt,
228
- iat: now,
229
- // If there is no deviceInfo, we are in a "password_grant" context
230
- auth_time: device?.info.authenticatedAt || new Date(),
231
- access_token: accessToken,
232
- code,
233
- })
234
- : undefined
235
-
236
228
  return this.buildTokenResponse(
237
229
  client,
238
230
  accessToken,
239
231
  refreshToken,
240
- idToken,
241
232
  expiresAt,
242
233
  parameters,
243
234
  account,
@@ -249,7 +240,6 @@ export class TokenManager {
249
240
  client: Client,
250
241
  accessToken: AccessToken,
251
242
  refreshToken: string | undefined,
252
- idToken: SignedJwt | undefined,
253
243
  expiresAt: Date,
254
244
  parameters: OAuthAuthenticationRequestParameters,
255
245
  account: Account,
@@ -259,8 +249,7 @@ export class TokenManager {
259
249
  access_token: accessToken,
260
250
  token_type: parameters.dpop_jkt ? 'DPoP' : 'Bearer',
261
251
  refresh_token: refreshToken,
262
- id_token: idToken,
263
- scope: parameters.scope ?? '',
252
+ scope: parameters.scope,
264
253
  authorization_details: authorizationDetails,
265
254
  get expires_in() {
266
255
  return dateToRelativeSeconds(expiresAt)
@@ -272,12 +261,6 @@ export class TokenManager {
272
261
  sub: account.sub,
273
262
  }
274
263
 
275
- await this.hooks.onTokenResponse?.call(null, tokenResponse, {
276
- client,
277
- parameters,
278
- account,
279
- })
280
-
281
264
  return tokenResponse
282
265
  }
283
266
 
@@ -316,7 +299,7 @@ export class TokenManager {
316
299
  throw new InvalidGrantError(`Invalid refresh token`)
317
300
  }
318
301
 
319
- const { account, info, data } = tokenInfo
302
+ const { account, data } = tokenInfo
320
303
  const { parameters } = data
321
304
 
322
305
  try {
@@ -400,26 +383,10 @@ export class TokenManager {
400
383
  authorization_details,
401
384
  })
402
385
 
403
- // https://openid.net/specs/openid-connect-core-1_0.html#rfc.section.3.1.3.3
404
- //
405
- // > In addition to the response parameters specified by OAuth 2.0, the
406
- // > following parameters MUST be included in the response:
407
- // > - id_token: ID Token value associated with the authenticated session.
408
- const scopes = parameters.scope?.split(' ')
409
- const idToken = scopes?.includes('openid')
410
- ? await this.signer.idToken(client, parameters, account, {
411
- exp: expiresAt,
412
- iat: now,
413
- auth_time: info?.authenticatedAt,
414
- access_token: accessToken,
415
- })
416
- : undefined
417
-
418
386
  return this.buildTokenResponse(
419
387
  client,
420
388
  accessToken,
421
389
  nextRefreshToken,
422
- idToken,
423
390
  expiresAt,
424
391
  parameters,
425
392
  account,
@@ -1,16 +0,0 @@
1
- import { JwtPayload } from '@atproto/jwk';
2
- /**
3
- * @see {@link https://openid.net/specs/openid-connect-core-1_0.html#ScopeClaims | OpenID Connect Core 1.0, 5.4. Requesting Claims using Scope Values}
4
- */
5
- export declare const OIDC_SCOPE_CLAIMS: Readonly<{
6
- email: readonly ["email", "email_verified"];
7
- phone: readonly ["phone_number", "phone_number_verified"];
8
- address: readonly ["address"];
9
- profile: readonly ["name", "family_name", "given_name", "middle_name", "nickname", "preferred_username", "gender", "picture", "profile", "website", "birthdate", "zoneinfo", "locale", "updated_at"];
10
- }>;
11
- export declare const OIDC_STANDARD_CLAIMS: readonly ("name" | "email" | "email_verified" | "phone_number" | "phone_number_verified" | "address" | "profile" | "family_name" | "given_name" | "middle_name" | "nickname" | "preferred_username" | "gender" | "picture" | "website" | "birthdate" | "zoneinfo" | "locale" | "updated_at")[];
12
- export type OIDCStandardClaim = (typeof OIDC_STANDARD_CLAIMS)[number];
13
- export type OIDCStandardPayload = Partial<{
14
- [K in OIDCStandardClaim]?: JwtPayload[K];
15
- }>;
16
- //# sourceMappingURL=claims.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"claims.d.ts","sourceRoot":"","sources":["../../src/oidc/claims.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AAEzC;;GAEG;AACH,eAAO,MAAM,iBAAiB;;;;;EAoB5B,CAAA;AAEF,eAAO,MAAM,oBAAoB,gSAEhC,CAAA;AAED,MAAM,MAAM,iBAAiB,GAAG,CAAC,OAAO,oBAAoB,CAAC,CAAC,MAAM,CAAC,CAAA;AACrE,MAAM,MAAM,mBAAmB,GAAG,OAAO,CAAC;KACvC,CAAC,IAAI,iBAAiB,CAAC,CAAC,EAAE,UAAU,CAAC,CAAC,CAAC;CACzC,CAAC,CAAA"}
@@ -1,29 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.OIDC_STANDARD_CLAIMS = exports.OIDC_SCOPE_CLAIMS = void 0;
4
- /**
5
- * @see {@link https://openid.net/specs/openid-connect-core-1_0.html#ScopeClaims | OpenID Connect Core 1.0, 5.4. Requesting Claims using Scope Values}
6
- */
7
- exports.OIDC_SCOPE_CLAIMS = Object.freeze({
8
- email: Object.freeze(['email', 'email_verified']),
9
- phone: Object.freeze(['phone_number', 'phone_number_verified']),
10
- address: Object.freeze(['address']),
11
- profile: Object.freeze([
12
- 'name',
13
- 'family_name',
14
- 'given_name',
15
- 'middle_name',
16
- 'nickname',
17
- 'preferred_username',
18
- 'gender',
19
- 'picture',
20
- 'profile',
21
- 'website',
22
- 'birthdate',
23
- 'zoneinfo',
24
- 'locale',
25
- 'updated_at',
26
- ]),
27
- });
28
- exports.OIDC_STANDARD_CLAIMS = Object.freeze(Object.values(exports.OIDC_SCOPE_CLAIMS).flat());
29
- //# sourceMappingURL=claims.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"claims.js","sourceRoot":"","sources":["../../src/oidc/claims.ts"],"names":[],"mappings":";;;AAEA;;GAEG;AACU,QAAA,iBAAiB,GAAG,MAAM,CAAC,MAAM,CAAC;IAC7C,KAAK,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,OAAO,EAAE,gBAAgB,CAAU,CAAC;IAC1D,KAAK,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,cAAc,EAAE,uBAAuB,CAAU,CAAC;IACxE,OAAO,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,SAAS,CAAU,CAAC;IAC5C,OAAO,EAAE,MAAM,CAAC,MAAM,CAAC;QACrB,MAAM;QACN,aAAa;QACb,YAAY;QACZ,aAAa;QACb,UAAU;QACV,oBAAoB;QACpB,QAAQ;QACR,SAAS;QACT,SAAS;QACT,SAAS;QACT,WAAW;QACX,UAAU;QACV,QAAQ;QACR,YAAY;KACJ,CAAC;CACZ,CAAC,CAAA;AAEW,QAAA,oBAAoB,GAAG,MAAM,CAAC,MAAM,CAC/C,MAAM,CAAC,MAAM,CAAC,yBAAiB,CAAC,CAAC,IAAI,EAAE,CACxC,CAAA"}
@@ -1,7 +0,0 @@
1
- import { OIDCStandardPayload } from './claims.js';
2
- export type Userinfo = OIDCStandardPayload & {
3
- sub: string;
4
- client_id: string;
5
- username?: string;
6
- };
7
- //# sourceMappingURL=userinfo.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"userinfo.d.ts","sourceRoot":"","sources":["../../src/oidc/userinfo.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAA;AAEjD,MAAM,MAAM,QAAQ,GAAG,mBAAmB,GAAG;IAE3C,GAAG,EAAE,MAAM,CAAA;IAGX,SAAS,EAAE,MAAM,CAAA;IAEjB,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB,CAAA"}
@@ -1,3 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- //# sourceMappingURL=userinfo.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"userinfo.js","sourceRoot":"","sources":["../../src/oidc/userinfo.ts"],"names":[],"mappings":""}
@@ -1,3 +0,0 @@
1
- import { OAuthAuthenticationRequestParameters, OidcClaimsParameter, OidcEntityType } from '@atproto/oauth-types';
2
- export declare function claimRequested(parameters: OAuthAuthenticationRequestParameters, entityType: OidcEntityType, claimName: OidcClaimsParameter, value: unknown): boolean;
3
- //# sourceMappingURL=claims-requested.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"claims-requested.d.ts","sourceRoot":"","sources":["../../src/parameters/claims-requested.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,oCAAoC,EACpC,mBAAmB,EACnB,cAAc,EACf,MAAM,sBAAsB,CAAA;AAG7B,wBAAgB,cAAc,CAC5B,UAAU,EAAE,oCAAoC,EAChD,UAAU,EAAE,cAAc,EAC1B,SAAS,EAAE,mBAAmB,EAC9B,KAAK,EAAE,OAAO,GACb,OAAO,CA+BT"}