@atproto/oauth-provider 0.19.7 → 0.19.8

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 (165) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/dist/errors/error-parser.js +5 -5
  3. package/dist/errors/error-parser.js.map +1 -1
  4. package/package.json +22 -18
  5. package/src/access-token/access-token-mode.ts +0 -4
  6. package/src/account/account-manager.ts +0 -591
  7. package/src/account/account-store.ts +0 -305
  8. package/src/account/sign-in-data.ts +0 -15
  9. package/src/account/sign-up-input.ts +0 -20
  10. package/src/client/client-auth.ts +0 -64
  11. package/src/client/client-data.ts +0 -9
  12. package/src/client/client-id.ts +0 -4
  13. package/src/client/client-info.ts +0 -13
  14. package/src/client/client-manager.ts +0 -772
  15. package/src/client/client-store.ts +0 -35
  16. package/src/client/client-utils.ts +0 -37
  17. package/src/client/client.ts +0 -394
  18. package/src/constants.ts +0 -80
  19. package/src/customization/branding.ts +0 -12
  20. package/src/customization/build-customization-css.ts +0 -55
  21. package/src/customization/build-customization-data.ts +0 -24
  22. package/src/customization/colors.ts +0 -48
  23. package/src/customization/customization.ts +0 -29
  24. package/src/customization/links.ts +0 -10
  25. package/src/device/device-data.ts +0 -11
  26. package/src/device/device-id.ts +0 -27
  27. package/src/device/device-manager.ts +0 -292
  28. package/src/device/device-store.ts +0 -31
  29. package/src/device/session-id.ts +0 -21
  30. package/src/dpop/dpop-manager.ts +0 -214
  31. package/src/dpop/dpop-nonce.ts +0 -121
  32. package/src/dpop/dpop-proof.ts +0 -6
  33. package/src/errors/access-denied-error.ts +0 -12
  34. package/src/errors/account-selection-required-error.ts +0 -12
  35. package/src/errors/authorization-error.ts +0 -45
  36. package/src/errors/consent-required-error.ts +0 -12
  37. package/src/errors/error-parser.ts +0 -147
  38. package/src/errors/handle-unavailable-error.ts +0 -23
  39. package/src/errors/invalid-authorization-details-error.ts +0 -27
  40. package/src/errors/invalid-client-error.ts +0 -20
  41. package/src/errors/invalid-client-id-error.ts +0 -31
  42. package/src/errors/invalid-client-metadata-error.ts +0 -68
  43. package/src/errors/invalid-credentials-error.ts +0 -29
  44. package/src/errors/invalid-dpop-key-binding-error.ts +0 -21
  45. package/src/errors/invalid-dpop-proof-error.ts +0 -13
  46. package/src/errors/invalid-grant-error.ts +0 -21
  47. package/src/errors/invalid-invite-code-error.ts +0 -10
  48. package/src/errors/invalid-redirect-uri-error.ts +0 -17
  49. package/src/errors/invalid-request-error.ts +0 -32
  50. package/src/errors/invalid-scope-error.ts +0 -15
  51. package/src/errors/invalid-token-error.ts +0 -60
  52. package/src/errors/login-required-error.ts +0 -12
  53. package/src/errors/oauth-error.ts +0 -28
  54. package/src/errors/second-authentication-factor-required-error.ts +0 -25
  55. package/src/errors/unauthorized-client-error.ts +0 -20
  56. package/src/errors/use-dpop-nonce-error.ts +0 -32
  57. package/src/errors/www-authenticate-error.ts +0 -64
  58. package/src/index.ts +0 -16
  59. package/src/lexicon/lexicon-data.ts +0 -11
  60. package/src/lexicon/lexicon-getter.ts +0 -55
  61. package/src/lexicon/lexicon-manager.ts +0 -116
  62. package/src/lexicon/lexicon-store.ts +0 -35
  63. package/src/lib/csp/index.ts +0 -101
  64. package/src/lib/hcaptcha.ts +0 -228
  65. package/src/lib/html/README.md +0 -9
  66. package/src/lib/html/build-document.ts +0 -151
  67. package/src/lib/html/escapers.ts +0 -66
  68. package/src/lib/html/html.ts +0 -56
  69. package/src/lib/html/hydration-data.ts +0 -19
  70. package/src/lib/html/index.ts +0 -5
  71. package/src/lib/html/tags.ts +0 -67
  72. package/src/lib/html/util.ts +0 -17
  73. package/src/lib/http/README.md +0 -11
  74. package/src/lib/http/accept.ts +0 -90
  75. package/src/lib/http/context.ts +0 -42
  76. package/src/lib/http/headers.ts +0 -15
  77. package/src/lib/http/index.ts +0 -10
  78. package/src/lib/http/method.ts +0 -18
  79. package/src/lib/http/middleware.ts +0 -169
  80. package/src/lib/http/parser.ts +0 -101
  81. package/src/lib/http/path.ts +0 -82
  82. package/src/lib/http/request.ts +0 -256
  83. package/src/lib/http/response.ts +0 -122
  84. package/src/lib/http/route.ts +0 -60
  85. package/src/lib/http/router.ts +0 -117
  86. package/src/lib/http/security-headers.ts +0 -100
  87. package/src/lib/http/stream.ts +0 -57
  88. package/src/lib/http/types.ts +0 -16
  89. package/src/lib/http/url.ts +0 -23
  90. package/src/lib/nsid.ts +0 -10
  91. package/src/lib/redis.ts +0 -25
  92. package/src/lib/util/authorization-header.ts +0 -28
  93. package/src/lib/util/cast.ts +0 -18
  94. package/src/lib/util/color.ts +0 -168
  95. package/src/lib/util/crypto.ts +0 -32
  96. package/src/lib/util/date.ts +0 -7
  97. package/src/lib/util/error.ts +0 -7
  98. package/src/lib/util/function.ts +0 -39
  99. package/src/lib/util/locale.ts +0 -12
  100. package/src/lib/util/object.ts +0 -23
  101. package/src/lib/util/redirect-uri.ts +0 -46
  102. package/src/lib/util/time.ts +0 -49
  103. package/src/lib/util/type.ts +0 -182
  104. package/src/lib/util/ui8.ts +0 -14
  105. package/src/lib/util/well-known.ts +0 -8
  106. package/src/lib/util/zod-error.ts +0 -26
  107. package/src/lib/write-form-redirect.ts +0 -58
  108. package/src/lib/write-html.ts +0 -70
  109. package/src/metadata/build-metadata.ts +0 -141
  110. package/src/oauth-client.ts +0 -3
  111. package/src/oauth-dpop.ts +0 -2
  112. package/src/oauth-errors.ts +0 -26
  113. package/src/oauth-hooks.ts +0 -519
  114. package/src/oauth-middleware.ts +0 -53
  115. package/src/oauth-provider.ts +0 -1110
  116. package/src/oauth-store.ts +0 -12
  117. package/src/oauth-verifier.ts +0 -260
  118. package/src/replay/replay-manager.ts +0 -51
  119. package/src/replay/replay-store-memory.ts +0 -36
  120. package/src/replay/replay-store-redis.ts +0 -30
  121. package/src/replay/replay-store.ts +0 -44
  122. package/src/request/code.ts +0 -23
  123. package/src/request/request-data.ts +0 -36
  124. package/src/request/request-id.ts +0 -22
  125. package/src/request/request-manager.ts +0 -521
  126. package/src/request/request-store.ts +0 -60
  127. package/src/request/request-uri.ts +0 -42
  128. package/src/result/authorization-redirect-parameters.ts +0 -24
  129. package/src/result/authorization-result-authorize-page.ts +0 -16
  130. package/src/result/authorization-result-redirect.ts +0 -8
  131. package/src/router/assets/assets-manifest.ts +0 -117
  132. package/src/router/assets/assets.ts +0 -124
  133. package/src/router/assets/csrf.ts +0 -79
  134. package/src/router/assets/send-account-page.ts +0 -23
  135. package/src/router/assets/send-authorization-page.ts +0 -42
  136. package/src/router/assets/send-cookie-error-page.ts +0 -21
  137. package/src/router/assets/send-error-page.ts +0 -25
  138. package/src/router/assets/send-redirect.ts +0 -140
  139. package/src/router/create-account-page-middleware.ts +0 -88
  140. package/src/router/create-api-middleware.ts +0 -1051
  141. package/src/router/create-authorization-page-middleware.ts +0 -281
  142. package/src/router/create-oauth-middleware.ts +0 -257
  143. package/src/router/error-handler.ts +0 -6
  144. package/src/router/middleware-options.ts +0 -9
  145. package/src/signer/access-token-payload.ts +0 -25
  146. package/src/signer/api-token-payload.ts +0 -18
  147. package/src/signer/signer.ts +0 -121
  148. package/src/token/refresh-token.ts +0 -30
  149. package/src/token/token-claims.ts +0 -22
  150. package/src/token/token-data.ts +0 -40
  151. package/src/token/token-id.ts +0 -25
  152. package/src/token/token-manager.ts +0 -427
  153. package/src/token/token-store.ts +0 -88
  154. package/src/types/authorization-response-error.ts +0 -27
  155. package/src/types/color-hue.ts +0 -3
  156. package/src/types/email-otp.ts +0 -3
  157. package/src/types/email.ts +0 -26
  158. package/src/types/handle.ts +0 -23
  159. package/src/types/invite-code.ts +0 -4
  160. package/src/types/par-response-error.ts +0 -25
  161. package/src/types/password.ts +0 -4
  162. package/src/types/rgb-color.ts +0 -21
  163. package/tsconfig.build.json +0 -8
  164. package/tsconfig.build.tsbuildinfo +0 -1
  165. package/tsconfig.json +0 -4
