@atproto/oauth-provider 0.19.6 → 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.
- package/CHANGELOG.md +33 -0
- package/dist/errors/error-parser.js +5 -5
- package/dist/errors/error-parser.js.map +1 -1
- package/package.json +22 -18
- package/src/access-token/access-token-mode.ts +0 -4
- package/src/account/account-manager.ts +0 -591
- package/src/account/account-store.ts +0 -305
- package/src/account/sign-in-data.ts +0 -15
- package/src/account/sign-up-input.ts +0 -20
- package/src/client/client-auth.ts +0 -64
- package/src/client/client-data.ts +0 -9
- package/src/client/client-id.ts +0 -4
- package/src/client/client-info.ts +0 -13
- package/src/client/client-manager.ts +0 -772
- package/src/client/client-store.ts +0 -35
- package/src/client/client-utils.ts +0 -37
- package/src/client/client.ts +0 -394
- package/src/constants.ts +0 -80
- package/src/customization/branding.ts +0 -12
- package/src/customization/build-customization-css.ts +0 -55
- package/src/customization/build-customization-data.ts +0 -24
- package/src/customization/colors.ts +0 -48
- package/src/customization/customization.ts +0 -29
- package/src/customization/links.ts +0 -10
- package/src/device/device-data.ts +0 -11
- package/src/device/device-id.ts +0 -27
- package/src/device/device-manager.ts +0 -292
- package/src/device/device-store.ts +0 -31
- package/src/device/session-id.ts +0 -21
- package/src/dpop/dpop-manager.ts +0 -214
- package/src/dpop/dpop-nonce.ts +0 -121
- package/src/dpop/dpop-proof.ts +0 -6
- package/src/errors/access-denied-error.ts +0 -12
- package/src/errors/account-selection-required-error.ts +0 -12
- package/src/errors/authorization-error.ts +0 -45
- package/src/errors/consent-required-error.ts +0 -12
- package/src/errors/error-parser.ts +0 -147
- package/src/errors/handle-unavailable-error.ts +0 -23
- package/src/errors/invalid-authorization-details-error.ts +0 -27
- package/src/errors/invalid-client-error.ts +0 -20
- package/src/errors/invalid-client-id-error.ts +0 -31
- package/src/errors/invalid-client-metadata-error.ts +0 -68
- package/src/errors/invalid-credentials-error.ts +0 -29
- package/src/errors/invalid-dpop-key-binding-error.ts +0 -21
- package/src/errors/invalid-dpop-proof-error.ts +0 -13
- package/src/errors/invalid-grant-error.ts +0 -21
- package/src/errors/invalid-invite-code-error.ts +0 -10
- package/src/errors/invalid-redirect-uri-error.ts +0 -17
- package/src/errors/invalid-request-error.ts +0 -32
- package/src/errors/invalid-scope-error.ts +0 -15
- package/src/errors/invalid-token-error.ts +0 -60
- package/src/errors/login-required-error.ts +0 -12
- package/src/errors/oauth-error.ts +0 -28
- package/src/errors/second-authentication-factor-required-error.ts +0 -25
- package/src/errors/unauthorized-client-error.ts +0 -20
- package/src/errors/use-dpop-nonce-error.ts +0 -32
- package/src/errors/www-authenticate-error.ts +0 -64
- package/src/index.ts +0 -16
- package/src/lexicon/lexicon-data.ts +0 -11
- package/src/lexicon/lexicon-getter.ts +0 -55
- package/src/lexicon/lexicon-manager.ts +0 -116
- package/src/lexicon/lexicon-store.ts +0 -35
- package/src/lib/csp/index.ts +0 -101
- package/src/lib/hcaptcha.ts +0 -228
- package/src/lib/html/README.md +0 -9
- package/src/lib/html/build-document.ts +0 -151
- package/src/lib/html/escapers.ts +0 -66
- package/src/lib/html/html.ts +0 -56
- package/src/lib/html/hydration-data.ts +0 -19
- package/src/lib/html/index.ts +0 -5
- package/src/lib/html/tags.ts +0 -67
- package/src/lib/html/util.ts +0 -17
- package/src/lib/http/README.md +0 -11
- package/src/lib/http/accept.ts +0 -90
- package/src/lib/http/context.ts +0 -42
- package/src/lib/http/headers.ts +0 -15
- package/src/lib/http/index.ts +0 -10
- package/src/lib/http/method.ts +0 -18
- package/src/lib/http/middleware.ts +0 -169
- package/src/lib/http/parser.ts +0 -101
- package/src/lib/http/path.ts +0 -82
- package/src/lib/http/request.ts +0 -256
- package/src/lib/http/response.ts +0 -122
- package/src/lib/http/route.ts +0 -60
- package/src/lib/http/router.ts +0 -117
- package/src/lib/http/security-headers.ts +0 -100
- package/src/lib/http/stream.ts +0 -57
- package/src/lib/http/types.ts +0 -16
- package/src/lib/http/url.ts +0 -23
- package/src/lib/nsid.ts +0 -10
- package/src/lib/redis.ts +0 -25
- package/src/lib/util/authorization-header.ts +0 -28
- package/src/lib/util/cast.ts +0 -18
- package/src/lib/util/color.ts +0 -168
- package/src/lib/util/crypto.ts +0 -32
- package/src/lib/util/date.ts +0 -7
- package/src/lib/util/error.ts +0 -7
- package/src/lib/util/function.ts +0 -39
- package/src/lib/util/locale.ts +0 -12
- package/src/lib/util/object.ts +0 -23
- package/src/lib/util/redirect-uri.ts +0 -46
- package/src/lib/util/time.ts +0 -49
- package/src/lib/util/type.ts +0 -182
- package/src/lib/util/ui8.ts +0 -14
- package/src/lib/util/well-known.ts +0 -8
- package/src/lib/util/zod-error.ts +0 -26
- package/src/lib/write-form-redirect.ts +0 -58
- package/src/lib/write-html.ts +0 -70
- package/src/metadata/build-metadata.ts +0 -141
- package/src/oauth-client.ts +0 -3
- package/src/oauth-dpop.ts +0 -2
- package/src/oauth-errors.ts +0 -26
- package/src/oauth-hooks.ts +0 -519
- package/src/oauth-middleware.ts +0 -53
- package/src/oauth-provider.ts +0 -1110
- package/src/oauth-store.ts +0 -12
- package/src/oauth-verifier.ts +0 -260
- package/src/replay/replay-manager.ts +0 -51
- package/src/replay/replay-store-memory.ts +0 -36
- package/src/replay/replay-store-redis.ts +0 -30
- package/src/replay/replay-store.ts +0 -44
- package/src/request/code.ts +0 -23
- package/src/request/request-data.ts +0 -36
- package/src/request/request-id.ts +0 -22
- package/src/request/request-manager.ts +0 -521
- package/src/request/request-store.ts +0 -60
- package/src/request/request-uri.ts +0 -42
- package/src/result/authorization-redirect-parameters.ts +0 -24
- package/src/result/authorization-result-authorize-page.ts +0 -16
- package/src/result/authorization-result-redirect.ts +0 -8
- package/src/router/assets/assets-manifest.ts +0 -117
- package/src/router/assets/assets.ts +0 -124
- package/src/router/assets/csrf.ts +0 -79
- package/src/router/assets/send-account-page.ts +0 -23
- package/src/router/assets/send-authorization-page.ts +0 -42
- package/src/router/assets/send-cookie-error-page.ts +0 -21
- package/src/router/assets/send-error-page.ts +0 -25
- package/src/router/assets/send-redirect.ts +0 -140
- package/src/router/create-account-page-middleware.ts +0 -88
- package/src/router/create-api-middleware.ts +0 -1051
- package/src/router/create-authorization-page-middleware.ts +0 -281
- package/src/router/create-oauth-middleware.ts +0 -257
- package/src/router/error-handler.ts +0 -6
- package/src/router/middleware-options.ts +0 -9
- package/src/signer/access-token-payload.ts +0 -25
- package/src/signer/api-token-payload.ts +0 -18
- package/src/signer/signer.ts +0 -121
- package/src/token/refresh-token.ts +0 -30
- package/src/token/token-claims.ts +0 -22
- package/src/token/token-data.ts +0 -40
- package/src/token/token-id.ts +0 -25
- package/src/token/token-manager.ts +0 -427
- package/src/token/token-store.ts +0 -88
- package/src/types/authorization-response-error.ts +0 -27
- package/src/types/color-hue.ts +0 -3
- package/src/types/email-otp.ts +0 -3
- package/src/types/email.ts +0 -26
- package/src/types/handle.ts +0 -23
- package/src/types/invite-code.ts +0 -4
- package/src/types/par-response-error.ts +0 -25
- package/src/types/password.ts +0 -4
- package/src/types/rgb-color.ts +0 -21
- package/tsconfig.build.json +0 -8
- package/tsconfig.build.tsbuildinfo +0 -1
- package/tsconfig.json +0 -4
package/src/oauth-store.ts
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Every store file exports all the types needed to implement that store. This
|
|
3
|
-
* files re-exports all the types from the x-store files.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
export * from './account/account-store.js'
|
|
7
|
-
export * from './client/client-store.js'
|
|
8
|
-
export * from './device/device-store.js'
|
|
9
|
-
export * from './lexicon/lexicon-store.js'
|
|
10
|
-
export * from './replay/replay-store.js'
|
|
11
|
-
export * from './request/request-store.js'
|
|
12
|
-
export * from './token/token-store.js'
|
package/src/oauth-verifier.ts
DELETED
|
@@ -1,260 +0,0 @@
|
|
|
1
|
-
import type { Redis, RedisOptions } from 'ioredis'
|
|
2
|
-
import { Key, Keyset, isSignedJwt } from '@atproto/jwk'
|
|
3
|
-
import {
|
|
4
|
-
OAuthAccessToken,
|
|
5
|
-
OAuthIssuerIdentifier,
|
|
6
|
-
OAuthTokenType,
|
|
7
|
-
oauthIssuerIdentifierSchema,
|
|
8
|
-
} from '@atproto/oauth-types'
|
|
9
|
-
import { DpopManager, DpopManagerOptions } from './dpop/dpop-manager.js'
|
|
10
|
-
import { DpopNonce } from './dpop/dpop-nonce.js'
|
|
11
|
-
import { DpopProof } from './dpop/dpop-proof.js'
|
|
12
|
-
import { InvalidDpopKeyBindingError } from './errors/invalid-dpop-key-binding-error.js'
|
|
13
|
-
import { InvalidDpopProofError } from './errors/invalid-dpop-proof-error.js'
|
|
14
|
-
import { InvalidTokenError } from './errors/invalid-token-error.js'
|
|
15
|
-
import { UseDpopNonceError } from './errors/use-dpop-nonce-error.js'
|
|
16
|
-
import { WWWAuthenticateError } from './errors/www-authenticate-error.js'
|
|
17
|
-
import { parseAuthorizationHeader } from './lib/util/authorization-header.js'
|
|
18
|
-
import { includedIn } from './lib/util/function.js'
|
|
19
|
-
import { OAuthHooks } from './oauth-hooks.js'
|
|
20
|
-
import { ReplayManager } from './replay/replay-manager.js'
|
|
21
|
-
import { ReplayStoreMemory } from './replay/replay-store-memory.js'
|
|
22
|
-
import { ReplayStoreRedis } from './replay/replay-store-redis.js'
|
|
23
|
-
import { ReplayStore } from './replay/replay-store.js'
|
|
24
|
-
import { AccessTokenPayload } from './signer/access-token-payload.js'
|
|
25
|
-
import { Signer } from './signer/signer.js'
|
|
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
|
-
}
|
|
58
|
-
|
|
59
|
-
export { DpopNonce, Key, Keyset }
|
|
60
|
-
export type {
|
|
61
|
-
AccessTokenPayload,
|
|
62
|
-
DpopProof,
|
|
63
|
-
OAuthTokenType,
|
|
64
|
-
RedisOptions,
|
|
65
|
-
ReplayStore,
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
export class OAuthVerifier {
|
|
69
|
-
private readonly onDecodeToken?: DecodeTokenHook
|
|
70
|
-
|
|
71
|
-
public readonly issuer: OAuthIssuerIdentifier
|
|
72
|
-
public readonly keyset: Keyset
|
|
73
|
-
|
|
74
|
-
public readonly dpopManager: DpopManager
|
|
75
|
-
public readonly replayManager: ReplayManager
|
|
76
|
-
public readonly signer: Signer
|
|
77
|
-
|
|
78
|
-
constructor({
|
|
79
|
-
redis,
|
|
80
|
-
issuer,
|
|
81
|
-
keyset,
|
|
82
|
-
replayStore = redis != null
|
|
83
|
-
? new ReplayStoreRedis({ redis })
|
|
84
|
-
: new ReplayStoreMemory(),
|
|
85
|
-
onDecodeToken,
|
|
86
|
-
|
|
87
|
-
...rest
|
|
88
|
-
}: OAuthVerifierOptions) {
|
|
89
|
-
const dpopMgrOptions: DpopManagerOptions = rest
|
|
90
|
-
|
|
91
|
-
const issuerParsed = oauthIssuerIdentifierSchema.parse(issuer)
|
|
92
|
-
const issuerUrl = new URL(issuerParsed)
|
|
93
|
-
|
|
94
|
-
// @TODO (?) support issuer with path
|
|
95
|
-
if (issuerUrl.pathname !== '/') {
|
|
96
|
-
throw new TypeError(
|
|
97
|
-
`"issuer" must be an URL with no path, search or hash (${issuerUrl})`,
|
|
98
|
-
)
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
this.issuer = issuerParsed
|
|
102
|
-
this.keyset = keyset instanceof Keyset ? keyset : new Keyset(keyset)
|
|
103
|
-
|
|
104
|
-
this.dpopManager = new DpopManager(dpopMgrOptions)
|
|
105
|
-
this.replayManager = new ReplayManager(replayStore)
|
|
106
|
-
this.signer = new Signer(this.issuer, this.keyset)
|
|
107
|
-
|
|
108
|
-
this.onDecodeToken = onDecodeToken
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
public nextDpopNonce() {
|
|
112
|
-
return this.dpopManager.nextNonce()
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
public async checkDpopProof(
|
|
116
|
-
httpMethod: string,
|
|
117
|
-
httpUrl: Readonly<URL>,
|
|
118
|
-
httpHeaders: Record<string, undefined | string | string[]>,
|
|
119
|
-
accessToken?: string,
|
|
120
|
-
): Promise<null | DpopProof> {
|
|
121
|
-
const dpopProof = await this.dpopManager.checkProof(
|
|
122
|
-
httpMethod,
|
|
123
|
-
httpUrl,
|
|
124
|
-
httpHeaders,
|
|
125
|
-
accessToken,
|
|
126
|
-
)
|
|
127
|
-
|
|
128
|
-
if (dpopProof) {
|
|
129
|
-
const unique = await this.replayManager.uniqueDpop(dpopProof.jti)
|
|
130
|
-
if (!unique) throw new InvalidDpopProofError('DPoP proof replayed')
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
return dpopProof
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
protected async decodeToken(
|
|
137
|
-
tokenType: OAuthTokenType,
|
|
138
|
-
token: OAuthAccessToken,
|
|
139
|
-
dpopProof: null | DpopProof,
|
|
140
|
-
): Promise<AccessTokenPayload> {
|
|
141
|
-
if (!isSignedJwt(token)) {
|
|
142
|
-
throw new InvalidTokenError(tokenType, `Malformed token`)
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
const { payload } = await this.signer
|
|
146
|
-
.verifyAccessToken(token)
|
|
147
|
-
.catch((err) => {
|
|
148
|
-
throw InvalidTokenError.from(err, tokenType)
|
|
149
|
-
})
|
|
150
|
-
|
|
151
|
-
if (payload.cnf?.jkt) {
|
|
152
|
-
// An access token with a cnf.jkt claim must be a DPoP token
|
|
153
|
-
if (tokenType !== 'DPoP') {
|
|
154
|
-
throw new InvalidTokenError(
|
|
155
|
-
'DPoP',
|
|
156
|
-
`Access token is bound to a DPoP proof, but token type is ${tokenType}`,
|
|
157
|
-
)
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
// DPoP token type must be used with a DPoP proof
|
|
161
|
-
if (!dpopProof) {
|
|
162
|
-
throw new InvalidDpopProofError(`DPoP proof required`)
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
// DPoP proof must be signed with the key that matches the "cnf" claim
|
|
166
|
-
if (payload.cnf.jkt !== dpopProof.jkt) {
|
|
167
|
-
throw new InvalidDpopKeyBindingError()
|
|
168
|
-
}
|
|
169
|
-
} else {
|
|
170
|
-
// An access token without a cnf.jkt claim must be a Bearer token
|
|
171
|
-
if (tokenType !== 'Bearer') {
|
|
172
|
-
throw new InvalidTokenError(
|
|
173
|
-
'Bearer',
|
|
174
|
-
`Bearer token type must be used without a DPoP proof`,
|
|
175
|
-
)
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
// @NOTE We ignore (but allow) DPoP proofs for Bearer tokens
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
const payloadOverride = await this.onDecodeToken?.call(null, {
|
|
182
|
-
tokenType,
|
|
183
|
-
token,
|
|
184
|
-
payload,
|
|
185
|
-
dpopProof,
|
|
186
|
-
})
|
|
187
|
-
|
|
188
|
-
return payloadOverride ?? payload
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
/**
|
|
192
|
-
* @throws {WWWAuthenticateError}
|
|
193
|
-
* @throws {InvalidTokenError}
|
|
194
|
-
*/
|
|
195
|
-
public async authenticateRequest(
|
|
196
|
-
httpMethod: string,
|
|
197
|
-
httpUrl: Readonly<URL>,
|
|
198
|
-
httpHeaders: Record<string, undefined | string | string[]>,
|
|
199
|
-
verifyOptions?: VerifyTokenPayloadOptions,
|
|
200
|
-
): Promise<AccessTokenPayload> {
|
|
201
|
-
const [tokenType, token] = parseAuthorizationHeader(
|
|
202
|
-
httpHeaders['authorization'],
|
|
203
|
-
)
|
|
204
|
-
try {
|
|
205
|
-
const dpopProof = await this.checkDpopProof(
|
|
206
|
-
httpMethod,
|
|
207
|
-
httpUrl,
|
|
208
|
-
httpHeaders,
|
|
209
|
-
token,
|
|
210
|
-
)
|
|
211
|
-
|
|
212
|
-
const tokenPayload = await this.decodeToken(tokenType, token, dpopProof)
|
|
213
|
-
|
|
214
|
-
this.verifyTokenPayload(tokenType, tokenPayload, verifyOptions)
|
|
215
|
-
|
|
216
|
-
return tokenPayload
|
|
217
|
-
} catch (err) {
|
|
218
|
-
if (err instanceof UseDpopNonceError) throw err.toWwwAuthenticateError()
|
|
219
|
-
if (err instanceof WWWAuthenticateError) throw err
|
|
220
|
-
|
|
221
|
-
throw InvalidTokenError.from(err, tokenType)
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
protected verifyTokenPayload(
|
|
226
|
-
tokenType: OAuthTokenType,
|
|
227
|
-
tokenPayload: AccessTokenPayload,
|
|
228
|
-
options?: VerifyTokenPayloadOptions,
|
|
229
|
-
): void {
|
|
230
|
-
if (options?.audience) {
|
|
231
|
-
const { aud } = tokenPayload
|
|
232
|
-
const hasMatch =
|
|
233
|
-
aud != null &&
|
|
234
|
-
(Array.isArray(aud)
|
|
235
|
-
? options.audience.some(includedIn, aud)
|
|
236
|
-
: options.audience.includes(aud))
|
|
237
|
-
if (!hasMatch) {
|
|
238
|
-
const details = `(got: ${aud}, expected one of: ${options.audience})`
|
|
239
|
-
throw new InvalidTokenError(tokenType, `Invalid audience ${details}`)
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
if (options?.scope) {
|
|
244
|
-
const { scope } = tokenPayload
|
|
245
|
-
const scopes = scope?.split(' ')
|
|
246
|
-
if (!scopes || !options.scope.some(includedIn, scopes)) {
|
|
247
|
-
const details = `(got: ${scope}, expected one of: ${options.scope})`
|
|
248
|
-
throw new InvalidTokenError(tokenType, `Invalid scope ${details}`)
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
if (tokenPayload.exp != null && tokenPayload.exp * 1000 <= Date.now()) {
|
|
253
|
-
const expirationDate = new Date(tokenPayload.exp * 1000).toISOString()
|
|
254
|
-
throw new InvalidTokenError(
|
|
255
|
-
tokenType,
|
|
256
|
-
`Token expired at ${expirationDate}`,
|
|
257
|
-
)
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
}
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
import { ClientId } from '../client/client-id.js'
|
|
2
|
-
import {
|
|
3
|
-
CLIENT_ASSERTION_MAX_AGE,
|
|
4
|
-
CODE_CHALLENGE_REPLAY_TIMEFRAME,
|
|
5
|
-
DPOP_NONCE_MAX_AGE,
|
|
6
|
-
JAR_MAX_AGE,
|
|
7
|
-
} from '../constants.js'
|
|
8
|
-
import { ReplayStore } from './replay-store.js'
|
|
9
|
-
|
|
10
|
-
const SECURITY_RATIO = 1.1 // 10% extra time for security
|
|
11
|
-
const asTimeFrame = (timeFrame: number) => Math.ceil(timeFrame * SECURITY_RATIO)
|
|
12
|
-
|
|
13
|
-
export class ReplayManager {
|
|
14
|
-
constructor(protected readonly replayStore: ReplayStore) {}
|
|
15
|
-
|
|
16
|
-
async uniqueAuth(
|
|
17
|
-
jti: string,
|
|
18
|
-
clientId: ClientId,
|
|
19
|
-
exp?: number,
|
|
20
|
-
): Promise<boolean> {
|
|
21
|
-
const timeFrame =
|
|
22
|
-
exp == null
|
|
23
|
-
? asTimeFrame(CLIENT_ASSERTION_MAX_AGE)
|
|
24
|
-
: exp * 1000 - Date.now()
|
|
25
|
-
return this.replayStore.unique(`Auth@${clientId}`, jti, timeFrame)
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
async uniqueJar(jti: string, clientId: ClientId): Promise<boolean> {
|
|
29
|
-
return this.replayStore.unique(
|
|
30
|
-
`JAR@${clientId}`,
|
|
31
|
-
jti,
|
|
32
|
-
asTimeFrame(JAR_MAX_AGE),
|
|
33
|
-
)
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
async uniqueDpop(jti: string, clientId?: ClientId): Promise<boolean> {
|
|
37
|
-
return this.replayStore.unique(
|
|
38
|
-
clientId ? `DPoP@${clientId}` : `DPoP`,
|
|
39
|
-
jti,
|
|
40
|
-
asTimeFrame(DPOP_NONCE_MAX_AGE),
|
|
41
|
-
)
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
async uniqueCodeChallenge(challenge: string): Promise<boolean> {
|
|
45
|
-
return this.replayStore.unique(
|
|
46
|
-
'CodeChallenge',
|
|
47
|
-
challenge,
|
|
48
|
-
asTimeFrame(CODE_CHALLENGE_REPLAY_TIMEFRAME),
|
|
49
|
-
)
|
|
50
|
-
}
|
|
51
|
-
}
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import type { ReplayStore } from './replay-store.js'
|
|
2
|
-
|
|
3
|
-
export class ReplayStoreMemory implements ReplayStore {
|
|
4
|
-
private lastCleanup = Date.now()
|
|
5
|
-
private nonces = new Map<string, number>()
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Returns true if the nonce is unique within the given time frame.
|
|
9
|
-
*/
|
|
10
|
-
async unique(
|
|
11
|
-
namespace: string,
|
|
12
|
-
nonce: string,
|
|
13
|
-
timeFrame: number,
|
|
14
|
-
): Promise<boolean> {
|
|
15
|
-
this.cleanup()
|
|
16
|
-
const key = `${namespace}:${nonce}`
|
|
17
|
-
|
|
18
|
-
const now = Date.now()
|
|
19
|
-
|
|
20
|
-
const exp = this.nonces.get(key)
|
|
21
|
-
this.nonces.set(key, now + timeFrame)
|
|
22
|
-
|
|
23
|
-
return exp == null || exp < now
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
private cleanup() {
|
|
27
|
-
const now = Date.now()
|
|
28
|
-
|
|
29
|
-
if (this.lastCleanup < now - 60_000) {
|
|
30
|
-
for (const [key, expires] of this.nonces) {
|
|
31
|
-
if (expires < now) this.nonces.delete(key)
|
|
32
|
-
}
|
|
33
|
-
this.lastCleanup = now
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
}
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import type { Redis } from 'ioredis'
|
|
2
|
-
import { CreateRedisOptions, createRedis } from '../lib/redis.js'
|
|
3
|
-
import type { ReplayStore } from './replay-store.js'
|
|
4
|
-
|
|
5
|
-
export type { CreateRedisOptions, Redis }
|
|
6
|
-
|
|
7
|
-
export type ReplayStoreRedisOptions = {
|
|
8
|
-
redis: CreateRedisOptions
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export class ReplayStoreRedis implements ReplayStore {
|
|
12
|
-
private readonly redis: Redis
|
|
13
|
-
|
|
14
|
-
constructor(options: ReplayStoreRedisOptions) {
|
|
15
|
-
this.redis = createRedis(options.redis)
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Returns true if the nonce is unique within the given time frame.
|
|
20
|
-
*/
|
|
21
|
-
async unique(
|
|
22
|
-
namespace: string,
|
|
23
|
-
nonce: string,
|
|
24
|
-
timeFrame: number,
|
|
25
|
-
): Promise<boolean> {
|
|
26
|
-
const key = `nonces:${namespace}:${nonce}`
|
|
27
|
-
const prev = await this.redis.set(key, '1', 'PX', timeFrame, 'GET')
|
|
28
|
-
return prev == null
|
|
29
|
-
}
|
|
30
|
-
}
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
import { Awaitable } from '../lib/util/type.js'
|
|
2
|
-
|
|
3
|
-
// Export all types needed to implement the ReplayStore interface
|
|
4
|
-
export type { Awaitable }
|
|
5
|
-
|
|
6
|
-
export interface ReplayStore {
|
|
7
|
-
/**
|
|
8
|
-
* Returns true if the nonce is unique within the given time frame. While not
|
|
9
|
-
* strictly necessary for security purposes, the namespace should be used to
|
|
10
|
-
* mitigate denial of service attacks from one client to the other.
|
|
11
|
-
*
|
|
12
|
-
* @param timeFrame expressed in milliseconds.
|
|
13
|
-
*/
|
|
14
|
-
unique(
|
|
15
|
-
namespace: string,
|
|
16
|
-
nonce: string,
|
|
17
|
-
timeFrame: number,
|
|
18
|
-
): Awaitable<boolean>
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export function isReplayStore(
|
|
22
|
-
implementation: Record<string, unknown> & Partial<ReplayStore>,
|
|
23
|
-
): implementation is Record<string, unknown> & ReplayStore {
|
|
24
|
-
return typeof implementation.unique === 'function'
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export function ifReplayStore(
|
|
28
|
-
implementation?: Record<string, unknown> & Partial<ReplayStore>,
|
|
29
|
-
): ReplayStore | undefined {
|
|
30
|
-
if (implementation && isReplayStore(implementation)) {
|
|
31
|
-
return implementation
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
return undefined
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export function asReplayStore(
|
|
38
|
-
implementation?: Record<string, unknown> & Partial<ReplayStore>,
|
|
39
|
-
): ReplayStore {
|
|
40
|
-
const store = ifReplayStore(implementation)
|
|
41
|
-
if (store) return store
|
|
42
|
-
|
|
43
|
-
throw new Error('Invalid ReplayStore implementation')
|
|
44
|
-
}
|
package/src/request/code.ts
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import { z } from 'zod'
|
|
2
|
-
import { CODE_BYTES_LENGTH, CODE_PREFIX } from '../constants.js'
|
|
3
|
-
import { randomHexId } from '../lib/util/crypto.js'
|
|
4
|
-
|
|
5
|
-
export const CODE_LENGTH = CODE_PREFIX.length + CODE_BYTES_LENGTH * 2 // hex encoding
|
|
6
|
-
|
|
7
|
-
export const codeSchema = z
|
|
8
|
-
.string()
|
|
9
|
-
.length(CODE_LENGTH) // hex encoding
|
|
10
|
-
.refine(
|
|
11
|
-
(v): v is `${typeof CODE_PREFIX}${string}` => v.startsWith(CODE_PREFIX),
|
|
12
|
-
{
|
|
13
|
-
message: `Invalid code format`,
|
|
14
|
-
},
|
|
15
|
-
)
|
|
16
|
-
|
|
17
|
-
export const isCode = (data: unknown): data is Code =>
|
|
18
|
-
codeSchema.safeParse(data).success
|
|
19
|
-
|
|
20
|
-
export type Code = z.infer<typeof codeSchema>
|
|
21
|
-
export const generateCode = async (): Promise<Code> => {
|
|
22
|
-
return `${CODE_PREFIX}${await randomHexId(CODE_BYTES_LENGTH)}`
|
|
23
|
-
}
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import { Did } from '@atproto/did'
|
|
2
|
-
import { OAuthAuthorizationRequestParameters } from '@atproto/oauth-types'
|
|
3
|
-
import { ClientAuth, ClientAuthLegacy } from '../client/client-auth.js'
|
|
4
|
-
import { ClientId } from '../client/client-id.js'
|
|
5
|
-
import { DeviceId } from '../device/device-id.js'
|
|
6
|
-
import { NonNullableKeys } from '../lib/util/type.js'
|
|
7
|
-
import { Code } from './code.js'
|
|
8
|
-
|
|
9
|
-
export type {
|
|
10
|
-
ClientAuth,
|
|
11
|
-
ClientAuthLegacy,
|
|
12
|
-
ClientId,
|
|
13
|
-
Code,
|
|
14
|
-
DeviceId,
|
|
15
|
-
Did,
|
|
16
|
-
OAuthAuthorizationRequestParameters,
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export type RequestData = {
|
|
20
|
-
clientId: ClientId
|
|
21
|
-
clientAuth: null | ClientAuth | ClientAuthLegacy
|
|
22
|
-
parameters: Readonly<OAuthAuthorizationRequestParameters>
|
|
23
|
-
expiresAt: Date
|
|
24
|
-
deviceId: DeviceId | null
|
|
25
|
-
did: Did | null
|
|
26
|
-
code: Code | null
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export type RequestDataAuthorized = NonNullableKeys<
|
|
30
|
-
RequestData,
|
|
31
|
-
'did' | 'deviceId'
|
|
32
|
-
>
|
|
33
|
-
|
|
34
|
-
export const isRequestDataAuthorized = (
|
|
35
|
-
data: RequestData,
|
|
36
|
-
): data is RequestDataAuthorized => data.did !== null && data.deviceId !== null
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import { z } from 'zod'
|
|
2
|
-
import { REQUEST_ID_BYTES_LENGTH, REQUEST_ID_PREFIX } from '../constants.js'
|
|
3
|
-
import { randomHexId } from '../lib/util/crypto.js'
|
|
4
|
-
|
|
5
|
-
export const REQUEST_ID_LENGTH =
|
|
6
|
-
REQUEST_ID_PREFIX.length + REQUEST_ID_BYTES_LENGTH * 2 // hex encoding
|
|
7
|
-
|
|
8
|
-
export const requestIdSchema = z
|
|
9
|
-
.string()
|
|
10
|
-
.length(REQUEST_ID_LENGTH)
|
|
11
|
-
.refine(
|
|
12
|
-
(v): v is `${typeof REQUEST_ID_PREFIX}${string}` =>
|
|
13
|
-
v.startsWith(REQUEST_ID_PREFIX),
|
|
14
|
-
{
|
|
15
|
-
message: `Invalid request ID format`,
|
|
16
|
-
},
|
|
17
|
-
)
|
|
18
|
-
|
|
19
|
-
export type RequestId = z.infer<typeof requestIdSchema>
|
|
20
|
-
export const generateRequestId = async (): Promise<RequestId> => {
|
|
21
|
-
return `${REQUEST_ID_PREFIX}${await randomHexId(REQUEST_ID_BYTES_LENGTH)}`
|
|
22
|
-
}
|