@atproto/oauth-provider 0.1.2 → 0.2.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.
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"}