@atproto/oauth-provider 0.11.1 → 0.12.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 (56) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/dist/access-token/access-token-mode.d.ts +1 -1
  3. package/dist/access-token/access-token-mode.d.ts.map +1 -1
  4. package/dist/access-token/access-token-mode.js +1 -1
  5. package/dist/access-token/access-token-mode.js.map +1 -1
  6. package/dist/lib/util/function.d.ts +1 -0
  7. package/dist/lib/util/function.d.ts.map +1 -1
  8. package/dist/lib/util/function.js +4 -0
  9. package/dist/lib/util/function.js.map +1 -1
  10. package/dist/oauth-hooks.d.ts +36 -3
  11. package/dist/oauth-hooks.d.ts.map +1 -1
  12. package/dist/oauth-hooks.js.map +1 -1
  13. package/dist/oauth-provider.d.ts +4 -4
  14. package/dist/oauth-provider.d.ts.map +1 -1
  15. package/dist/oauth-provider.js +10 -16
  16. package/dist/oauth-provider.js.map +1 -1
  17. package/dist/oauth-verifier.d.ts +22 -9
  18. package/dist/oauth-verifier.d.ts.map +1 -1
  19. package/dist/oauth-verifier.js +61 -6
  20. package/dist/oauth-verifier.js.map +1 -1
  21. package/dist/request/request-manager.d.ts.map +1 -1
  22. package/dist/request/request-manager.js +11 -7
  23. package/dist/request/request-manager.js.map +1 -1
  24. package/dist/signer/{signed-token-payload.d.ts → access-token-payload.d.ts} +3 -3
  25. package/dist/signer/{signed-token-payload.d.ts.map → access-token-payload.d.ts.map} +1 -1
  26. package/dist/signer/{signed-token-payload.js → access-token-payload.js} +3 -3
  27. package/dist/signer/{signed-token-payload.js.map → access-token-payload.js.map} +1 -1
  28. package/dist/signer/signer.d.ts +3 -3
  29. package/dist/signer/signer.d.ts.map +1 -1
  30. package/dist/signer/signer.js +2 -2
  31. package/dist/signer/signer.js.map +1 -1
  32. package/dist/token/token-claims.d.ts +23 -0
  33. package/dist/token/token-claims.d.ts.map +1 -0
  34. package/dist/token/token-claims.js +3 -0
  35. package/dist/token/token-claims.js.map +1 -0
  36. package/dist/token/token-manager.d.ts +11 -6
  37. package/dist/token/token-manager.d.ts.map +1 -1
  38. package/dist/token/token-manager.js +46 -28
  39. package/dist/token/token-manager.js.map +1 -1
  40. package/package.json +8 -8
  41. package/src/access-token/access-token-mode.ts +1 -1
  42. package/src/lib/util/function.ts +4 -0
  43. package/src/oauth-hooks.ts +43 -1
  44. package/src/oauth-provider.ts +17 -31
  45. package/src/oauth-verifier.ts +122 -50
  46. package/src/request/request-manager.ts +12 -8
  47. package/src/signer/{signed-token-payload.ts → access-token-payload.ts} +2 -2
  48. package/src/signer/signer.ts +7 -7
  49. package/src/token/token-claims.ts +21 -0
  50. package/src/token/token-manager.ts +64 -58
  51. package/tsconfig.build.tsbuildinfo +1 -1
  52. package/dist/token/verify-token-claims.d.ts +0 -20
  53. package/dist/token/verify-token-claims.d.ts.map +0 -1
  54. package/dist/token/verify-token-claims.js +0 -53
  55. package/dist/token/verify-token-claims.js.map +0 -1
  56. package/src/token/verify-token-claims.ts +0 -101
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atproto/oauth-provider",
3
- "version": "0.11.1",
3
+ "version": "0.12.0",
4
4
  "license": "MIT",
5
5
  "description": "Generic OAuth2 and OpenID Connect provider for Node.js. Currently only supports features needed for Atproto.",