@@ -1,121 +0,0 @@
1
- import {
2
- JwtHeader,
3
- JwtPayload,
4
- JwtPayloadGetter,
5
- JwtSignHeader,
6
- Key,
7
- Keyset,
8
- SignedJwt,
9
- VerifyOptions,
10
- VerifyResult,
11
- } from '@atproto/jwk'
12
- import { EPHEMERAL_SESSION_MAX_AGE } from '../constants.js'
13
- import { dateToEpoch } from '../lib/util/date.js'
14
- import { OmitKey, RequiredKey } from '../lib/util/type.js'
15
- import {
16
- AccessTokenPayload,
17
- accessTokenPayloadSchema,
18
- } from './access-token-payload.js'
19
- import { ApiTokenPayload, apiTokenPayloadSchema } from './api-token-payload.js'
20
-
21
- export type SignPayload = JwtPayload & { iss?: never }
22
-
23
- export { Keyset }
24
- export type { JwtPayloadGetter, JwtSignHeader, SignedJwt, VerifyOptions }
25
-
26
- export class Signer {
27
- constructor(
28
- public readonly issuer: string,
29
- public readonly keyset: Keyset,
30
- ) {}
31
-
32
- async verify<C extends string = never>(
33
- token: SignedJwt,
34
- options?: Omit<VerifyOptions<C>, 'issuer'>,
35
- ): Promise<VerifyResult<C> & { key: Key }> {
36
- return this.keyset.verifyJwt<C>(token, {
37
- ...options,
38
- issuer: [this.issuer],
39
- })
40
- }
41
-
42
- public async sign(
43
- signHeader: JwtSignHeader,
44
- payload: SignPayload | JwtPayloadGetter<SignPayload>,
45
- ): Promise<SignedJwt> {
46
- return this.keyset.createJwt(signHeader, async (protectedHeader, key) => ({
47
- ...(typeof payload === 'function'
48
- ? await payload(protectedHeader, key)
49
- : payload),
50
- iss: this.issuer,
51
- }))
52
- }
53
-
54
- async createAccessToken(
55
- payload: OmitKey<AccessTokenPayload, 'iss'>,
56
- ): Promise<SignedJwt> {
57
- return this.sign(
58
- {
59
- // https://datatracker.ietf.org/doc/html/rfc9068#section-2.1
60
- alg: undefined,
61
- typ: 'at+jwt',
62
- },
63
- payload,
64
- )
65
- }
66
-
67
- async verifyAccessToken<C extends string = never>(
68
- token: SignedJwt,
69
- options?: Omit<VerifyOptions<C>, 'issuer' | 'typ'>,
70
- ): Promise<{
71
- protectedHeader: JwtHeader
72
- payload: RequiredKey<AccessTokenPayload, C>
73
- }> {
74
- const result = await this.verify<C>(token, { ...options, typ: 'at+jwt' })
75
- return {
76
- protectedHeader: result.protectedHeader,
77
- payload: accessTokenPayloadSchema.parse(result.payload) as RequiredKey<
78
- AccessTokenPayload,
79
- C
80
- >,
81
- }
82
- }
83
-
84
- async createEphemeralToken(
85
- payload: OmitKey<ApiTokenPayload, 'iss' | 'aud' | 'iat'>,
86
- ): Promise<SignedJwt> {
87
- return this.sign(
88
- {
89
- alg: undefined,
90
- typ: 'at+jwt',
91
- },
92
- {
93
- ...payload,
94
- aud: `oauth-provider-api@${this.issuer}`,
95
- iat: dateToEpoch(),
96
- },
97
- )
98
- }
99
-
100
- async verifyEphemeralToken<C extends string = never>(
101
- token: SignedJwt,
102
- options?: Omit<VerifyOptions<C>, 'issuer' | 'audience' | 'typ'>,
103
- ): Promise<{
104
- protectedHeader: JwtHeader
105
- payload: RequiredKey<ApiTokenPayload, C>
106
- }> {
107
- const result = await this.verify<C>(token, {
108
- ...options,
109
- maxTokenAge: options?.maxTokenAge ?? EPHEMERAL_SESSION_MAX_AGE / 1e3,
110
- audience: `oauth-provider-api@${this.issuer}`,
111
- typ: 'at+jwt',
112
- })
113
- return {
114
- protectedHeader: result.protectedHeader,
115
- payload: apiTokenPayloadSchema.parse(result.payload) as RequiredKey<
116
- ApiTokenPayload,
117
- C
118
- >,
119
- }
120
- }
121
- }
@@ -1,30 +0,0 @@
1
- import { z } from 'zod'
2
- import {
3
- REFRESH_TOKEN_BYTES_LENGTH,
4
- REFRESH_TOKEN_PREFIX,
5
- } from '../constants.js'
6
- import { randomHexId } from '../lib/util/crypto.js'
7
-
8
- export const REFRESH_TOKEN_LENGTH =
9
- REFRESH_TOKEN_PREFIX.length + REFRESH_TOKEN_BYTES_LENGTH * 2 // hex encoding
10
-
11
- export const refreshTokenSchema = z
12
- .string()
13
- .length(REFRESH_TOKEN_LENGTH)
14
- .refine(
15
- (v): v is `${typeof REFRESH_TOKEN_PREFIX}${string}` =>
16
- v.startsWith(REFRESH_TOKEN_PREFIX),
17
- {
18
- message: `Invalid refresh token format`,
19
- },
20
- )
21
-
22
- export const isRefreshToken = (data: unknown): data is RefreshToken =>
23
- refreshTokenSchema.safeParse(data).success
24
-
25
- export type RefreshToken = z.infer<typeof refreshTokenSchema>
26
- export const generateRefreshToken = async (): Promise<RefreshToken> => {
27
- return `${REFRESH_TOKEN_PREFIX}${await randomHexId(
28
- REFRESH_TOKEN_BYTES_LENGTH,
29
- )}`
30
- }
@@ -1,22 +0,0 @@
1
- import { Did } from '@atproto/did'
2
- import { OAuthScope } from '@atproto/oauth-types'
3
- import { ClientId } from '../client/client-id.js'
4
- import { TokenId } from './token-id.js'
5
-
6
- /**
7
- * The access token claims that will be set by the {@link TokenManager} and that
8
- * will be passed to the "onCreateToken" hook.
9
- *
10
- * @note "iss" is missing here because it cannot be altered and will always be
11
- * set to the Authorization Server's identifier.
12
- */
13
- export type TokenClaims = {
14
- jti: TokenId
15
- sub: Did
16
- iat: number
17
- exp: number
18
- aud: string | [string, ...string[]]
19
- cnf?: { jkt: string }
20
- scope?: OAuthScope
21
- client_id: ClientId
22
- }
@@ -1,40 +0,0 @@
1
- import { Did } from '@atproto/did'
2
- import {
3
- OAuthAuthorizationDetails,
4
- OAuthAuthorizationRequestParameters,
5
- } from '@atproto/oauth-types'
6
- import { ClientAuth, ClientAuthLegacy } from '../client/client-auth.js'
7
- import { ClientId } from '../client/client-id.js'
8
- import { DeviceId } from '../device/device-id.js'
9
- import { Code } from '../request/code.js'
10
-
11
- export type {
12
- ClientAuth,
13
- ClientId,
14
- Code,
15
- DeviceId,
16
- Did,
17
- OAuthAuthorizationDetails,
18
- OAuthAuthorizationRequestParameters,
19
- }
20
-
21
- export type TokenData = {
22
- createdAt: Date
23
- updatedAt: Date
24
- expiresAt: Date
25
- clientId: ClientId
26
- clientAuth: ClientAuth | ClientAuthLegacy
27
- deviceId: DeviceId | null
28
- did: Did
29
- parameters: OAuthAuthorizationRequestParameters
30
- details?: null // Legacy field, not used
31
- code: Code | null
32
-
33
- /**
34
- * This will contain the parameter scope, translated into permissions
35
- *
36
- * @note null because this didn't use to exist. New tokens should always
37
- * include a scope.
38
- */
39
- scope: string | null
40
- }
@@ -1,25 +0,0 @@
1
- import { z } from 'zod'
2
- import { TOKEN_ID_BYTES_LENGTH, TOKEN_ID_PREFIX } from '../constants.js'
3
- import { randomHexId } from '../lib/util/crypto.js'
4
-
5
- export const TOKEN_ID_LENGTH =
6
- TOKEN_ID_PREFIX.length + TOKEN_ID_BYTES_LENGTH * 2 // hex encoding
7
-
8
- export const tokenIdSchema = z
9
- .string()
10
- .length(TOKEN_ID_LENGTH)
11
- .refine(
12
- (v): v is `${typeof TOKEN_ID_PREFIX}${string}` =>
13
- v.startsWith(TOKEN_ID_PREFIX),
14
- {
15
- message: `Invalid token ID format`,
16
- },
17
- )
18
-
19
- export type TokenId = z.infer<typeof tokenIdSchema>
20
- export const generateTokenId = async (): Promise<TokenId> => {
21
- return `${TOKEN_ID_PREFIX}${await randomHexId(TOKEN_ID_BYTES_LENGTH)}`
22
- }
23
-
24
- export const isTokenId = (data: unknown): data is TokenId =>
25
- tokenIdSchema.safeParse(data).success
@@ -1,427 +0,0 @@
1
- import type { Did } from '@atproto/did'
2
- import { SignedJwt, isSignedJwt } from '@atproto/jwk'
3
- import { LexResolverError } from '@atproto/lex-resolver'
4
- import type { Account } from '@atproto/oauth-provider-api'
5
- import {
6
- OAuthAccessToken,
7
- OAuthAuthorizationRequestParameters,
8
- OAuthScope,
9
- OAuthTokenResponse,
10
- OAuthTokenType,
11
- } from '@atproto/oauth-types'
12
- import { AccessTokenMode } from '../access-token/access-token-mode.js'
13
- import { ClientAuth } from '../client/client-auth.js'
14
- import { Client } from '../client/client.js'
15
- import { TOKEN_MAX_AGE } from '../constants.js'
16
- import { DeviceId } from '../device/device-id.js'
17
- import { InvalidGrantError } from '../errors/invalid-grant-error.js'
18
- import { InvalidRequestError } from '../errors/invalid-request-error.js'
19
- import { InvalidTokenError } from '../errors/invalid-token-error.js'
20
- import { LexiconManager } from '../lexicon/lexicon-manager.js'
21
- import { RequestMetadata } from '../lib/http/request.js'
22
- import { dateToEpoch, dateToRelativeSeconds } from '../lib/util/date.js'
23
- import { OAuthHooks } from '../oauth-hooks.js'
24
- import { Code, isCode } from '../request/code.js'
25
- import { AccessTokenPayload } from '../signer/access-token-payload.js'
26
- import { Signer } from '../signer/signer.js'
27
- import {
28
- RefreshToken,
29
- generateRefreshToken,
30
- isRefreshToken,
31
- } from './refresh-token.js'
32
- import { TokenClaims } from './token-claims.js'
33
- import { TokenId, generateTokenId, isTokenId } from './token-id.js'
34
- import { CreateTokenData, TokenInfo, TokenStore } from './token-store.js'
35
-
36
- export { AccessTokenMode, Signer }
37
- export type { OAuthHooks, TokenStore }
38
-
39
- export class TokenManager {
40
- constructor(
41
- protected readonly store: TokenStore,
42
- protected readonly lexiconManager: LexiconManager,
43
- protected readonly signer: Signer,
44
- protected readonly hooks: OAuthHooks,
45
- protected readonly accessTokenMode: AccessTokenMode,
46
- protected readonly tokenMaxAge = TOKEN_MAX_AGE,
47
- ) {}
48
-
49
- protected createTokenExpiry(now = new Date()) {
50
- return new Date(now.getTime() + this.tokenMaxAge)
51
- }
52
-
53
- protected async createAccessToken(
54
- tokenId: TokenId,
55
- client: Client,
56
- account: Account,
57
- parameters: OAuthAuthorizationRequestParameters,
58
- issuedAt: Date,
59
- expiresAt: Date,
60
- scope: OAuthScope,
61
- ): Promise<OAuthAccessToken> {
62
- const claims: TokenClaims = {
63
- jti: tokenId,
64
- sub: account.did,
65
- iat: dateToEpoch(issuedAt),
66
- exp: dateToEpoch(expiresAt),
67
- aud: account.pds,
68
-
69
- ...(parameters.dpop_jkt && {
70
- cnf: { jkt: parameters.dpop_jkt },
71
- }),
72
-
73
- // Because tokens can end-up being quite big, we only include the scope in
74
- // stateless mode.
75
- ...(this.accessTokenMode === AccessTokenMode.stateless && {
76
- scope,
77
- }),
78
-
79
- // https://datatracker.ietf.org/doc/html/rfc8693#section-4.3
80
- client_id: client.id,
81
- }
82
-
83
- const claimsOverride = await this.hooks.onCreateToken?.call(null, {
84
- client,
85
- account,
86
- parameters,
87
- claims,
88
- })
89
-
90
- return this.signer.createAccessToken(claimsOverride ?? claims)
91
- }
92
-
93
- async createToken(
94
- client: Client,
95
- clientAuth: ClientAuth,
96
- clientMetadata: RequestMetadata,
97
- account: Account,
98
- deviceId: null | DeviceId,
99
- parameters: OAuthAuthorizationRequestParameters,
100
- code: Code,
101
- ): Promise<OAuthTokenResponse> {
102
- await this.validateTokenParams(client, clientAuth, parameters)
103
-
104
- const tokenId = await generateTokenId()
105
- const refreshToken = client.metadata.grant_types.includes('refresh_token')
106
- ? await generateRefreshToken()
107
- : undefined
108
-
109
- const now = new Date()
110
- const expiresAt = this.createTokenExpiry(now)
111
-
112
- const scope = await this.lexiconManager
113
- .buildTokenScope(parameters.scope!)
114
- .catch((err) => {
115
- // Parse expected errors
116
- if (err instanceof LexResolverError) {
117
- throw new InvalidRequestError(err.message, err)
118
- }
119
-
120
- // Unexpected error
121
- throw err
122
- })
123
-
124
- const accessToken = await this.createAccessToken(
125
- tokenId,
126
- client,
127
- account,
128
- parameters,
129
- now,
130
- expiresAt,
131
- scope,
132
- )
133
-
134
- const response = this.buildTokenResponse(
135
- inferTokenType(parameters),
136
- accessToken,
137
- refreshToken,
138
- expiresAt,
139
- account.did,
140
- scope,
141
- )
142
-
143
- const tokenData: CreateTokenData = {
144
- createdAt: now,
145
- updatedAt: now,
146
- expiresAt,
147
- clientId: client.id,
148
- clientAuth,
149
- deviceId,
150
- did: account.did,
151
- parameters,
152
- details: null,
153
- scope,
154
- code,
155
- }
156
-
157
- await this.store.createToken(tokenId, tokenData, refreshToken)
158
-
159
- try {
160
- await this.hooks.onTokenCreated?.call(null, {
161
- client,
162
- clientAuth,
163
- clientMetadata,
164
- account,
165
- parameters,
166
- })
167
-
168
- return response
169
- } catch (err) {
170
- // If the hook fails, we delete the token to avoid leaving a dangling
171
- // token in the store.
172
- await this.deleteToken(tokenId)
173
- throw err
174
- }
175
- }
176
-
177
- protected async validateTokenParams(
178
- client: Client,
179
- clientAuth: ClientAuth,
180
- parameters: OAuthAuthorizationRequestParameters,
181
- ): Promise<void> {
182
- if (client.metadata.dpop_bound_access_tokens && !parameters.dpop_jkt) {
183
- throw new InvalidGrantError(
184
- `DPoP JKT is required for DPoP bound access tokens`,
185
- )
186
- }
187
- }
188
-
189
- protected buildTokenResponse(
190
- tokenType: OAuthTokenType,
191
- accessToken: OAuthAccessToken,
192
- refreshToken: string | undefined,
193
- expiresAt: Date,
194
- did: Did,
195
- scope: string,
196
- ): OAuthTokenResponse {
197
- return {
198
- access_token: accessToken,
199
- token_type: tokenType,
200
- refresh_token: refreshToken,
201
- scope,
202
-
203
- // @NOTE using a getter so that the value gets computed when the JSON
204
- // response is generated, allowing to value to be as accurate as possible.
205
- get expires_in() {
206
- return dateToRelativeSeconds(expiresAt)
207
- },
208
-
209
- // ATPROTO extension: add the sub claim to the token response to allow
210
- // clients to resolve the PDS url (audience) using the did resolution
211
- // mechanism.
212
- sub: did,
213
- }
214
- }
215
-
216
- async rotateToken(
217
- client: Client,
218
- clientAuth: ClientAuth,
219
- clientMetadata: RequestMetadata,
220
- tokenInfo: TokenInfo,
221
- ): Promise<OAuthTokenResponse> {
222
- const { account, data } = tokenInfo
223
- const { parameters } = data
224
-
225
- await this.validateTokenParams(client, clientAuth, parameters)
226
-
227
- const nextTokenId = await generateTokenId()
228
- const nextRefreshToken = await generateRefreshToken()
229
-
230
- const now = new Date()
231
- const expiresAt = this.createTokenExpiry(now)
232
-
233
- // @NOTE since the permission sets are stored in a persistent store,
234
- // it's fine to propagate a 500 (server_error) here as the values should
235
- // be retrievable from the store.
236
- const scope = await this.lexiconManager.buildTokenScope(parameters.scope!)
237
-
238
- await this.store.rotateToken(tokenInfo.id, nextTokenId, nextRefreshToken, {
239
- updatedAt: now,
240
- expiresAt,
241
- // @NOTE Normally, the clientAuth not change over time. There are two
242
- // exceptions:
243
- // - Upgrade from a legacy representation of client authentication to
244
- // a modern one.
245
- // - Allow clients to become "confidential" if they were previously
246
- // "public"
247
- clientAuth,
248
- scope,
249
- })
250
-
251
- const accessToken = await this.createAccessToken(
252
- nextTokenId,
253
- client,
254
- account,
255
- parameters,
256
- now,
257
- expiresAt,
258
- scope,
259
- )
260
-
261
- const response = this.buildTokenResponse(
262
- inferTokenType(parameters),
263
- accessToken,
264
- nextRefreshToken,
265
- expiresAt,
266
- account.did,
267
- scope,
268
- )
269
-
270
- await this.hooks.onTokenRefreshed?.call(null, {
271
- client,
272
- clientAuth,
273
- clientMetadata,
274
- account,
275
- parameters,
276
- })
277
-
278
- return response
279
- }
280
-
281
- /**
282
- * @note The token validity is not guaranteed. The caller must ensure that the
283
- * token is valid before using the returned token info.
284
- */
285
- public async findToken(token: string): Promise<null | TokenInfo> {
286
- if (isTokenId(token)) {
287
- return this.getTokenInfo(token)
288
- } else if (isCode(token)) {
289
- return this.findByCode(token)
290
- } else if (isRefreshToken(token)) {
291
- return this.findByRefreshToken(token)
292
- } else if (isSignedJwt(token)) {
293
- return this.findByAccessToken(token)
294
- } else {
295
- throw new InvalidRequestError(`Invalid token`)
296
- }
297
- }
298
-
299
- public async findByAccessToken(token: SignedJwt): Promise<null | TokenInfo> {
300
- const { payload } = await this.signer.verifyAccessToken(token, {
301
- clockTolerance: Infinity,
302
- })
303
-
304
- const tokenInfo = await this.getTokenInfo(payload.jti)
305
- if (!tokenInfo) return null
306
-
307
- // Fool-proof: Invalid store implementation ?
308
- if (payload.sub !== tokenInfo.account.did) {
309
- await this.deleteToken(tokenInfo.id)
310
- throw new Error(
311
- `Account sub (${tokenInfo.account.did}) does not match token sub (${payload.sub})`,
312
- )
313
- }
314
-
315
- return tokenInfo
316
- }
317
-
318
- protected async findByRefreshToken(
319
- token: RefreshToken,
320
- ): Promise<null | TokenInfo> {
321
- return this.store.findTokenByRefreshToken(token)
322
- }
323
-
324
- public async consumeRefreshToken(token: RefreshToken): Promise<TokenInfo> {
325
- // @NOTE concurrent refreshes of the same refresh token could theoretically
326
- // lead to two new tokens (access & refresh) being created. This is deemed
327
- // acceptable for now (as the mechanism can only be used once since only one
328
- // of the two refresh token created will be valid, and any future refresh
329
- // attempts from outdated tokens will cause the entire session to be
330
- // invalidated). Ideally, the store should be able to handle this case by
331
- // atomically consuming the refresh token and returning the token info.
332
-
333
- // @TODO Add another store method that atomically consumes the refresh token
334
- // with a lock.
335
- const tokenInfo = await this.findByRefreshToken(token).catch((err) => {
336
- throw InvalidGrantError.from(err, `Invalid refresh token`)
337
- })
338
-
339
- if (!tokenInfo) {
340
- throw new InvalidGrantError(`Invalid refresh token`)
341
- }
342
-
343
- if (tokenInfo.currentRefreshToken !== token) {
344
- await this.deleteToken(tokenInfo.id)
345
- throw new InvalidGrantError(`Refresh token replayed`)
346
- }
347
-
348
- return tokenInfo
349
- }
350
-
351
- public async findByCode(code: Code): Promise<null | TokenInfo> {
352
- return this.store.findTokenByCode(code)
353
- }
354
-
355
- public async deleteToken(tokenId: TokenId): Promise<void> {
356
- return this.store.deleteToken(tokenId)
357
- }
358
-
359
- async getTokenInfo(tokenId: TokenId): Promise<null | TokenInfo> {
360
- return this.store.readToken(tokenId)
361
- }
362
-
363
- /**
364
- * This method is called to when decoding a token that was encoded in
365
- * {@link AccessTokenMode.light} mode, using data from the store to fill the
366
- * data that was omitted in the token itself.
367
- */
368
- async loadTokenClaims(
369
- tokenType: OAuthTokenType,
370
- tokenPayload: AccessTokenPayload,
371
- ): Promise<TokenClaims> {
372
- const tokenId = tokenPayload.jti
373
- const tokenInfo = await this.getTokenInfo(tokenId).catch((err) => {
374
- throw InvalidTokenError.from(err, tokenType)
375
- })
376
-
377
- if (!tokenInfo) {
378
- throw new InvalidTokenError(tokenType, `Invalid token`)
379
- }
380
-
381
- const { account, data } = tokenInfo
382
-
383
- // Fool proof, make sure that the database & token payload are consistent.
384
- // These should both be either undefined or a string so it's safe to compare
385
- // the values directly.
386
- if (tokenPayload.cnf?.jkt !== data.parameters.dpop_jkt) {
387
- await this.deleteToken(tokenId)
388
- throw new InvalidTokenError(tokenType, `Invalid token`)
389
- }
390
-
391
- if (isCurrentTokenExpired(tokenInfo)) {
392
- await this.deleteToken(tokenId)
393
- throw new InvalidTokenError(tokenType, `Token expired`)
394
- }
395
-
396
- return {
397
- jti: tokenId,
398
- sub: account.did,
399
- iat: dateToEpoch(data.updatedAt),
400
- exp: dateToEpoch(data.expiresAt),
401
- aud: account.pds,
402
- scope: data.scope ?? data.parameters.scope,
403
- // https://datatracker.ietf.org/doc/html/rfc8693#section-4.3
404
- client_id: data.clientId,
405
- }
406
- }
407
-
408
- async listAccountTokens(did: Did): Promise<TokenInfo[]> {
409
- const results = await this.store.listAccountTokens(did)
410
- return results
411
- .filter((tokenInfo) => tokenInfo.account.did === did) // Fool proof
412
- .filter((tokenInfo) => !isCurrentTokenExpired(tokenInfo))
413
- }
414
- }
415
-
416
- function isCurrentTokenExpired(tokenInfo: TokenInfo): boolean {
417
- return tokenInfo.data.expiresAt.getTime() < Date.now()
418
- }
419
-
420
- function inferTokenType(
421
- parameters: OAuthAuthorizationRequestParameters,
422
- ): OAuthTokenType {
423
- if (parameters.dpop_jkt) {
424
- return 'DPoP'
425
- }
426
- return 'Bearer'
427
- }