@atproto/oauth-provider 0.2.16 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- package/CHANGELOG.md +34 -0
- package/dist/account/account-store.d.ts +1 -1
- package/dist/account/account-store.d.ts.map +1 -1
- package/dist/account/account-store.js +6 -9
- package/dist/account/account-store.js.map +1 -1
- package/dist/account/account.d.ts +1 -1
- package/dist/account/account.d.ts.map +1 -1
- package/dist/assets/app/bundle-manifest.json +2 -2
- package/dist/assets/app/main.js +1 -1
- package/dist/assets/app/main.js.map +1 -1
- package/dist/assets/assets-middleware.d.ts.map +1 -1
- package/dist/assets/assets-middleware.js.map +1 -1
- package/dist/assets/index.d.ts.map +1 -1
- package/dist/assets/index.js +7 -6
- package/dist/assets/index.js.map +1 -1
- package/dist/client/client-auth.d.ts +1 -1
- package/dist/client/client-auth.d.ts.map +1 -1
- package/dist/client/client-auth.js +1 -1
- package/dist/client/client-auth.js.map +1 -1
- package/dist/client/client-manager.d.ts +2 -2
- package/dist/client/client-manager.d.ts.map +1 -1
- package/dist/client/client-manager.js +8 -10
- package/dist/client/client-manager.js.map +1 -1
- package/dist/client/client-store.d.ts.map +1 -1
- package/dist/client/client-store.js.map +1 -1
- package/dist/client/client-utils.d.ts.map +1 -1
- package/dist/client/client-utils.js.map +1 -1
- package/dist/client/client.d.ts +1 -1
- package/dist/client/client.d.ts.map +1 -1
- package/dist/client/client.js +1 -1
- package/dist/client/client.js.map +1 -1
- package/dist/device/device-data.d.ts +7 -8
- package/dist/device/device-data.d.ts.map +1 -1
- package/dist/device/device-data.js +3 -2
- package/dist/device/device-data.js.map +1 -1
- package/dist/device/device-id.d.ts.map +1 -1
- package/dist/device/device-id.js.map +1 -1
- package/dist/device/device-manager.d.ts +104 -19
- package/dist/device/device-manager.d.ts.map +1 -1
- package/dist/device/device-manager.js +44 -31
- package/dist/device/device-manager.js.map +1 -1
- package/dist/device/session-id.d.ts.map +1 -1
- package/dist/device/session-id.js.map +1 -1
- package/dist/dpop/dpop-manager.d.ts.map +1 -1
- package/dist/dpop/dpop-manager.js.map +1 -1
- package/dist/dpop/dpop-nonce.d.ts.map +1 -1
- package/dist/dpop/dpop-nonce.js.map +1 -1
- package/dist/errors/invalid-client-metadata-error.js +1 -1
- package/dist/errors/invalid-client-metadata-error.js.map +1 -1
- package/dist/errors/invalid-token-error.d.ts.map +1 -1
- package/dist/errors/invalid-token-error.js +1 -1
- package/dist/errors/invalid-token-error.js.map +1 -1
- package/dist/errors/www-authenticate-error.d.ts.map +1 -1
- package/dist/errors/www-authenticate-error.js.map +1 -1
- package/dist/lib/http/accept.d.ts +2 -1
- package/dist/lib/http/accept.d.ts.map +1 -1
- package/dist/lib/http/accept.js.map +1 -1
- package/dist/lib/http/method.d.ts +1 -1
- package/dist/lib/http/method.d.ts.map +1 -1
- package/dist/lib/http/middleware.d.ts +1 -1
- package/dist/lib/http/middleware.d.ts.map +1 -1
- package/dist/lib/http/request.d.ts +10 -1
- package/dist/lib/http/request.d.ts.map +1 -1
- package/dist/lib/http/request.js +40 -2
- package/dist/lib/http/request.js.map +1 -1
- package/dist/lib/http/response.d.ts +3 -2
- package/dist/lib/http/response.d.ts.map +1 -1
- package/dist/lib/http/response.js.map +1 -1
- package/dist/lib/http/route.d.ts +2 -1
- package/dist/lib/http/route.d.ts.map +1 -1
- package/dist/lib/http/route.js.map +1 -1
- package/dist/lib/http/router.d.ts +2 -1
- package/dist/lib/http/router.d.ts.map +1 -1
- package/dist/lib/http/router.js.map +1 -1
- package/dist/lib/http/stream.d.ts +1 -1
- package/dist/lib/http/stream.d.ts.map +1 -1
- package/dist/lib/http/stream.js +1 -1
- package/dist/lib/http/stream.js.map +1 -1
- package/dist/lib/http/types.d.ts +0 -1
- package/dist/lib/http/types.d.ts.map +1 -1
- package/dist/lib/util/authorization-header.d.ts.map +1 -1
- package/dist/lib/util/authorization-header.js +1 -1
- package/dist/lib/util/authorization-header.js.map +1 -1
- package/dist/lib/util/function.d.ts +8 -0
- package/dist/lib/util/function.d.ts.map +1 -1
- package/dist/lib/util/function.js +1 -1
- package/dist/lib/util/function.js.map +1 -1
- package/dist/lib/util/hostname.d.ts.map +1 -1
- package/dist/lib/util/time.d.ts +7 -1
- package/dist/lib/util/time.d.ts.map +1 -1
- package/dist/lib/util/time.js +23 -12
- package/dist/lib/util/time.js.map +1 -1
- package/dist/metadata/build-metadata.d.ts.map +1 -1
- package/dist/metadata/build-metadata.js.map +1 -1
- package/dist/oauth-hooks.d.ts +56 -4
- package/dist/oauth-hooks.d.ts.map +1 -1
- package/dist/oauth-hooks.js +8 -0
- package/dist/oauth-hooks.js.map +1 -1
- package/dist/oauth-provider.d.ts +13 -10
- package/dist/oauth-provider.d.ts.map +1 -1
- package/dist/oauth-provider.js +36 -58
- package/dist/oauth-provider.js.map +1 -1
- package/dist/oauth-verifier.d.ts +1 -1
- package/dist/oauth-verifier.d.ts.map +1 -1
- package/dist/oauth-verifier.js.map +1 -1
- package/dist/output/build-authorize-data.d.ts.map +1 -1
- package/dist/output/build-authorize-data.js.map +1 -1
- package/dist/output/build-error-payload.d.ts.map +1 -1
- package/dist/output/build-error-payload.js +1 -1
- package/dist/output/build-error-payload.js.map +1 -1
- package/dist/output/output-manager.d.ts +1 -1
- package/dist/output/output-manager.d.ts.map +1 -1
- package/dist/output/output-manager.js.map +1 -1
- package/dist/output/send-authorize-redirect.d.ts +1 -1
- package/dist/output/send-authorize-redirect.d.ts.map +1 -1
- package/dist/output/send-authorize-redirect.js.map +1 -1
- package/dist/output/send-web-page.d.ts +1 -1
- package/dist/output/send-web-page.d.ts.map +1 -1
- package/dist/output/send-web-page.js.map +1 -1
- package/dist/replay/replay-store-redis.d.ts.map +1 -1
- package/dist/replay/replay-store-redis.js.map +1 -1
- package/dist/request/code.d.ts.map +1 -1
- package/dist/request/code.js.map +1 -1
- package/dist/request/request-data.d.ts.map +1 -1
- package/dist/request/request-data.js.map +1 -1
- package/dist/request/request-id.d.ts.map +1 -1
- package/dist/request/request-id.js.map +1 -1
- package/dist/request/request-info.d.ts +1 -1
- package/dist/request/request-info.d.ts.map +1 -1
- package/dist/request/request-manager.d.ts +2 -1
- package/dist/request/request-manager.d.ts.map +1 -1
- package/dist/request/request-manager.js +10 -2
- package/dist/request/request-manager.js.map +1 -1
- package/dist/request/request-store-memory.d.ts +1 -1
- package/dist/request/request-store-memory.d.ts.map +1 -1
- package/dist/request/request-store-redis.d.ts +1 -1
- package/dist/request/request-store-redis.d.ts.map +1 -1
- package/dist/request/request-store-redis.js.map +1 -1
- package/dist/request/request-uri.d.ts.map +1 -1
- package/dist/request/request-uri.js.map +1 -1
- package/dist/signer/signed-token-payload.d.ts +1 -1
- package/dist/signer/signed-token-payload.d.ts.map +1 -1
- package/dist/signer/signed-token-payload.js +2 -5
- package/dist/signer/signed-token-payload.js.map +1 -1
- package/dist/signer/signer.d.ts +1 -1
- package/dist/signer/signer.d.ts.map +1 -1
- package/dist/signer/signer.js.map +1 -1
- package/dist/token/refresh-token.d.ts.map +1 -1
- package/dist/token/refresh-token.js.map +1 -1
- package/dist/token/token-claims.d.ts +1 -1
- package/dist/token/token-claims.d.ts.map +1 -1
- package/dist/token/token-claims.js +2 -5
- package/dist/token/token-claims.js.map +1 -1
- package/dist/token/token-data.d.ts.map +1 -1
- package/dist/token/token-id.d.ts.map +1 -1
- package/dist/token/token-id.js.map +1 -1
- package/dist/token/token-manager.d.ts +3 -2
- package/dist/token/token-manager.d.ts.map +1 -1
- package/dist/token/token-manager.js +40 -25
- package/dist/token/token-manager.js.map +1 -1
- package/dist/token/verify-token-claims.d.ts.map +1 -1
- package/dist/token/verify-token-claims.js.map +1 -1
- package/package.json +13 -12
- package/rollup.config.js +4 -5
- package/src/account/account-store.ts +1 -2
- package/src/account/account.ts +1 -1
- package/src/assets/app/hooks/use-api.ts +1 -2
- package/src/assets/app/lib/api.ts +0 -1
- package/src/assets/assets-middleware.ts +0 -1
- package/src/assets/index.ts +2 -3
- package/src/client/client-auth.ts +1 -2
- package/src/client/client-manager.ts +22 -25
- package/src/client/client-store.ts +0 -1
- package/src/client/client-utils.ts +0 -1
- package/src/client/client.ts +13 -14
- package/src/device/device-data.ts +3 -3
- package/src/device/device-id.ts +0 -1
- package/src/device/device-manager.ts +94 -79
- package/src/device/session-id.ts +0 -1
- package/src/dpop/dpop-manager.ts +0 -2
- package/src/dpop/dpop-nonce.ts +0 -1
- package/src/errors/invalid-client-metadata-error.ts +1 -1
- package/src/errors/invalid-token-error.ts +1 -2
- package/src/errors/www-authenticate-error.ts +0 -1
- package/src/lib/http/accept.ts +2 -7
- package/src/lib/http/method.ts +1 -1
- package/src/lib/http/middleware.ts +1 -1
- package/src/lib/http/request.ts +66 -4
- package/src/lib/http/response.ts +3 -3
- package/src/lib/http/route.ts +2 -1
- package/src/lib/http/router.ts +2 -1
- package/src/lib/http/stream.ts +4 -5
- package/src/lib/http/types.ts +0 -1
- package/src/lib/util/authorization-header.ts +1 -2
- package/src/lib/util/function.ts +19 -2
- package/src/lib/util/hostname.ts +1 -1
- package/src/lib/util/time.ts +35 -18
- package/src/metadata/build-metadata.ts +0 -1
- package/src/oauth-hooks.ts +73 -14
- package/src/oauth-provider.ts +77 -37
- package/src/oauth-verifier.ts +1 -2
- package/src/output/build-authorize-data.ts +0 -1
- package/src/output/build-error-payload.ts +1 -2
- package/src/output/output-manager.ts +3 -4
- package/src/output/send-authorize-redirect.ts +1 -2
- package/src/output/send-web-page.ts +3 -4
- package/src/replay/replay-manager.ts +1 -1
- package/src/replay/replay-store-redis.ts +0 -1
- package/src/request/code.ts +0 -1
- package/src/request/request-data.ts +0 -1
- package/src/request/request-id.ts +0 -1
- package/src/request/request-info.ts +1 -1
- package/src/request/request-manager.ts +16 -6
- package/src/request/request-store-memory.ts +1 -1
- package/src/request/request-store-redis.ts +1 -2
- package/src/request/request-uri.ts +0 -1
- package/src/signer/signed-token-payload.ts +1 -2
- package/src/signer/signer.ts +1 -2
- package/src/token/refresh-token.ts +0 -1
- package/src/token/token-claims.ts +1 -2
- package/src/token/token-data.ts +0 -1
- package/src/token/token-id.ts +0 -1
- package/src/token/token-manager.ts +53 -25
- package/src/token/verify-token-claims.ts +0 -1
- package/tsconfig.backend.tsbuildinfo +1 -1
- package/dist/device/device-details.d.ts +0 -16
- package/dist/device/device-details.d.ts.map +0 -1
- package/dist/device/device-details.js +0 -34
- package/dist/device/device-details.js.map +0 -1
- package/src/device/device-details.ts +0 -43
package/src/lib/util/function.ts
CHANGED
@@ -1,7 +1,24 @@
|
|
1
|
+
/**
|
2
|
+
* This function serves two purposes:
|
3
|
+
* - It ensures that the return value is a Promise, even if the function returns
|
4
|
+
* a "thenable" (i.e. a Promise-like object).
|
5
|
+
* - It allows to avoid assigning a `this` context to the function, which is
|
6
|
+
* particularly useful when the function is a member of a "private" object.
|
7
|
+
*/
|
1
8
|
export async function callAsync<F extends (...args: any[]) => unknown>(
|
2
9
|
this: ThisParameterType<F>,
|
3
10
|
fn: F,
|
4
11
|
...args: Parameters<F>
|
5
|
-
): Promise<Awaited<ReturnType<F>>>
|
6
|
-
|
12
|
+
): Promise<Awaited<ReturnType<F>>>
|
13
|
+
export async function callAsync<F extends (...args: any[]) => unknown>(
|
14
|
+
this: ThisParameterType<F>,
|
15
|
+
fn?: F,
|
16
|
+
...args: Parameters<F>
|
17
|
+
): Promise<Awaited<ReturnType<F>> | undefined>
|
18
|
+
export async function callAsync<F extends (...args: any[]) => unknown>(
|
19
|
+
this: ThisParameterType<F>,
|
20
|
+
fn?: F,
|
21
|
+
...args: Parameters<F>
|
22
|
+
): Promise<Awaited<ReturnType<F>> | undefined> {
|
23
|
+
return (await fn?.(...args)) as Awaited<ReturnType<F>> | undefined
|
7
24
|
}
|
package/src/lib/util/hostname.ts
CHANGED
package/src/lib/util/time.ts
CHANGED
@@ -1,33 +1,50 @@
|
|
1
|
+
import { setTimeout as sleep } from 'node:timers/promises'
|
1
2
|
import { Awaitable } from './type.js'
|
2
3
|
|
4
|
+
export function onOvertimeDefault(options: {
|
5
|
+
start: number
|
6
|
+
end: number
|
7
|
+
elapsed: number
|
8
|
+
time: number
|
9
|
+
}): void {
|
10
|
+
console.warn(
|
11
|
+
`constantTime: execution time was ${options.elapsed}ms (which is greater than ${options.time}ms). You should increase the "time" to properly defend against timing attacks.`,
|
12
|
+
)
|
13
|
+
}
|
14
|
+
|
3
15
|
/**
|
4
16
|
* Utility function to protect against timing attacks.
|
5
17
|
*/
|
6
|
-
export async function constantTime<T>(
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
18
|
+
export async function constantTime<R, T = unknown>(
|
19
|
+
this: T,
|
20
|
+
time: number,
|
21
|
+
fn: (this: T) => Awaitable<R>,
|
22
|
+
onOvertime = onOvertimeDefault,
|
23
|
+
): Promise<R> {
|
24
|
+
if (!Number.isFinite(time) || time <= 0) {
|
25
|
+
throw new TypeError(`"time" must be a positive number`)
|
12
26
|
}
|
13
27
|
|
14
28
|
const start = Date.now()
|
15
29
|
try {
|
16
|
-
return await fn()
|
30
|
+
return await fn.call(this)
|
17
31
|
} finally {
|
18
|
-
const
|
32
|
+
const end = Date.now()
|
33
|
+
const elapsed = end - start
|
19
34
|
|
20
|
-
|
21
|
-
|
35
|
+
const remaining = time - elapsed
|
36
|
+
if (remaining >= 0) {
|
37
|
+
// Happy path, execution time was smaller than "time"
|
38
|
+
await sleep(remaining)
|
39
|
+
} else {
|
40
|
+
// The function execution took longer than "time"
|
41
|
+
onOvertime({ start, end, elapsed, time })
|
22
42
|
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
console.warn(
|
27
|
-
`constantTime: execution time was ${delta}ms, waiting for the next multiple of ${delay}ms. You should increase the delay to properly defend against timing attacks.`,
|
28
|
-
)
|
29
|
-
}
|
43
|
+
// Sleep until the next multiple of "time" to mitigate any attack
|
44
|
+
const multiplier = Math.ceil(elapsed / time)
|
45
|
+
const remaining = multiplier * time - elapsed
|
30
46
|
|
31
|
-
|
47
|
+
await sleep(remaining)
|
48
|
+
}
|
32
49
|
}
|
33
50
|
}
|
package/src/oauth-hooks.ts
CHANGED
@@ -5,28 +5,35 @@ import {
|
|
5
5
|
OAuthClientMetadata,
|
6
6
|
OAuthTokenResponse,
|
7
7
|
} from '@atproto/oauth-types'
|
8
|
-
|
9
8
|
import { Account } from './account/account.js'
|
10
9
|
import { ClientAuth } from './client/client-auth.js'
|
11
10
|
import { ClientId } from './client/client-id.js'
|
12
11
|
import { ClientInfo } from './client/client-info.js'
|
13
12
|
import { Client } from './client/client.js'
|
14
13
|
import { InvalidAuthorizationDetailsError } from './errors/invalid-authorization-details-error.js'
|
14
|
+
import { RequestMetadata } from './lib/http/request.js'
|
15
15
|
import { Awaitable } from './lib/util/type.js'
|
16
|
+
import { AccessDeniedError, OAuthError } from './oauth-errors.js'
|
17
|
+
import { DeviceId } from './oauth-store.js'
|
16
18
|
|
17
19
|
// Make sure all types needed to implement the OAuthHooks are exported
|
18
|
-
export
|
19
|
-
|
20
|
+
export {
|
21
|
+
AccessDeniedError,
|
22
|
+
type Account,
|
23
|
+
type Awaitable,
|
20
24
|
Client,
|
21
|
-
ClientAuth,
|
22
|
-
ClientId,
|
23
|
-
ClientInfo,
|
25
|
+
type ClientAuth,
|
26
|
+
type ClientId,
|
27
|
+
type ClientInfo,
|
28
|
+
type DeviceId,
|
24
29
|
InvalidAuthorizationDetailsError,
|
25
|
-
Jwks,
|
26
|
-
OAuthAuthorizationDetails,
|
27
|
-
OAuthAuthorizationRequestParameters,
|
28
|
-
OAuthClientMetadata,
|
29
|
-
|
30
|
+
type Jwks,
|
31
|
+
type OAuthAuthorizationDetails,
|
32
|
+
type OAuthAuthorizationRequestParameters,
|
33
|
+
type OAuthClientMetadata,
|
34
|
+
OAuthError,
|
35
|
+
type OAuthTokenResponse,
|
36
|
+
type RequestMetadata,
|
30
37
|
}
|
31
38
|
|
32
39
|
export type OAuthHooks = {
|
@@ -37,10 +44,10 @@ export type OAuthHooks = {
|
|
37
44
|
* @throws {InvalidClientMetadataError} if the metadata is invalid
|
38
45
|
* @see {@link InvalidClientMetadataError}
|
39
46
|
*/
|
40
|
-
|
47
|
+
getClientInfo?: (
|
41
48
|
clientId: ClientId,
|
42
49
|
data: { metadata: OAuthClientMetadata; jwks?: Jwks },
|
43
|
-
) => Awaitable<
|
50
|
+
) => Awaitable<undefined | Partial<ClientInfo>>
|
44
51
|
|
45
52
|
/**
|
46
53
|
* Allows enriching the authorization details with additional information
|
@@ -48,9 +55,61 @@ export type OAuthHooks = {
|
|
48
55
|
*
|
49
56
|
* @see {@link https://datatracker.ietf.org/doc/html/rfc9396 | RFC 9396}
|
50
57
|
*/
|
51
|
-
|
58
|
+
getAuthorizationDetails?: (data: {
|
52
59
|
client: Client
|
60
|
+
clientAuth: ClientAuth
|
61
|
+
clientMetadata: RequestMetadata
|
53
62
|
parameters: OAuthAuthorizationRequestParameters
|
54
63
|
account: Account
|
55
64
|
}) => Awaitable<undefined | OAuthAuthorizationDetails>
|
65
|
+
|
66
|
+
/**
|
67
|
+
* This hook is called when a client is authorized.
|
68
|
+
*
|
69
|
+
* @throws {AccessDeniedError} to deny the authorization request and redirect
|
70
|
+
* the user to the client with an OAuth error (other errors will result in an
|
71
|
+
* internal server error being displayed to the user)
|
72
|
+
*
|
73
|
+
* @note We use `deviceMetadata` instead of `clientMetadata` to make it clear
|
74
|
+
* that this metadata is from the user device, which might be different from
|
75
|
+
* the client metadata (because the OAuth client could live in a backend).
|
76
|
+
*/
|
77
|
+
onAuthorized?: (data: {
|
78
|
+
client: Client
|
79
|
+
account: Account
|
80
|
+
parameters: OAuthAuthorizationRequestParameters
|
81
|
+
deviceId: DeviceId
|
82
|
+
deviceMetadata: RequestMetadata
|
83
|
+
}) => Awaitable<void>
|
84
|
+
|
85
|
+
/**
|
86
|
+
* This hook is called when an authorized client exchanges an authorization
|
87
|
+
* code for an access token.
|
88
|
+
*
|
89
|
+
* @throws {OAuthError} to cancel the token creation and revoke the session
|
90
|
+
*/
|
91
|
+
onTokenCreated?: (data: {
|
92
|
+
client: Client
|
93
|
+
clientAuth: ClientAuth
|
94
|
+
clientMetadata: RequestMetadata
|
95
|
+
account: Account
|
96
|
+
parameters: OAuthAuthorizationRequestParameters
|
97
|
+
/** null when "password grant" used (in which case {@link onAuthorized} won't have been called) */
|
98
|
+
deviceId: null | DeviceId
|
99
|
+
}) => Awaitable<void>
|
100
|
+
|
101
|
+
/**
|
102
|
+
* This hook is called when an authorized client refreshes an access token.
|
103
|
+
*
|
104
|
+
* @throws {OAuthError} to cancel the token refresh and revoke the session
|
105
|
+
*/
|
106
|
+
onTokenRefreshed?: (data: {
|
107
|
+
client: Client
|
108
|
+
clientAuth: ClientAuth
|
109
|
+
clientMetadata: RequestMetadata
|
110
|
+
account: Account
|
111
|
+
parameters: OAuthAuthorizationRequestParameters
|
112
|
+
/** null when "password grant" used (in which case {@link onAuthorized} won't have been called) */
|
113
|
+
deviceId: null | DeviceId
|
114
|
+
}) => Awaitable<void>
|
56
115
|
}
|
package/src/oauth-provider.ts
CHANGED
@@ -1,6 +1,8 @@
|
|
1
|
-
import {
|
2
|
-
import {
|
3
|
-
import
|
1
|
+
import type { IncomingMessage, ServerResponse } from 'node:http'
|
2
|
+
import { mediaType } from '@hapi/accept'
|
3
|
+
import createHttpError from 'http-errors'
|
4
|
+
import type { Redis, RedisOptions } from 'ioredis'
|
5
|
+
import { ZodError, z } from 'zod'
|
4
6
|
import { Jwks, Keyset } from '@atproto/jwk'
|
5
7
|
import {
|
6
8
|
CLIENT_ASSERTION_TYPE_JWT_BEARER,
|
@@ -29,11 +31,9 @@ import {
|
|
29
31
|
oauthTokenIdentificationSchema,
|
30
32
|
oauthTokenRequestSchema,
|
31
33
|
} from '@atproto/oauth-types'
|
32
|
-
import {
|
33
|
-
import
|
34
|
-
import
|
35
|
-
import z, { ZodError } from 'zod'
|
36
|
-
|
34
|
+
import { safeFetchWrap } from '@atproto-labs/fetch-node'
|
35
|
+
import { SimpleStore } from '@atproto-labs/simple-store'
|
36
|
+
import { SimpleStoreMemory } from '@atproto-labs/simple-store-memory'
|
37
37
|
import { AccessTokenType } from './access-token/access-token-type.js'
|
38
38
|
import { AccountManager } from './account/account-manager.js'
|
39
39
|
import {
|
@@ -55,7 +55,7 @@ import { ClientStore, ifClientStore } from './client/client-store.js'
|
|
55
55
|
import { Client } from './client/client.js'
|
56
56
|
import { AUTHENTICATION_MAX_AGE, TOKEN_MAX_AGE } from './constants.js'
|
57
57
|
import { DeviceId } from './device/device-id.js'
|
58
|
-
import { DeviceManager } from './device/device-manager.js'
|
58
|
+
import { DeviceManager, DeviceManagerOptions } from './device/device-manager.js'
|
59
59
|
import { DeviceStore, asDeviceStore } from './device/device-store.js'
|
60
60
|
import { AccessDeniedError } from './errors/access-denied-error.js'
|
61
61
|
import { AccountSelectionRequiredError } from './errors/account-selection-required-error.js'
|
@@ -70,10 +70,8 @@ import { UnauthorizedClientError } from './errors/unauthorized-client-error.js'
|
|
70
70
|
import { WWWAuthenticateError } from './errors/www-authenticate-error.js'
|
71
71
|
import {
|
72
72
|
Handler,
|
73
|
-
IncomingMessage,
|
74
73
|
Middleware,
|
75
74
|
Router,
|
76
|
-
ServerResponse,
|
77
75
|
combineMiddlewares,
|
78
76
|
parseHttpRequest,
|
79
77
|
setupCsrfToken,
|
@@ -86,6 +84,7 @@ import {
|
|
86
84
|
validateSameOrigin,
|
87
85
|
writeJson,
|
88
86
|
} from './lib/http/index.js'
|
87
|
+
import { RequestMetadata } from './lib/http/request.js'
|
89
88
|
import { dateToEpoch, dateToRelativeSeconds } from './lib/util/date.js'
|
90
89
|
import { Override } from './lib/util/type.js'
|
91
90
|
import { CustomMetadata, buildMetadata } from './metadata/build-metadata.js'
|
@@ -125,17 +124,17 @@ export type OAuthProviderStore = Partial<
|
|
125
124
|
>
|
126
125
|
|
127
126
|
export {
|
128
|
-
Keyset,
|
129
127
|
type CustomMetadata,
|
130
128
|
type Customization,
|
131
129
|
type Handler,
|
130
|
+
Keyset,
|
132
131
|
type OAuthAuthorizationServerMetadata,
|
133
132
|
}
|
134
133
|
|
135
134
|
export type RouterOptions<
|
136
135
|
Req extends IncomingMessage = IncomingMessage,
|
137
136
|
Res extends ServerResponse = ServerResponse,
|
138
|
-
> = {
|
137
|
+
> = DeviceManagerOptions & {
|
139
138
|
onError?: (req: Req, res: Res, err: unknown, message: string) => void
|
140
139
|
}
|
141
140
|
|
@@ -530,9 +529,10 @@ export class OAuthProvider extends OAuthVerifier {
|
|
530
529
|
* @see {@link https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-11#section-4.1.1}
|
531
530
|
*/
|
532
531
|
protected async authorize(
|
533
|
-
|
534
|
-
credentials: OAuthClientCredentialsNone,
|
532
|
+
clientCredentials: OAuthClientCredentialsNone,
|
535
533
|
query: OAuthAuthorizationRequestQuery,
|
534
|
+
deviceId: DeviceId,
|
535
|
+
deviceMetadata: RequestMetadata,
|
536
536
|
): Promise<AuthorizationResultRedirect | AuthorizationResultAuthorize> {
|
537
537
|
const { issuer } = this
|
538
538
|
|
@@ -547,7 +547,7 @@ export class OAuthProvider extends OAuthVerifier {
|
|
547
547
|
: null
|
548
548
|
|
549
549
|
const client = await this.clientManager
|
550
|
-
.getClient(
|
550
|
+
.getClient(clientCredentials.client_id)
|
551
551
|
.catch(accessDeniedCatcher)
|
552
552
|
|
553
553
|
const { clientAuth, parameters, uri } =
|
@@ -581,10 +581,11 @@ export class OAuthProvider extends OAuthVerifier {
|
|
581
581
|
}
|
582
582
|
|
583
583
|
const code = await this.requestManager.setAuthorized(
|
584
|
-
client,
|
585
584
|
uri,
|
586
|
-
|
585
|
+
client,
|
587
586
|
ssoSession.account,
|
587
|
+
deviceId,
|
588
|
+
deviceMetadata,
|
588
589
|
)
|
589
590
|
|
590
591
|
return { issuer, client, parameters, redirect: { code } }
|
@@ -597,10 +598,11 @@ export class OAuthProvider extends OAuthVerifier {
|
|
597
598
|
const ssoSession = ssoSessions[0]!
|
598
599
|
if (!ssoSession.loginRequired && !ssoSession.consentRequired) {
|
599
600
|
const code = await this.requestManager.setAuthorized(
|
600
|
-
client,
|
601
601
|
uri,
|
602
|
-
|
602
|
+
client,
|
603
603
|
ssoSession.account,
|
604
|
+
deviceId,
|
605
|
+
deviceMetadata,
|
604
606
|
)
|
605
607
|
|
606
608
|
return { issuer, client, parameters, redirect: { code } }
|
@@ -721,10 +723,11 @@ export class OAuthProvider extends OAuthVerifier {
|
|
721
723
|
}
|
722
724
|
|
723
725
|
protected async acceptRequest(
|
724
|
-
deviceId: DeviceId,
|
725
726
|
uri: RequestUri,
|
726
727
|
clientId: ClientId,
|
727
728
|
sub: string,
|
729
|
+
deviceId: DeviceId,
|
730
|
+
deviceMetadata: RequestMetadata,
|
728
731
|
): Promise<AuthorizationResultRedirect> {
|
729
732
|
const { issuer } = this
|
730
733
|
const client = await this.clientManager.getClient(clientId)
|
@@ -747,10 +750,11 @@ export class OAuthProvider extends OAuthVerifier {
|
|
747
750
|
}
|
748
751
|
|
749
752
|
const code = await this.requestManager.setAuthorized(
|
750
|
-
client,
|
751
753
|
uri,
|
752
|
-
|
754
|
+
client,
|
753
755
|
account,
|
756
|
+
deviceId,
|
757
|
+
deviceMetadata,
|
754
758
|
)
|
755
759
|
|
756
760
|
await this.accountManager.addAuthorizedClient(
|
@@ -792,11 +796,13 @@ export class OAuthProvider extends OAuthVerifier {
|
|
792
796
|
}
|
793
797
|
|
794
798
|
protected async token(
|
795
|
-
|
799
|
+
clientCredentials: OAuthClientCredentials,
|
800
|
+
clientMetadata: RequestMetadata,
|
796
801
|
request: OAuthTokenRequest,
|
797
802
|
dpopJkt: null | string,
|
798
803
|
): Promise<OAuthTokenResponse> {
|
799
|
-
const [client, clientAuth] =
|
804
|
+
const [client, clientAuth] =
|
805
|
+
await this.authenticateClient(clientCredentials)
|
800
806
|
|
801
807
|
if (!this.metadata.grant_types_supported?.includes(request.grant_type)) {
|
802
808
|
throw new InvalidGrantError(
|
@@ -811,11 +817,23 @@ export class OAuthProvider extends OAuthVerifier {
|
|
811
817
|
}
|
812
818
|
|
813
819
|
if (request.grant_type === 'authorization_code') {
|
814
|
-
return this.codeGrant(
|
820
|
+
return this.codeGrant(
|
821
|
+
client,
|
822
|
+
clientAuth,
|
823
|
+
clientMetadata,
|
824
|
+
request,
|
825
|
+
dpopJkt,
|
826
|
+
)
|
815
827
|
}
|
816
828
|
|
817
829
|
if (request.grant_type === 'refresh_token') {
|
818
|
-
return this.refreshTokenGrant(
|
830
|
+
return this.refreshTokenGrant(
|
831
|
+
client,
|
832
|
+
clientAuth,
|
833
|
+
clientMetadata,
|
834
|
+
request,
|
835
|
+
dpopJkt,
|
836
|
+
)
|
819
837
|
}
|
820
838
|
|
821
839
|
throw new InvalidGrantError(
|
@@ -826,6 +844,7 @@ export class OAuthProvider extends OAuthVerifier {
|
|
826
844
|
protected async codeGrant(
|
827
845
|
client: Client,
|
828
846
|
clientAuth: ClientAuth,
|
847
|
+
clientMetadata: RequestMetadata,
|
829
848
|
input: OAuthAuthorizationCodeGrantTokenRequest,
|
830
849
|
dpopJkt: null | string,
|
831
850
|
): Promise<OAuthTokenResponse> {
|
@@ -869,6 +888,7 @@ export class OAuthProvider extends OAuthVerifier {
|
|
869
888
|
return await this.tokenManager.create(
|
870
889
|
client,
|
871
890
|
clientAuth,
|
891
|
+
clientMetadata,
|
872
892
|
account,
|
873
893
|
{ id: deviceId, info },
|
874
894
|
parameters,
|
@@ -891,10 +911,17 @@ export class OAuthProvider extends OAuthVerifier {
|
|
891
911
|
async refreshTokenGrant(
|
892
912
|
client: Client,
|
893
913
|
clientAuth: ClientAuth,
|
914
|
+
clientMetadata: RequestMetadata,
|
894
915
|
input: OAuthRefreshTokenGrantTokenRequest,
|
895
916
|
dpopJkt: null | string,
|
896
917
|
): Promise<OAuthTokenResponse> {
|
897
|
-
return this.tokenManager.refresh(
|
918
|
+
return this.tokenManager.refresh(
|
919
|
+
client,
|
920
|
+
clientAuth,
|
921
|
+
clientMetadata,
|
922
|
+
input,
|
923
|
+
dpopJkt,
|
924
|
+
)
|
898
925
|
}
|
899
926
|
|
900
927
|
/**
|
@@ -999,7 +1026,7 @@ export class OAuthProvider extends OAuthVerifier {
|
|
999
1026
|
Req extends IncomingMessage = IncomingMessage,
|
1000
1027
|
Res extends ServerResponse = ServerResponse,
|
1001
1028
|
>(options?: RouterOptions<Req, Res>) {
|
1002
|
-
const deviceManager = new DeviceManager(this.deviceStore)
|
1029
|
+
const deviceManager = new DeviceManager(this.deviceStore, options)
|
1003
1030
|
const outputManager = new OutputManager(this.customization)
|
1004
1031
|
|
1005
1032
|
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
@@ -1225,7 +1252,9 @@ export class OAuthProvider extends OAuthVerifier {
|
|
1225
1252
|
jsonHandler(async function (req, _res) {
|
1226
1253
|
const payload = await parseHttpRequest(req, ['json', 'urlencoded'])
|
1227
1254
|
|
1228
|
-
const
|
1255
|
+
const clientMetadata = await deviceManager.getRequestMetadata(req)
|
1256
|
+
|
1257
|
+
const clientCredentials = await oauthClientCredentialsSchema
|
1229
1258
|
.parseAsync(payload, { path: ['body'] })
|
1230
1259
|
.catch(throwInvalidClient)
|
1231
1260
|
|
@@ -1239,7 +1268,12 @@ export class OAuthProvider extends OAuthVerifier {
|
|
1239
1268
|
this.url,
|
1240
1269
|
)
|
1241
1270
|
|
1242
|
-
return server.token(
|
1271
|
+
return server.token(
|
1272
|
+
clientCredentials,
|
1273
|
+
clientMetadata,
|
1274
|
+
tokenRequest,
|
1275
|
+
dpopJkt,
|
1276
|
+
)
|
1243
1277
|
}),
|
1244
1278
|
)
|
1245
1279
|
|
@@ -1314,11 +1348,11 @@ export class OAuthProvider extends OAuthVerifier {
|
|
1314
1348
|
|
1315
1349
|
const query = Object.fromEntries(this.url.searchParams)
|
1316
1350
|
|
1317
|
-
const
|
1351
|
+
const clientCredentials = await oauthClientCredentialsSchema
|
1318
1352
|
.parseAsync(query, { path: ['body'] })
|
1319
1353
|
.catch(throwInvalidRequest)
|
1320
1354
|
|
1321
|
-
if ('client_secret' in
|
1355
|
+
if ('client_secret' in clientCredentials) {
|
1322
1356
|
throw new InvalidRequestError('Client secret must not be provided')
|
1323
1357
|
}
|
1324
1358
|
|
@@ -1326,10 +1360,15 @@ export class OAuthProvider extends OAuthVerifier {
|
|
1326
1360
|
.parseAsync(query, { path: ['query'] })
|
1327
1361
|
.catch(throwInvalidRequest)
|
1328
1362
|
|
1329
|
-
const { deviceId } = await deviceManager.load(req, res)
|
1363
|
+
const { deviceId, deviceMetadata } = await deviceManager.load(req, res)
|
1330
1364
|
|
1331
1365
|
const data = await server
|
1332
|
-
.authorize(
|
1366
|
+
.authorize(
|
1367
|
+
clientCredentials,
|
1368
|
+
authorizationRequest,
|
1369
|
+
deviceId,
|
1370
|
+
deviceMetadata,
|
1371
|
+
)
|
1333
1372
|
.catch((err) => accessDeniedToRedirectCatcher(req, res, err))
|
1334
1373
|
|
1335
1374
|
switch (true) {
|
@@ -1432,14 +1471,15 @@ export class OAuthProvider extends OAuthVerifier {
|
|
1432
1471
|
true,
|
1433
1472
|
)
|
1434
1473
|
|
1435
|
-
const { deviceId } = await deviceManager.load(req, res)
|
1474
|
+
const { deviceId, deviceMetadata } = await deviceManager.load(req, res)
|
1436
1475
|
|
1437
1476
|
const data = await server
|
1438
1477
|
.acceptRequest(
|
1439
|
-
deviceId,
|
1440
1478
|
input.request_uri,
|
1441
1479
|
input.client_id,
|
1442
1480
|
input.account_sub,
|
1481
|
+
deviceId,
|
1482
|
+
deviceMetadata,
|
1443
1483
|
)
|
1444
1484
|
.catch((err) => accessDeniedToRedirectCatcher(req, res, err))
|
1445
1485
|
|
package/src/oauth-verifier.ts
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
import type { Redis, RedisOptions } from 'ioredis'
|
1
2
|
import { Key, Keyset, isSignedJwt } from '@atproto/jwk'
|
2
3
|
import {
|
3
4
|
OAuthAccessToken,
|
@@ -5,8 +6,6 @@ import {
|
|
5
6
|
OAuthTokenType,
|
6
7
|
oauthIssuerIdentifierSchema,
|
7
8
|
} from '@atproto/oauth-types'
|
8
|
-
import type { Redis, RedisOptions } from 'ioredis'
|
9
|
-
|
10
9
|
import { AccessTokenType } from './access-token/access-token-type.js'
|
11
10
|
import { DpopManager, DpopManagerOptions } from './dpop/dpop-manager.js'
|
12
11
|
import { DpopNonce } from './dpop/dpop-nonce.js'
|
@@ -1,17 +1,16 @@
|
|
1
|
-
import { ServerResponse } from 'node:http'
|
2
|
-
|
1
|
+
import type { ServerResponse } from 'node:http'
|
3
2
|
import { Asset } from '../assets/asset.js'
|
4
3
|
import { getAsset } from '../assets/index.js'
|
5
|
-
import {
|
4
|
+
import { Html, cssCode, html } from '../lib/html/index.js'
|
6
5
|
import {
|
7
6
|
AuthorizationResultAuthorize,
|
8
7
|
buildAuthorizeData,
|
9
8
|
} from './build-authorize-data.js'
|
10
9
|
import { buildErrorPayload, buildErrorStatus } from './build-error-payload.js'
|
11
10
|
import {
|
11
|
+
Customization,
|
12
12
|
buildCustomizationCss,
|
13
13
|
buildCustomizationData,
|
14
|
-
Customization,
|
15
14
|
} from './customization.js'
|
16
15
|
import { declareBackendData, sendWebPage } from './send-web-page.js'
|
17
16
|
|
@@ -1,9 +1,8 @@
|
|
1
|
+
import type { ServerResponse } from 'node:http'
|
1
2
|
import {
|
2
3
|
OAuthAuthorizationRequestParameters,
|
3
4
|
OAuthTokenType,
|
4
5
|
} from '@atproto/oauth-types'
|
5
|
-
import { ServerResponse } from 'node:http'
|
6
|
-
|
7
6
|
import { InvalidRequestError } from '../errors/invalid-request-error.js'
|
8
7
|
import { html, js } from '../lib/html/index.js'
|
9
8
|
import { Code } from '../request/code.js'
|
@@ -1,14 +1,13 @@
|
|
1
1
|
import { createHash } from 'node:crypto'
|
2
|
-
import { ServerResponse } from 'node:http'
|
3
|
-
|
2
|
+
import type { ServerResponse } from 'node:http'
|
4
3
|
import {
|
5
4
|
AssetRef,
|
6
|
-
buildDocument,
|
7
5
|
BuildDocumentOptions,
|
8
6
|
Html,
|
7
|
+
buildDocument,
|
9
8
|
js,
|
10
9
|
} from '../lib/html/index.js'
|
11
|
-
import {
|
10
|
+
import { WriteResponseOptions, writeHtml } from '../lib/http/response.js'
|
12
11
|
|
13
12
|
export function declareBackendData(name: string, data: unknown) {
|
14
13
|
// The script tag is removed after the data is assigned to the global variable
|
package/src/request/code.ts
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
import { OAuthAuthorizationRequestParameters } from '@atproto/oauth-types'
|
2
|
-
import { ClientId } from '../client/client-id.js'
|
3
2
|
import { ClientAuth } from '../client/client-auth.js'
|
3
|
+
import { ClientId } from '../client/client-id.js'
|
4
4
|
import { RequestId } from './request-id.js'
|
5
5
|
import { RequestUri } from './request-uri.js'
|
6
6
|
|