6
6
  "keywords": [
@@ -43,21 +43,21 @@
43
43
  "jose": "^5.2.0",
44
44
  "zod": "^3.23.8",
45
45
  "@atproto-labs/fetch": "0.2.3",
46
- "@atproto-labs/pipe": "0.1.1",
47
46
  "@atproto-labs/fetch-node": "0.1.10",
47
+ "@atproto-labs/pipe": "0.1.1",
48
48
  "@atproto-labs/simple-store": "0.3.0",
49
49
  "@atproto-labs/simple-store-memory": "0.1.4",
50
- "@atproto/common": "^0.4.11",
50
+ "@atproto/common": "^0.4.12",
51
51
  "@atproto/did": "0.2.0",
52
52
  "@atproto/jwk": "0.5.0",
53
53
  "@atproto/jwk-jose": "0.1.10",
54
- "@atproto/lexicon": "0.5.0",
55
- "@atproto/lexicon-resolver": "0.2.0",
54
+ "@atproto/lexicon": "0.5.1",
55
+ "@atproto/lexicon-resolver": "0.2.1",
56
56
  "@atproto/oauth-types": "0.4.1",
57
57
  "@atproto/oauth-provider-api": "0.3.0",
58
- "@atproto/oauth-provider-frontend": "0.2.0",
59
- "@atproto/oauth-provider-ui": "0.3.0",
60
- "@atproto/oauth-scopes": "0.1.0",
58
+ "@atproto/oauth-provider-frontend": "0.2.1",
59
+ "@atproto/oauth-provider-ui": "0.3.1",
60
+ "@atproto/oauth-scopes": "0.2.0",
61
61
  "@atproto/syntax": "0.4.1"
62
62
  },
63
63
  "devDependencies": {
@@ -1,4 +1,4 @@
1
1
  export enum AccessTokenMode {
2
2
  stateless = 'stateless',
3
- light = 'light',
3
+ stateful = 'stateful',
4
4
  }
@@ -33,3 +33,7 @@ export function invokeOnce<T extends (this: any, ...a: any[]) => any>(
33
33
  throw new Error('Function called multiple times')
34
34
  } as T
35
35
  }
36
+
37
+ export function includedIn<T>(this: readonly T[], value: T): boolean {
38
+ return this.includes(value)
39
+ }
@@ -1,10 +1,12 @@
1
1
  import { Jwks } from '@atproto/jwk'
2
2
  import type { Account } from '@atproto/oauth-provider-api'
3
3
  import {
4
+ OAuthAccessToken,
4
5
  OAuthAuthorizationDetails,
5
6
  OAuthAuthorizationRequestParameters,
6
7
  OAuthClientMetadata,
7
8
  OAuthTokenResponse,
9
+ OAuthTokenType,
8
10
  } from '@atproto/oauth-types'
9
11
  import { SignInData } from './account/sign-in-data.js'
10
12
  import { SignUpInput } from './account/sign-up-input.js'
@@ -12,6 +14,7 @@ import { ClientAuth } from './client/client-auth.js'
12
14
  import { ClientId } from './client/client-id.js'
13
15
  import { ClientInfo } from './client/client-info.js'
14
16
  import { Client } from './client/client.js'
17
+ import { DpopProof } from './dpop/dpop-proof.js'
15
18
  import { AccessDeniedError } from './errors/access-denied-error.js'
16
19
  import { AuthorizationError } from './errors/authorization-error.js'
17
20
  import { InvalidRequestError } from './errors/invalid-request-error.js'
@@ -22,13 +25,16 @@ import {
22
25
  HcaptchaVerifyResult,
23
26
  } from './lib/hcaptcha.js'
24
27
  import { RequestMetadata } from './lib/http/request.js'
25
- import { Awaitable } from './lib/util/type.js'
28
+ import { Awaitable, OmitKey } from './lib/util/type.js'
26
29
  import { DeviceId, SignUpData } from './oauth-store.js'
27
30
  import { RequestId } from './request/request-id.js'
31
+ import { AccessTokenPayload } from './signer/access-token-payload.js'
32
+ import { TokenClaims } from './token/token-claims.js'
28
33
 
29
34
  // Make sure all types needed to implement the OAuthHooks are exported
