@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.
- package/CHANGELOG.md +26 -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
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
import { OAuthClientMetadata } from '@atproto/oauth-types'
|
|
2
|
-
import { Awaitable, buildInterfaceChecker } from '../lib/util/type.js'
|
|
3
|
-
import { ClientId } from './client-id.js'
|
|
4
|
-
|
|
5
|
-
// Export all types needed to implement the ClientStore interface
|
|
6
|
-
export * from './client-data.js'
|
|
7
|
-
export * from './client-id.js'
|
|
8
|
-
export type { Awaitable, OAuthClientMetadata }
|
|
9
|
-
|
|
10
|
-
export interface ClientStore {
|
|
11
|
-
findClient(clientId: ClientId): Awaitable<OAuthClientMetadata>
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export const isClientStore = buildInterfaceChecker<ClientStore>([
|
|
15
|
-
'findClient', //
|
|
16
|
-
])
|
|
17
|
-
|
|
18
|
-
export function ifClientStore<V extends Partial<ClientStore>>(
|
|
19
|
-
implementation?: V,
|
|
20
|
-
): (V & ClientStore) | undefined {
|
|
21
|
-
if (implementation && isClientStore(implementation)) {
|
|
22
|
-
return implementation
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
return undefined
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export function asClientStore<V extends Partial<ClientStore>>(
|
|
29
|
-
implementation?: V,
|
|
30
|
-
): V & ClientStore {
|
|
31
|
-
const store = ifClientStore(implementation)
|
|
32
|
-
if (store) return store
|
|
33
|
-
|
|
34
|
-
throw new Error('Invalid ClientStore implementation')
|
|
35
|
-
}
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
OAuthClientIdDiscoverable,
|
|
3
|
-
isLocalHostname,
|
|
4
|
-
parseOAuthDiscoverableClientId,
|
|
5
|
-
} from '@atproto/oauth-types'
|
|
6
|
-
import { InvalidClientIdError } from '../errors/invalid-client-id-error.js'
|
|
7
|
-
import { InvalidRedirectUriError } from '../errors/invalid-redirect-uri-error.js'
|
|
8
|
-
|
|
9
|
-
export function parseRedirectUri(redirectUri: string): URL {
|
|
10
|
-
try {
|
|
11
|
-
return new URL(redirectUri)
|
|
12
|
-
} catch (err) {
|
|
13
|
-
throw InvalidRedirectUriError.from(err)
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export function parseDiscoverableClientId(
|
|
18
|
-
clientId: OAuthClientIdDiscoverable,
|
|
19
|
-
): URL {
|
|
20
|
-
try {
|
|
21
|
-
const url = parseOAuthDiscoverableClientId(clientId)
|
|
22
|
-
|
|
23
|
-
// Extra validation, prevent usage of invalid internet domain names.
|
|
24
|
-
if (isLocalHostname(url.hostname)) {
|
|
25
|
-
throw new InvalidClientIdError(
|
|
26
|
-
"The client_id's TLD must not be a local hostname",
|
|
27
|
-
)
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
return url
|
|
31
|
-
} catch (err) {
|
|
32
|
-
throw InvalidClientIdError.from(
|
|
33
|
-
err,
|
|
34
|
-
'Invalid discoverable client identifier',
|
|
35
|
-
)
|
|
36
|
-
}
|
|
37
|
-
}
|
package/src/client/client.ts
DELETED
|
@@ -1,394 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
JWTClaimVerificationOptions,
|
|
3
|
-
type JWTHeaderParameters,
|
|
4
|
-
type JWTPayload,
|
|
5
|
-
type JWTVerifyOptions,
|
|
6
|
-
type JWTVerifyResult,
|
|
7
|
-
type KeyLike,
|
|
8
|
-
type ResolvedKey,
|
|
9
|
-
UnsecuredJWT,
|
|
10
|
-
type UnsecuredResult,
|
|
11
|
-
calculateJwkThumbprint,
|
|
12
|
-
createLocalJWKSet,
|
|
13
|
-
createRemoteJWKSet,
|
|
14
|
-
errors,
|
|
15
|
-
exportJWK,
|
|
16
|
-
jwtVerify,
|
|
17
|
-
} from 'jose'
|
|
18
|
-
import { Jwks, SignedJwt, UnsignedJwt } from '@atproto/jwk'
|
|
19
|
-
import {
|
|
20
|
-
CLIENT_ASSERTION_TYPE_JWT_BEARER,
|
|
21
|
-
OAuthAuthorizationRequestParameters,
|
|
22
|
-
OAuthClientCredentials,
|
|
23
|
-
OAuthClientMetadata,
|
|
24
|
-
OAuthRedirectUri,
|
|
25
|
-
} from '@atproto/oauth-types'
|
|
26
|
-
import { CLIENT_ASSERTION_MAX_AGE, JAR_MAX_AGE } from '../constants.js'
|
|
27
|
-
import { AuthorizationError } from '../errors/authorization-error.js'
|
|
28
|
-
import { InvalidAuthorizationDetailsError } from '../errors/invalid-authorization-details-error.js'
|
|
29
|
-
import { InvalidClientError } from '../errors/invalid-client-error.js'
|
|
30
|
-
import { InvalidClientMetadataError } from '../errors/invalid-client-metadata-error.js'
|
|
31
|
-
import { InvalidRequestError } from '../errors/invalid-request-error.js'
|
|
32
|
-
import { InvalidScopeError } from '../errors/invalid-scope-error.js'
|
|
33
|
-
import { asArray } from '../lib/util/cast.js'
|
|
34
|
-
import { compareRedirectUri } from '../lib/util/redirect-uri.js'
|
|
35
|
-
import { Awaitable } from '../lib/util/type.js'
|
|
36
|
-
import { ClientAuth } from './client-auth.js'
|
|
37
|
-
import { ClientId } from './client-id.js'
|
|
38
|
-
import { ClientInfo } from './client-info.js'
|
|
39
|
-
|
|
40
|
-
const { JOSEError } = errors
|
|
41
|
-
|
|
42
|
-
export class Client {
|
|
43
|
-
/**
|
|
44
|
-
* @see {@link https://www.iana.org/assignments/oauth-parameters/oauth-parameters.xhtml#token-endpoint-auth-method}
|
|
45
|
-
*/
|
|
46
|
-
static readonly AUTH_METHODS_SUPPORTED = ['none', 'private_key_jwt'] as const
|
|
47
|
-
|
|
48
|
-
private readonly keyGetter: (
|
|
49
|
-
protectedHeader: JWTHeaderParameters,
|
|
50
|
-
) => Awaitable<KeyLike | Uint8Array>
|
|
51
|
-
|
|
52
|
-
constructor(
|
|
53
|
-
public readonly id: ClientId,
|
|
54
|
-
public readonly metadata: OAuthClientMetadata,
|
|
55
|
-
public readonly jwks: undefined | Jwks = metadata.jwks,
|
|
56
|
-
public readonly info: ClientInfo,
|
|
57
|
-
) {
|
|
58
|
-
// If the remote JWKS content is provided, we don't need to fetch it again.
|
|
59
|
-
this.keyGetter =
|
|
60
|
-
jwks || !metadata.jwks_uri
|
|
61
|
-
? createLocalJWKSet(jwks || { keys: [] })
|
|
62
|
-
: createRemoteJWKSet(new URL(metadata.jwks_uri), {})
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* @see {@link https://www.rfc-editor.org/rfc/rfc9101.html#name-request-object-2}
|
|
67
|
-
*/
|
|
68
|
-
public async decodeRequestObject(
|
|
69
|
-
jar: SignedJwt | UnsignedJwt,
|
|
70
|
-
audience: string,
|
|
71
|
-
) {
|
|
72
|
-
// https://www.rfc-editor.org/rfc/rfc9101.html#name-request-object-2
|
|
73
|
-
// > If signed, the Authorization Request Object SHOULD contain the Claims
|
|
74
|
-
// > iss (issuer) and aud (audience) as members with their semantics being
|
|
75
|
-
// > the same as defined in the JWT [RFC7519] specification. The value of
|
|
76
|
-
// > aud should be the value of the authorization server (AS) issuer, as
|
|
77
|
-
// > defined in RFC 8414 [RFC8414].
|
|
78
|
-
try {
|
|
79
|
-
// We need to special case the "none" algorithm, as the validation method
|
|
80
|
-
// is different for signed and unsigned JWTs.
|
|
81
|
-
if (this.metadata.request_object_signing_alg === 'none') {
|
|
82
|
-
return await this.jwtVerifyUnsecured(jar, {
|
|
83
|
-
audience,
|
|
84
|
-
maxTokenAge: JAR_MAX_AGE / 1e3,
|
|
85
|
-
allowMissingAudience: true,
|
|
86
|
-
allowMissingIssuer: true,
|
|
87
|
-
})
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
return await this.jwtVerify(jar, {
|
|
91
|
-
audience,
|
|
92
|
-
maxTokenAge: JAR_MAX_AGE / 1e3,
|
|
93
|
-
algorithms: this.metadata.request_object_signing_alg
|
|
94
|
-
? [this.metadata.request_object_signing_alg]
|
|
95
|
-
: // https://openid.net/specs/openid-connect-registration-1_0.html#rfc.section.2
|
|
96
|
-
//
|
|
97
|
-
// > The default, if omitted, is that any algorithm supported by the OP
|
|
98
|
-
// > and the RP MAY be used.
|
|
99
|
-
undefined,
|
|
100
|
-
})
|
|
101
|
-
} catch (err) {
|
|
102
|
-
const message =
|
|
103
|
-
err instanceof JOSEError
|
|
104
|
-
? `Invalid "request" object: ${err.message}`
|
|
105
|
-
: `Invalid "request" object`
|
|
106
|
-
|
|
107
|
-
throw new InvalidRequestError(message, err)
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
protected async jwtVerifyUnsecured<PayloadType = JWTPayload>(
|
|
112
|
-
token: string,
|
|
113
|
-
{
|
|
114
|
-
audience,
|
|
115
|
-
allowMissingAudience = false,
|
|
116
|
-
allowMissingIssuer = false,
|
|
117
|
-
...options
|
|
118
|
-
}: Omit<JWTClaimVerificationOptions, 'issuer'> & {
|
|
119
|
-
allowMissingIssuer?: boolean
|
|
120
|
-
allowMissingAudience?: boolean
|
|
121
|
-
} = {},
|
|
122
|
-
): Promise<UnsecuredResult<PayloadType>> {
|
|
123
|
-
// jose does not support `allowMissingAudience` and `allowMissingIssuer`
|
|
124
|
-
// options, so we need to handle audience and issuer checks manually (see
|
|
125
|
-
// bellow).
|
|
126
|
-
|
|
127
|
-
const result = UnsecuredJWT.decode<PayloadType>(token, options)
|
|
128
|
-
|
|
129
|
-
if (!allowMissingIssuer || result.payload.iss != null) {
|
|
130
|
-
if (result.payload.iss !== this.id) {
|
|
131
|
-
throw new JOSEError(`Invalid "iss" claim "${result.payload.iss}"`)
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
if (!allowMissingAudience || result.payload.aud != null) {
|
|
136
|
-
if (audience != null) {
|
|
137
|
-
const payloadAud = asArray(result.payload.aud)
|
|
138
|
-
if (!asArray(audience).some((aud) => payloadAud.includes(aud))) {
|
|
139
|
-
throw new JOSEError(`Invalid "aud" claim "${result.payload.aud}"`)
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
return result
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
protected async jwtVerify<PayloadType = JWTPayload>(
|
|
148
|
-
token: string,
|
|
149
|
-
options?: Omit<JWTVerifyOptions, 'issuer'>,
|
|
150
|
-
): Promise<JWTVerifyResult<PayloadType> & ResolvedKey<KeyLike>> {
|
|
151
|
-
return jwtVerify<PayloadType>(token, this.keyGetter, {
|
|
152
|
-
...options,
|
|
153
|
-
issuer: this.id,
|
|
154
|
-
})
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
/**
|
|
158
|
-
* @see {@link https://datatracker.ietf.org/doc/html/rfc6749#section-2.3.1}
|
|
159
|
-
* @see {@link https://datatracker.ietf.org/doc/html/rfc7523#section-3}
|
|
160
|
-
* @see {@link https://www.iana.org/assignments/oauth-parameters/oauth-parameters.xhtml#token-endpoint-auth-method}
|
|
161
|
-
*/
|
|
162
|
-
public async authenticate(
|
|
163
|
-
input: OAuthClientCredentials,
|
|
164
|
-
checks: {
|
|
165
|
-
authorizationServerIdentifier: string
|
|
166
|
-
},
|
|
167
|
-
): Promise<ClientAuth> {
|
|
168
|
-
const method = this.metadata.token_endpoint_auth_method
|
|
169
|
-
|
|
170
|
-
if (method === 'none') {
|
|
171
|
-
return { method: 'none' }
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
if (method === 'private_key_jwt') {
|
|
175
|
-
if (!('client_assertion' in input)) {
|
|
176
|
-
throw new InvalidRequestError(
|
|
177
|
-
`client authentication method "${method}" required a "client_assertion"`,
|
|
178
|
-
)
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
if (input.client_assertion_type === CLIENT_ASSERTION_TYPE_JWT_BEARER) {
|
|
182
|
-
// https://www.rfc-editor.org/rfc/rfc7523.html#section-3
|
|
183
|
-
|
|
184
|
-
const result = await this.jwtVerify<{
|
|
185
|
-
jti: string
|
|
186
|
-
exp?: number
|
|
187
|
-
}>(input.client_assertion, {
|
|
188
|
-
// > 1. The JWT MUST contain an "iss" (issuer) claim that contains a
|
|
189
|
-
// > unique identifier for the entity that issued the JWT.
|
|
190
|
-
//
|
|
191
|
-
// The "issuer" is already checked by jwtVerify()
|
|
192
|
-
|
|
193
|
-
// > 2. The JWT MUST contain a "sub" (subject) claim identifying the
|
|
194
|
-
// > principal that is the subject of the JWT. Two cases need to be
|
|
195
|
-
// > differentiated: [...] For client authentication, the subject
|
|
196
|
-
// > MUST be the "client_id" of the OAuth client.
|
|
197
|
-
subject: this.id,
|
|
198
|
-
|
|
199
|
-
// > 3. The JWT MUST contain an "aud" (audience) claim containing a
|
|
200
|
-
// > value that identifies the authorization server as an intended
|
|
201
|
-
// > audience. The token endpoint URL of the authorization server
|
|
202
|
-
// > MAY be used as a value for an "aud" element to identify the
|
|
203
|
-
// > authorization server as an intended audience of the JWT.
|
|
204
|
-
audience: checks.authorizationServerIdentifier,
|
|
205
|
-
|
|
206
|
-
requiredClaims: [
|
|
207
|
-
// > 4. The JWT MUST contain an "exp" (expiration time) claim that
|
|
208
|
-
// > limits the time window during which the JWT can be used.
|
|
209
|
-
//
|
|
210
|
-
// @TODO The presence of "exp" didn't use to be enforced by this
|
|
211
|
-
// implementation (or provided by the oauth-client). This is mostly
|
|
212
|
-
// fine because "iat" *is* required, but this makes this
|
|
213
|
-
// implementation non compliant with RFC7523. We can't just make it
|
|
214
|
-
// required as it might break existing clients.
|
|
215
|
-
|
|
216
|
-
// 'exp',
|
|
217
|
-
|
|
218
|
-
// > 7. The JWT MAY contain a "jti" (JWT ID) claim that provides a
|
|
219
|
-
// > unique identifier for the token. The authorization server
|
|
220
|
-
// > MAY ensure that JWTs are not replayed by maintaining the set
|
|
221
|
-
// > of used "jti" values for the length of time for which the
|
|
222
|
-
// > JWT would be considered valid based on the applicable "exp"
|
|
223
|
-
// > instant.
|
|
224
|
-
'jti',
|
|
225
|
-
],
|
|
226
|
-
|
|
227
|
-
// > 5. The JWT MAY contain an "nbf" (not before) claim that
|
|
228
|
-
// > identifies the time before which the token MUST NOT be
|
|
229
|
-
// > accepted for processing.
|
|
230
|
-
//
|
|
231
|
-
// This is already enforced by jose
|
|
232
|
-
|
|
233
|
-
// > 6. The JWT MAY contain an "iat" (issued at) claim that identifies
|
|
234
|
-
// > the time at which the JWT was issued. Note that the
|
|
235
|
-
// > authorization server may reject JWTs with an "iat" claim value
|
|
236
|
-
// > that is unreasonably far in the past.
|
|
237
|
-
maxTokenAge: CLIENT_ASSERTION_MAX_AGE / 1000,
|
|
238
|
-
}).catch((err) => {
|
|
239
|
-
const msg =
|
|
240
|
-
err instanceof JOSEError
|
|
241
|
-
? `Validation of "client_assertion" failed: ${err.message}`
|
|
242
|
-
: `Unable to verify "client_assertion" JWT`
|
|
243
|
-
|
|
244
|
-
throw new InvalidClientError(msg, err)
|
|
245
|
-
})
|
|
246
|
-
|
|
247
|
-
if (!result.protectedHeader.kid) {
|
|
248
|
-
throw new InvalidClientError(`"kid" required in client_assertion`)
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
return {
|
|
252
|
-
method: 'private_key_jwt',
|
|
253
|
-
jti: result.payload.jti,
|
|
254
|
-
exp: result.payload.exp,
|
|
255
|
-
jkt: await authJwkThumbprint(result.key),
|
|
256
|
-
alg: result.protectedHeader.alg,
|
|
257
|
-
kid: result.protectedHeader.kid,
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
throw new InvalidClientError(
|
|
262
|
-
`Unsupported client_assertion_type "${input.client_assertion_type}"`,
|
|
263
|
-
)
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
// @ts-expect-error Ensure to keep Client.AUTH_METHODS_SUPPORTED in sync
|
|
267
|
-
// with the implementation of this function.
|
|
268
|
-
if (Client.AUTH_METHODS_SUPPORTED.includes(method)) {
|
|
269
|
-
throw new Error(
|
|
270
|
-
`verifyCredentials() should implement all of ${[
|
|
271
|
-
Client.AUTH_METHODS_SUPPORTED,
|
|
272
|
-
]}`,
|
|
273
|
-
)
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
throw new InvalidClientMetadataError(
|
|
277
|
-
`Unsupported token_endpoint_auth_method "${method}"`,
|
|
278
|
-
)
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
/**
|
|
282
|
-
* Validates the request parameters against the client metadata.
|
|
283
|
-
*/
|
|
284
|
-
public validateRequest(
|
|
285
|
-
parameters: Readonly<OAuthAuthorizationRequestParameters>,
|
|
286
|
-
): Readonly<OAuthAuthorizationRequestParameters> {
|
|
287
|
-
if (parameters.client_id !== this.id) {
|
|
288
|
-
throw new AuthorizationError(
|
|
289
|
-
parameters,
|
|
290
|
-
'The "client_id" parameter field does not match the value used to authenticate the client',
|
|
291
|
-
)
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
if (parameters.scope !== undefined) {
|
|
295
|
-
// Any scope requested by the client must be registered in the client
|
|
296
|
-
// metadata.
|
|
297
|
-
const declaredScopes = this.metadata.scope?.split(' ')
|
|
298
|
-
|
|
299
|
-
if (!declaredScopes) {
|
|
300
|
-
throw new InvalidScopeError(
|
|
301
|
-
parameters,
|
|
302
|
-
'Client has no declared scopes in its metadata',
|
|
303
|
-
)
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
for (const scope of parameters.scope.split(' ')) {
|
|
307
|
-
if (!declaredScopes.includes(scope)) {
|
|
308
|
-
throw new InvalidScopeError(
|
|
309
|
-
parameters,
|
|
310
|
-
`Scope "${scope}" is not declared in the client metadata`,
|
|
311
|
-
)
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
if (!this.metadata.response_types.includes(parameters.response_type)) {
|
|
317
|
-
throw new AuthorizationError(
|
|
318
|
-
parameters,
|
|
319
|
-
`Invalid response_type "${parameters.response_type}" requested by the client`,
|
|
320
|
-
)
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
if (parameters.response_type.includes('code')) {
|
|
324
|
-
if (!this.metadata.grant_types.includes('authorization_code')) {
|
|
325
|
-
throw new AuthorizationError(
|
|
326
|
-
parameters,
|
|
327
|
-
`This client is not allowed to use the "authorization_code" grant type`,
|
|
328
|
-
)
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
const { redirect_uri } = parameters
|
|
333
|
-
if (redirect_uri) {
|
|
334
|
-
if (
|
|
335
|
-
!this.metadata.redirect_uris.some((uri) =>
|
|
336
|
-
compareRedirectUri(uri, redirect_uri),
|
|
337
|
-
)
|
|
338
|
-
) {
|
|
339
|
-
throw new AuthorizationError(
|
|
340
|
-
parameters,
|
|
341
|
-
`Invalid redirect_uri ${redirect_uri}`,
|
|
342
|
-
)
|
|
343
|
-
}
|
|
344
|
-
} else {
|
|
345
|
-
const { defaultRedirectUri } = this
|
|
346
|
-
if (defaultRedirectUri) {
|
|
347
|
-
parameters = { ...parameters, redirect_uri: defaultRedirectUri }
|
|
348
|
-
} else {
|
|
349
|
-
// https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-10#authorization-request
|
|
350
|
-
//
|
|
351
|
-
// > "redirect_uri": OPTIONAL if only one redirect URI is registered for
|
|
352
|
-
// > this client. REQUIRED if multiple redirect URIs are registered for this
|
|
353
|
-
// > client.
|
|
354
|
-
throw new AuthorizationError(parameters, 'redirect_uri is required')
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
if (parameters.authorization_details) {
|
|
359
|
-
const { authorization_details_types } = this.metadata
|
|
360
|
-
if (!authorization_details_types) {
|
|
361
|
-
throw new InvalidAuthorizationDetailsError(
|
|
362
|
-
parameters,
|
|
363
|
-
'Client Metadata does not declare any "authorization_details"',
|
|
364
|
-
)
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
for (const detail of parameters.authorization_details) {
|
|
368
|
-
if (!authorization_details_types?.includes(detail.type)) {
|
|
369
|
-
throw new InvalidAuthorizationDetailsError(
|
|
370
|
-
parameters,
|
|
371
|
-
`Client Metadata does not declare any "authorization_details" of type "${detail.type}"`,
|
|
372
|
-
)
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
return parameters
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
get defaultRedirectUri(): OAuthRedirectUri | undefined {
|
|
381
|
-
const { redirect_uris } = this.metadata
|
|
382
|
-
return redirect_uris.length === 1 ? redirect_uris[0] : undefined
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
export async function authJwkThumbprint(
|
|
387
|
-
key: Uint8Array | KeyLike,
|
|
388
|
-
): Promise<string> {
|
|
389
|
-
try {
|
|
390
|
-
return await calculateJwkThumbprint(await exportJWK(key), 'sha512')
|
|
391
|
-
} catch (err) {
|
|
392
|
-
throw new InvalidClientError('Unable to compute JWK thumbprint', err)
|
|
393
|
-
}
|
|
394
|
-
}
|
package/src/constants.ts
DELETED
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
// The purpose of the prefix is to provide type safety
|
|
2
|
-
|
|
3
|
-
export const DEVICE_ID_PREFIX = 'dev-'
|
|
4
|
-
export const DEVICE_ID_BYTES_LENGTH = 16 // 128 bits
|
|
5
|
-
|
|
6
|
-
export const SESSION_ID_PREFIX = 'ses-'
|
|
7
|
-
export const SESSION_ID_BYTES_LENGTH = 16 // 128 bits - only valid if device id is valid
|
|
8
|
-
|
|
9
|
-
export const REFRESH_TOKEN_PREFIX = 'ref-'
|
|
10
|
-
export const REFRESH_TOKEN_BYTES_LENGTH = 32 // 256 bits
|
|
11
|
-
|
|
12
|
-
export const TOKEN_ID_PREFIX = 'tok-'
|
|
13
|
-
export const TOKEN_ID_BYTES_LENGTH = 16 // 128 bits - used as `jti` in JWTs (cannot be forged)
|
|
14
|
-
|
|
15
|
-
export const REQUEST_ID_PREFIX = 'req-'
|
|
16
|
-
export const REQUEST_ID_BYTES_LENGTH = 16 // 128 bits
|
|
17
|
-
|
|
18
|
-
export const CODE_PREFIX = 'cod-'
|
|
19
|
-
export const CODE_BYTES_LENGTH = 32
|
|
20
|
-
|
|
21
|
-
const SECOND = 1e3
|
|
22
|
-
const MINUTE = 60 * SECOND
|
|
23
|
-
const HOUR = 60 * MINUTE
|
|
24
|
-
const DAY = 24 * HOUR
|
|
25
|
-
const WEEK = 7 * DAY
|
|
26
|
-
const YEAR = 365.25 * DAY
|
|
27
|
-
const MONTH = YEAR / 12
|
|
28
|
-
|
|
29
|
-
/** 7 days */
|
|
30
|
-
export const AUTHENTICATION_MAX_AGE = 7 * DAY
|
|
31
|
-
|
|
32
|
-
/** 15 minutes */
|
|
33
|
-
export const EPHEMERAL_SESSION_MAX_AGE = 15 * MINUTE
|
|
34
|
-
|
|
35
|
-
/** 60 minutes */
|
|
36
|
-
export const TOKEN_MAX_AGE = 60 * MINUTE
|
|
37
|
-
|
|
38
|
-
/** 5 minutes */
|
|
39
|
-
export const AUTHORIZATION_INACTIVITY_TIMEOUT = 5 * MINUTE
|
|
40
|
-
|
|
41
|
-
/** 2 week */
|
|
42
|
-
export const PUBLIC_CLIENT_SESSION_LIFETIME = 2 * WEEK
|
|
43
|
-
|
|
44
|
-
/** @see {@link PUBLIC_CLIENT_SESSION_LIFETIME} */
|
|
45
|
-
export const PUBLIC_CLIENT_REFRESH_LIFETIME = PUBLIC_CLIENT_SESSION_LIFETIME
|
|
46
|
-
|
|
47
|
-
/** 2 years */
|
|
48
|
-
export const CONFIDENTIAL_CLIENT_SESSION_LIFETIME = 2 * YEAR
|
|
49
|
-
|
|
50
|
-
/** 3 months */
|
|
51
|
-
export const CONFIDENTIAL_CLIENT_REFRESH_LIFETIME = 3 * MONTH
|
|
52
|
-
|
|
53
|
-
/** 5 minutes */
|
|
54
|
-
export const PAR_EXPIRES_IN = 5 * MINUTE
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* 59 seconds (should be less than a minute)
|
|
58
|
-
*
|
|
59
|
-
* > "A general guidance for the validity time would be less than a minute."
|
|
60
|
-
*
|
|
61
|
-
* @see {@link https://datatracker.ietf.org/doc/html/rfc9101#section-10.2 | JWT-Secured Authorization Request (JAR) - Section 10.2 (d)}
|
|
62
|
-
*/
|
|
63
|
-
export const JAR_MAX_AGE = 59 * SECOND
|
|
64
|
-
|
|
65
|
-
/** 1 minute */
|
|
66
|
-
export const CLIENT_ASSERTION_MAX_AGE = 1 * MINUTE
|
|
67
|
-
|
|
68
|
-
/** 3 minutes */
|
|
69
|
-
export const DPOP_NONCE_MAX_AGE = 3 * MINUTE
|
|
70
|
-
|
|
71
|
-
/** 5 seconds */
|
|
72
|
-
export const SESSION_FIXATION_MAX_AGE = 5 * SECOND
|
|
73
|
-
|
|
74
|
-
/** 1 day */
|
|
75
|
-
export const CODE_CHALLENGE_REPLAY_TIMEFRAME = 1 * DAY
|
|
76
|
-
|
|
77
|
-
/** 5 minutes */
|
|
78
|
-
export const LEXICON_REFRESH_FREQUENCY = 5 * MINUTE
|
|
79
|
-
|
|
80
|
-
export const NODE_ENV = process.env.NODE_ENV || 'production'
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import { z } from 'zod'
|
|
2
|
-
import { colorsSchema } from './colors.js'
|
|
3
|
-
import { linksSchema } from './links.js'
|
|
4
|
-
|
|
5
|
-
export const brandingSchema = z.object({
|
|
6
|
-
name: z.string().optional(),
|
|
7
|
-
logo: z.string().url().optional(),
|
|
8
|
-
colors: colorsSchema.optional(),
|
|
9
|
-
links: z.array(linksSchema).optional(),
|
|
10
|
-
})
|
|
11
|
-
export type BrandingInput = z.input<typeof brandingSchema>
|
|
12
|
-
export type Branding = z.infer<typeof brandingSchema>
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
RgbColor,
|
|
3
|
-
extractHue,
|
|
4
|
-
hslToRgb,
|
|
5
|
-
pickContrastColor,
|
|
6
|
-
} from '../lib/util/color.js'
|
|
7
|
-
import { Branding } from './branding.js'
|
|
8
|
-
import { COLOR_NAMES } from './colors.js'
|
|
9
|
-
import { Customization } from './customization.js'
|
|
10
|
-
|
|
11
|
-
export function buildCustomizationCss({
|
|
12
|
-
branding,
|
|
13
|
-
}: Customization): undefined | string {
|
|
14
|
-
const vars = Array.from(buildCustomizationVars(branding))
|
|
15
|
-
if (vars.length) return `:root { ${vars.join(' ')} }`
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function* buildCustomizationVars(branding?: Branding): Generator<string> {
|
|
19
|
-
if (branding?.colors) {
|
|
20
|
-
const contrastSaturation = branding.colors.contrastSaturation ?? 30
|
|
21
|
-
yield `--contrast-sat: ${contrastSaturation.toFixed(2)}%;`
|
|
22
|
-
|
|
23
|
-
const contrastLight: RgbColor =
|
|
24
|
-
branding.colors.light ??
|
|
25
|
-
// Corresponds to color-contrast-975
|
|
26
|
-
hslToRgb({
|
|
27
|
-
h: branding.colors.primaryHue ?? 0,
|
|
28
|
-
s: contrastSaturation / 100,
|
|
29
|
-
l: 0.07,
|
|
30
|
-
})
|
|
31
|
-
const contrastDark: RgbColor =
|
|
32
|
-
branding.colors.dark ??
|
|
33
|
-
// Corresponds to color-contrast-25
|
|
34
|
-
hslToRgb({
|
|
35
|
-
h: branding.colors.primaryHue ?? 0,
|
|
36
|
-
s: contrastSaturation / 100,
|
|
37
|
-
l: 0.953,
|
|
38
|
-
})
|
|
39
|
-
|
|
40
|
-
for (const name of COLOR_NAMES) {
|
|
41
|
-
const value = branding.colors[name]
|
|
42
|
-
if (!value) continue // Skip missing colors
|
|
43
|
-
|
|
44
|
-
const contrast =
|
|
45
|
-
branding.colors[`${name}Contrast`] ??
|
|
46
|
-
pickContrastColor(value, contrastLight, contrastDark)
|
|
47
|
-
|
|
48
|
-
const hue = branding.colors[`${name}Hue`] ?? extractHue(value)
|
|
49
|
-
|
|
50
|
-
yield `--branding-color-${name}: ${value.r} ${value.g} ${value.b};`
|
|
51
|
-
yield `--branding-color-${name}-contrast: ${contrast.r} ${contrast.g} ${contrast.b};`
|
|
52
|
-
yield `--branding-color-${name}-hue: ${hue};`
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
}
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import type { CustomizationData } from '@atproto/oauth-provider-api'
|
|
2
|
-
import type { Customization } from './customization.js'
|
|
3
|
-
|
|
4
|
-
export function buildCustomizationData({
|
|
5
|
-
branding,
|
|
6
|
-
availableUserDomains,
|
|
7
|
-
inviteCodeRequired,
|
|
8
|
-
show2FaWarningOnEmailUpdate,
|
|
9
|
-
hcaptcha,
|
|
10
|
-
}: Customization): CustomizationData {
|
|
11
|
-
// @NOTE the front end does not need colors here as they will be injected as
|
|
12
|
-
// CSS variables.
|
|
13
|
-
// @NOTE We only copy the values explicitly needed to avoid leaking sensitive
|
|
14
|
-
// data (in case the caller passed more than what we expect).
|
|
15
|
-
return {
|
|
16
|
-
availableUserDomains,
|
|
17
|
-
inviteCodeRequired,
|
|
18
|
-
show2FaWarningOnEmailUpdate,
|
|
19
|
-
hcaptchaSiteKey: hcaptcha?.siteKey,
|
|
20
|
-
name: branding?.name,
|
|
21
|
-
logo: branding?.logo,
|
|
22
|
-
links: branding?.links,
|
|
23
|
-
}
|
|
24
|
-
}
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
import { z } from 'zod'
|
|
2
|
-
import { colorHueSchema } from '../types/color-hue.js'
|
|
3
|
-
import { rgbColorSchema } from '../types/rgb-color.js'
|
|
4
|
-
|
|
5
|
-
export const COLOR_NAMES = [
|
|
6
|
-
'primary',
|
|
7
|
-
'error',
|
|
8
|
-
'warning',
|
|
9
|
-
'info',
|
|
10
|
-
'success',
|
|
11
|
-
] as const
|
|
12
|
-
export type ColorName = (typeof COLOR_NAMES)[number]
|
|
13
|
-
|
|
14
|
-
export const colorsSchema = z
|
|
15
|
-
.object({
|
|
16
|
-
// The "light" and "dark" colors are used as default for unspecified
|
|
17
|
-
// contrast colors. The color that has the highest contrast ratio with the
|
|
18
|
-
// color base will be used. e.G. If "primary" is specified but
|
|
19
|
-
// "primaryContrast" is not, then the contrast color will be either "light"
|
|
20
|
-
// or "dark" depending on which one has the highest contrast ratio with
|
|
21
|
-
// "primary".
|
|
22
|
-
light: rgbColorSchema.optional(),
|
|
23
|
-
dark: rgbColorSchema.optional(),
|
|
24
|
-
|
|
25
|
-
// The "contrastSaturation" is used to compute the saturation of the
|
|
26
|
-
// "contrast" color. The "contrast" color is a (dynamic) color derived from
|
|
27
|
-
// the "primaryHue" color with the specified saturation and a variable
|
|
28
|
-
// lightness. "color-contrast-900" is used for default text, while
|
|
29
|
-
// "color-contrast-0" is used for the page background.
|
|
30
|
-
contrastSaturation: z.number().min(0).max(100).optional(),
|
|
31
|
-
})
|
|
32
|
-
.extend(
|
|
33
|
-
Object.fromEntries(
|
|
34
|
-
COLOR_NAMES.map((name) => [name, rgbColorSchema.optional()]),
|
|
35
|
-
) as Record<ColorName, z.ZodOptional<typeof rgbColorSchema>>,
|
|
36
|
-
)
|
|
37
|
-
.extend(
|
|
38
|
-
Object.fromEntries(
|
|
39
|
-
COLOR_NAMES.map((name) => [`${name}Contrast`, rgbColorSchema.optional()]),
|
|
40
|
-
) as Record<`${ColorName}Contrast`, z.ZodOptional<typeof rgbColorSchema>>,
|
|
41
|
-
)
|
|
42
|
-
.extend(
|
|
43
|
-
Object.fromEntries(
|
|
44
|
-
COLOR_NAMES.map((name) => [`${name}Hue`, colorHueSchema.optional()]),
|
|
45
|
-
) as Record<`${ColorName}Hue`, z.ZodOptional<typeof colorHueSchema>>,
|
|
46
|
-
)
|
|
47
|
-
|
|
48
|
-
export type Colors = z.infer<typeof colorsSchema>
|