30
35
  export {
31
36
  AccessDeniedError,
37
+ type AccessTokenPayload,
32
38
  type Account,
33
39
  AuthorizationError,
34
40
  type Awaitable,
@@ -37,20 +43,24 @@ export {
37
43
  type ClientId,
38
44
  type ClientInfo,
39
45
  type DeviceId,
46
+ type DpopProof,
40
47
  type HcaptchaClientTokens,
41
48
  type HcaptchaConfig,
42
49
  type HcaptchaVerifyResult,
43
50
  InvalidRequestError,
44
51
  type Jwks,
52
+ type OAuthAccessToken,
45
53
  type OAuthAuthorizationDetails,
46
54
  type OAuthAuthorizationRequestParameters,
47
55
  type OAuthClientMetadata,
48
56
  OAuthError,
49
57
  type OAuthTokenResponse,
58
+ type OAuthTokenType,
50
59
  type RequestMetadata,
51
60
  type SignInData,
52
61
  type SignUpData,
53
62
  type SignUpInput,
63
+ type TokenClaims,
54
64
  }
55
65
 
56
66
  export type OAuthHooks = {
@@ -151,6 +161,38 @@ export type OAuthHooks = {
151
161
  requestId: RequestId
152
162
  }) => Awaitable<void>
153
163
 
164
+ /**
165
+ * This hook is called whenever a token is about to be created. You can use
166
+ * it to modify the token claims or perform additional validation.
167
+ *
168
+ * This hook should never throw an error.
169
+ */
170
+ onCreateToken?: (data: {
171
+ client: Client
172
+ account: Account
173
+ parameters: OAuthAuthorizationRequestParameters
174
+ claims: TokenClaims
175
+ }) => Awaitable<void | OmitKey<AccessTokenPayload, 'iss'>>
176
+
177
+ /**
178
+ * This hook is called whenever a token was just decoded, and basic validation
179
+ * was performed (signature, expiration, not-before).
180
+ *
181
+ * It can be used to modify the payload (e.g., to add custom claims), or to
182
+ * perform additional validation.
183
+ *
184
+ * This hook is called when authenticating requests through the
185
+ * `authenticateRequest()` method in `OAuthVerifier` and `OAuthProvider`.
186
+ *
187
+ * Any error thrown here will be propagated.
188
+ */
189
+ onDecodeToken?: (data: {
190
+ tokenType: OAuthTokenType
191
+ token: OAuthAccessToken
192
+ payload: AccessTokenPayload
193
+ dpopProof: null | DpopProof
194
+ }) => Promise<AccessTokenPayload | void>
195
+
154
196
  /**
155
197
  * This hook is called when an authorized client exchanges an authorization
156
198
  * code for an access token.
@@ -85,6 +85,7 @@ import {
85
85
  DpopProof,
86
86
  OAuthVerifier,
87
87
  OAuthVerifierOptions,
88
+ VerifyTokenPayloadOptions,
88
89
  } from './oauth-verifier.js'
89
90
  import { ReplayStore, ifReplayStore } from './replay/replay-store.js'
90
91
  import { codeSchema } from './request/code.js'
@@ -95,6 +96,7 @@ import { AuthorizationRedirectParameters } from './result/authorization-redirect
95
96
  import { AuthorizationResultAuthorizePage } from './result/authorization-result-authorize-page.js'
96
97
  import { AuthorizationResultRedirect } from './result/authorization-result-redirect.js'
97
98
  import { ErrorHandler } from './router/error-handler.js'
99
+ import { AccessTokenPayload } from './signer/access-token-payload.js'
98
100
  import { TokenData } from './token/token-data.js'
99
101
  import { TokenManager } from './token/token-manager.js'
100
102
  import {
@@ -102,14 +104,11 @@ import {
102
104
  asTokenStore,
103
105
  refreshTokenSchema,
104
106
  } from './token/token-store.js'
105
- import {
106
- VerifyTokenClaimsOptions,
107
- VerifyTokenClaimsResult,
108
- } from './token/verify-token-claims.js'
109
107
  import { isPARResponseError } from './types/par-response-error.js'
110
108
 
111
109
  export { AccessTokenMode, Keyset }
112
110
  export type {
111
+ AccessTokenPayload,
113
112
  AuthorizationRedirectParameters,
114
113
  AuthorizationResultAuthorizePage as AuthorizationResultAuthorize,
115
114
  AuthorizationResultRedirect,
@@ -123,6 +122,7 @@ export type {
123
122
  LexiconResolver,
124
123
  MultiLangString,
125
124
  OAuthAuthorizationServerMetadata,
125
+ VerifyTokenPayloadOptions,
126
126
  }
127
127
 
128
128
  type OAuthProviderConfig = {
@@ -1075,41 +1075,27 @@ export class OAuthProvider extends OAuthVerifier {
1075
1075
  }
1076
1076
  }
1077
1077
 
1078
- protected override async verifyToken(
1078
+ protected override async decodeToken(
1079
1079
  tokenType: OAuthTokenType,
1080
1080
  token: OAuthAccessToken,
1081
1081
  dpopProof: null | DpopProof,
1082
- verifyOptions?: VerifyTokenClaimsOptions,
1083
- ): Promise<VerifyTokenClaimsResult> {
1084
- if (this.accessTokenMode === AccessTokenMode.stateless) {
1085
- return super.verifyToken(tokenType, token, dpopProof, verifyOptions)
1086
- }
1087
-
1088
- if (this.accessTokenMode === AccessTokenMode.light) {
1089
- const { tokenClaims } = await super.verifyToken(
1090
- tokenType,
1091
- token,
1092
- dpopProof,
1093
- // Do not verify the scope and audience in case of "light" tokens.
1094
- // these will be checked through the tokenManager hereafter.
1095
- undefined,
1096
- )
1082
+ ): Promise<AccessTokenPayload> {
1083
+ const tokenPayload = await super.decodeToken(tokenType, token, dpopProof)
1097
1084
 
1098
- const tokenId = tokenClaims.jti
1085
+ if (this.accessTokenMode !== AccessTokenMode.stateless) {
1086
+ // @NOTE in non stateless mode, some claims can be omitted (most notably
1087
+ // "scope"). We load the token claims here (allowing to ensure that the
1088
+ // token is still valid, and to retrieve a (potentially updated) set of
1089
+ // claims).
1099
1090
 
1100
- // In addition to verifying the signature (through the verifier above), we
1101
- // also verify the tokenId is still valid using a database to fetch
1102
- // missing data from "light" token.
1103
- return this.tokenManager.verifyToken(
1104
- token,
1091
+ const tokenClaims = await this.tokenManager.loadTokenClaims(
1105
1092
  tokenType,
1106
- tokenId,
1107
- dpopProof,
1108
- verifyOptions,
1093
+ tokenPayload,
1109
1094
  )
1095
+
1096
+ Object.assign(tokenPayload, tokenClaims)
1110
1097
  }
1111
1098
 
1112
- // Fool-proof
1113
- throw new Error('Invalid access token mode')
1099
+ return tokenPayload
1114
1100
  }
1115
1101
  }
@@ -9,53 +9,65 @@ import {
9
9
  import { DpopManager, DpopManagerOptions } from './dpop/dpop-manager.js'
10
10
  import { DpopNonce } from './dpop/dpop-nonce.js'
11
11
  import { DpopProof } from './dpop/dpop-proof.js'
12
+ import { InvalidDpopKeyBindingError } from './errors/invalid-dpop-key-binding-error.js'
12
13
  import { InvalidDpopProofError } from './errors/invalid-dpop-proof-error.js'
13
14
  import { InvalidTokenError } from './errors/invalid-token-error.js'
14
15
  import { UseDpopNonceError } from './errors/use-dpop-nonce-error.js'
15
16
  import { WWWAuthenticateError } from './errors/www-authenticate-error.js'
16
17
  import { parseAuthorizationHeader } from './lib/util/authorization-header.js'
17
- import { Override } from './lib/util/type.js'
18
+ import { includedIn } from './lib/util/function.js'
19
+ import { OAuthHooks } from './oauth-hooks.js'
18
20
  import { ReplayManager } from './replay/replay-manager.js'
19
21
  import { ReplayStoreMemory } from './replay/replay-store-memory.js'
20
22
  import { ReplayStoreRedis } from './replay/replay-store-redis.js'
21
23
  import { ReplayStore } from './replay/replay-store.js'
24
+ import { AccessTokenPayload } from './signer/access-token-payload.js'
22
25
  import { Signer } from './signer/signer.js'
23
- import {
24
- VerifyTokenClaimsOptions,
25
- VerifyTokenClaimsResult,
26
- verifyTokenClaims,
27
- } from './token/verify-token-claims.js'
28
-
29
- export type * from './token/verify-token-claims.js'
30
-
31
- export type OAuthVerifierOptions = Override<
32
- DpopManagerOptions,
33
- {
34
- /**
35
- * The "issuer" identifier of the OAuth provider, this is the base URL of the
36
- * OAuth provider.
37
- */
38
- issuer: URL | string
39
-
40
- /**
41
- * The keyset used to sign access tokens.
42
- */
43
- keyset: Keyset | Iterable<Key | undefined | null | false>
44
-
45
- /**
46
- * A redis instance to use for replay protection. If not provided, replay
47
- * protection will use memory storage.
48
- */
49
- redis?: Redis | RedisOptions | string
50
-
51
- replayStore?: ReplayStore
52
- }
53
- >
26
+
27
+ export type DecodeTokenHook = OAuthHooks['onDecodeToken']
28
+
29
+ export type OAuthVerifierOptions = DpopManagerOptions & {
30
+ /**
31
+ * The "issuer" identifier of the OAuth provider, this is the base URL of the
32
+ * OAuth provider.
33
+ */
34
+ issuer: URL | string
35
+
36
+ /**
37
+ * The keyset used to sign access tokens.
38
+ */
39
+ keyset: Keyset | Iterable<Key | undefined | null | false>
40
+
41
+ /**
42
+ * A redis instance to use for replay protection. If not provided, replay
43
+ * protection will use memory storage.
44
+ */
45
+ redis?: Redis | RedisOptions | string
46
+
47
+ replayStore?: ReplayStore
48
+
49
+ onDecodeToken?: DecodeTokenHook
50
+ }
51
+
52
+ export type VerifyTokenPayloadOptions = {
53
+ /** One of these audience must be included in the token audience(s) */
54
+ audience?: [string, ...string[]]
55
+ /** One of these scope must be included in the token scope(s) */
56
+ scope?: [string, ...string[]]
57
+ }
54
58
 
55
59
  export { DpopNonce, Key, Keyset }
56
- export type { DpopProof, RedisOptions, ReplayStore }
60
+ export type {
61
+ AccessTokenPayload,
62
+ DpopProof,
63
+ OAuthTokenType,
64
+ RedisOptions,
65
+ ReplayStore,
66
+ }
57
67
 
58
68
  export class OAuthVerifier {
69
+ private readonly onDecodeToken?: DecodeTokenHook
70
+
59
71
  public readonly issuer: OAuthIssuerIdentifier
60
72
  public readonly keyset: Keyset
61
73
 
@@ -70,6 +82,7 @@ export class OAuthVerifier {
70
82
  replayStore = redis != null
71
83
  ? new ReplayStoreRedis({ redis })
72
84
  : new ReplayStoreMemory(),
85
+ onDecodeToken,
73
86
 
74
87
  ...rest
75
88
  }: OAuthVerifierOptions) {
@@ -118,12 +131,11 @@ export class OAuthVerifier {
118
131
  return dpopProof
119
132
  }
120
133
 
121
- protected async verifyToken(
134
+ protected async decodeToken(
122
135
  tokenType: OAuthTokenType,
123
136
  token: OAuthAccessToken,
124
137
  dpopProof: null | DpopProof,
125
- verifyOptions?: VerifyTokenClaimsOptions,
126
- ): Promise<VerifyTokenClaimsResult> {
138
+ ): Promise<AccessTokenPayload> {
127
139
  if (!isSignedJwt(token)) {
128
140
  throw new InvalidTokenError(tokenType, `Malformed token`)
129
141
  }
@@ -134,22 +146,56 @@ export class OAuthVerifier {
134
146
  throw InvalidTokenError.from(err, tokenType)
135
147
  })
136
148
 
137
- return verifyTokenClaims(
138
- token,
139
- payload.jti,
149
+ if (payload.cnf?.jkt) {
150
+ // An access token with a cnf.jkt claim must be a DPoP token
151
+ if (tokenType !== 'DPoP') {
152
+ throw new InvalidTokenError(
153
+ 'DPoP',
154
+ `Access token is bound to a DPoP proof, but token type is ${tokenType}`,
155
+ )
156
+ }
157
+
158
+ // DPoP token type must be used with a DPoP proof
159
+ if (!dpopProof) {
160
+ throw new InvalidDpopProofError(`DPoP proof required`)
161
+ }
162
+
163
+ // DPoP proof must be signed with the key that matches the "cnf" claim
164
+ if (payload.cnf.jkt !== dpopProof.jkt) {
165
+ throw new InvalidDpopKeyBindingError()
166
+ }
167
+ } else {
168
+ // An access token without a cnf.jkt claim must be a Bearer token
169
+ if (tokenType !== 'Bearer') {
170
+ throw new InvalidTokenError(
171
+ 'Bearer',
172
+ `Bearer token type must be used without a DPoP proof`,
173
+ )
174
+ }
175
+
176
+ // @NOTE We ignore (but allow) DPoP proofs for Bearer tokens
177
+ }
178
+
179
+ const payloadOverride = await this.onDecodeToken?.call(null, {
140
180
  tokenType,
181
+ token,
141
182
  payload,
142
183
  dpopProof,
143
- verifyOptions,
144
- )
184
+ })
185
+
186
+ return payloadOverride ?? payload
145
187
  }
146
188
 
189
+ /**
190
+ * @throws {WWWAuthenticateError}
191
+ * @throws {InvalidTokenError}
192
+ */
147
193
  public async authenticateRequest(
148
194
  httpMethod: string,
149
195
  httpUrl: Readonly<URL>,
150
196
  httpHeaders: Record<string, undefined | string | string[]>,
151
- verifyOptions?: VerifyTokenClaimsOptions,
152
- ): Promise<VerifyTokenClaimsResult> {
197
+ verifyOptions?: VerifyTokenPayloadOptions,
198
+ ): Promise<AccessTokenPayload> {
153
199
  const [tokenType, token] = parseAuthorizationHeader(
154
200
  httpHeaders['authorization'],
155
201
  )
@@ -161,14 +207,11 @@ export class OAuthVerifier {
161
207
  token,
162
208
  )
163
209
 
164
- const tokenResult = await this.verifyToken(
165
- tokenType,
166
- token,
167
- dpopProof,
168
- verifyOptions,
169
- )
210
+ const tokenPayload = await this.decodeToken(tokenType, token, dpopProof)
170
211
 
171
- return tokenResult
212
+ this.verifyTokenPayload(tokenType, tokenPayload, verifyOptions)
213
+
214
+ return tokenPayload
172
215
  } catch (err) {
173
216
  if (err instanceof UseDpopNonceError) throw err.toWwwAuthenticateError()
174
217
  if (err instanceof WWWAuthenticateError) throw err
@@ -176,4 +219,33 @@ export class OAuthVerifier {
176
219
  throw InvalidTokenError.from(err, tokenType)
177
220
  }
178
221
  }
222
+
223
+ protected verifyTokenPayload(
224
+ tokenType: OAuthTokenType,
225
+ tokenPayload: AccessTokenPayload,
226
+ options?: VerifyTokenPayloadOptions,
227
+ ): void {
228
+ if (options?.audience) {
229
+ const { aud } = tokenPayload
230
+ const hasMatch =
231
+ aud != null &&
232
+ (Array.isArray(aud)
233
+ ? options.audience.some(includedIn, aud)
234
+ : options.audience.includes(aud))
235
+ if (!hasMatch) {
236
+ throw new InvalidTokenError(tokenType, `Invalid audience`)
237
+ }
238
+ }
239
+
240
+ if (options?.scope) {
241
+ const scopes = tokenPayload.scope?.split(' ')
242
+ if (!scopes || !options.scope.some(includedIn, scopes)) {
243
+ throw new InvalidTokenError(tokenType, `Invalid scope`)
244
+ }
245
+ }
246
+
247
+ if (tokenPayload.exp != null && tokenPayload.exp * 1000 <= Date.now()) {
248
+ throw new InvalidTokenError(tokenType, `Token expired`)
249
+ }
250
+ }
179
251
  }
@@ -293,18 +293,22 @@ export class RequestManager {
293
293
  // Make sure that every nsid in the scope resolves to a valid permission set
294
294
  // lexicon
295
295
  if (parameters.scope) {
296
- await this.lexiconManager
297
- .getPermissionSetsFromScope(parameters.scope)
298
- .catch((cause) => {
296
+ try {
297
+ await this.lexiconManager.getPermissionSetsFromScope(parameters.scope)
298
+ } catch (err) {
299
+ // Parse expected errors
300
+ if (err instanceof LexiconResolutionError) {
299
301
  throw new AuthorizationError(
300
302
  parameters,
301
- cause instanceof LexiconResolutionError
302
- ? cause.message
303
- : 'Unable to retrieve included permission sets',
303
+ err.message,
304
304
  'invalid_scope',
305
- cause,
305
+ err,
306
306
  )
307
- })
307
+ }
308
+
309
+ // Unexpected error
310
+ throw err
311
+ }
308
312
  }
309
313
 
310
314
  return parameters
@@ -4,7 +4,7 @@ import { clientIdSchema } from '../client/client-id.js'
4
4
  import { subSchema } from '../oidc/sub.js'
5
5
  import { tokenIdSchema } from '../token/token-id.js'
6
6
 
7
- export const signedTokenPayloadSchema = jwtPayloadSchema
7
+ export const accessTokenPayloadSchema = jwtPayloadSchema
8
8
  .partial()
9
9
  .extend({
10
10
  // Following are required
@@ -22,4 +22,4 @@ export const signedTokenPayloadSchema = jwtPayloadSchema
22
22
  })
23
23
  .passthrough()
24
24
 
25
- export type SignedTokenPayload = z.infer<typeof signedTokenPayloadSchema>
25
+ export type AccessTokenPayload = z.infer<typeof accessTokenPayloadSchema>
@@ -9,11 +9,11 @@ import {
9
9
  import { EPHEMERAL_SESSION_MAX_AGE } from '../constants.js'
10
10
  import { dateToEpoch } from '../lib/util/date.js'
11
11
  import { OmitKey, RequiredKey } from '../lib/util/type.js'
12
- import { ApiTokenPayload, apiTokenPayloadSchema } from './api-token-payload.js'
13
12
  import {
14
- SignedTokenPayload,
15
- signedTokenPayloadSchema,
16
- } from './signed-token-payload.js'
13
+ AccessTokenPayload,
14
+ accessTokenPayloadSchema,
15
+ } from './access-token-payload.js'
16
+ import { ApiTokenPayload, apiTokenPayloadSchema } from './api-token-payload.js'
17
17
 
18
18
  export type SignPayload = JwtPayload & { iss?: never }
19
19
 
@@ -49,7 +49,7 @@ export class Signer {
49
49
  }
50
50
 
51
51
  async createAccessToken(
52
- payload: OmitKey<SignedTokenPayload, 'iss'>,
52
+ payload: OmitKey<AccessTokenPayload, 'iss'>,
53
53
  ): Promise<SignedJwt> {
54
54
  return this.sign(
55
55
  {
@@ -68,8 +68,8 @@ export class Signer {
68
68
  const result = await this.verify<C>(token, { ...options, typ: 'at+jwt' })
69
69
  return {
70
70
  protectedHeader: result.protectedHeader,
71
- payload: signedTokenPayloadSchema.parse(result.payload) as RequiredKey<
72
- SignedTokenPayload,
71
+ payload: accessTokenPayloadSchema.parse(result.payload) as RequiredKey<
72
+ AccessTokenPayload,
73
73
  C
74
74
  >,
75
75
  }
@@ -0,0 +1,21 @@
1
+ import { OAuthScope } from '@atproto/oauth-types'
2
+ import { ClientId } from '../client/client-id.js'
3
+ import { TokenId } from './token-id.js'
4
+
5
+ /**
6
+ * The access token claims that will be set by the {@link TokenManager} and that
7
+ * will be passed to the "onCreateToken" hook.
8
+ *
9
+ * @note "iss" is missing here because it cannot be altered and will always be
10
+ * set to the Authorization Server's identifier.
11
+ */
12
+ export type TokenClaims = {
13
+ jti: TokenId
14
+ sub: string
15
+ iat: number
16
+ exp: number
17
+ aud: string | [string, ...string[]]
18
+ cnf?: { jkt: string }
19
+ scope?: OAuthScope
20
+ client_id: ClientId
21
+ }