@atproto/oauth-provider 0.1.0 → 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- package/CHANGELOG.md +13 -0
- package/dist/account/account-manager.d.ts +2 -2
- package/dist/account/account-manager.d.ts.map +1 -1
- package/dist/account/account-manager.js.map +1 -1
- package/dist/account/account-store.d.ts +19 -6
- package/dist/account/account-store.d.ts.map +1 -1
- package/dist/account/account-store.js +16 -1
- package/dist/account/account-store.js.map +1 -1
- package/dist/assets/app/bundle-manifest.json +3 -3
- package/dist/assets/app/main.css +1 -1
- package/dist/assets/app/main.js +3 -3
- package/dist/assets/app/main.js.map +1 -1
- package/dist/client/client-auth.d.ts.map +1 -1
- package/dist/client/client-auth.js +2 -2
- package/dist/client/client-auth.js.map +1 -1
- package/dist/client/client-manager.d.ts.map +1 -1
- package/dist/client/client-manager.js +3 -1
- package/dist/client/client-manager.js.map +1 -1
- package/dist/client/client.d.ts.map +1 -1
- package/dist/client/client.js +3 -3
- package/dist/client/client.js.map +1 -1
- package/dist/dpop/dpop-manager.d.ts.map +1 -1
- package/dist/dpop/dpop-manager.js +3 -3
- package/dist/dpop/dpop-manager.js.map +1 -1
- package/dist/errors/invalid-token-error.d.ts.map +1 -1
- package/dist/errors/invalid-token-error.js +3 -2
- package/dist/errors/invalid-token-error.js.map +1 -1
- package/dist/errors/second-authentication-factor-required-error.d.ts +13 -0
- package/dist/errors/second-authentication-factor-required-error.d.ts.map +1 -0
- package/dist/errors/second-authentication-factor-required-error.js +23 -0
- package/dist/errors/second-authentication-factor-required-error.js.map +1 -0
- package/dist/lib/util/authorization-header.js +1 -1
- package/dist/lib/util/authorization-header.js.map +1 -1
- package/dist/metadata/build-metadata.d.ts.map +1 -1
- package/dist/metadata/build-metadata.js +2 -0
- package/dist/metadata/build-metadata.js.map +1 -1
- package/dist/oauth-errors.d.ts +1 -0
- package/dist/oauth-errors.d.ts.map +1 -1
- package/dist/oauth-errors.js +3 -1
- package/dist/oauth-errors.js.map +1 -1
- package/dist/oauth-provider.d.ts +101 -4
- package/dist/oauth-provider.d.ts.map +1 -1
- package/dist/oauth-provider.js +98 -110
- package/dist/oauth-provider.js.map +1 -1
- package/dist/output/{send-authorize-page.d.ts → build-authorize-data.d.ts} +2 -5
- package/dist/output/build-authorize-data.d.ts.map +1 -0
- package/dist/output/build-authorize-data.js +22 -0
- package/dist/output/build-authorize-data.js.map +1 -0
- package/dist/output/build-error-payload.d.ts.map +1 -1
- package/dist/output/build-error-payload.js +4 -3
- package/dist/output/build-error-payload.js.map +1 -1
- package/dist/output/customization.d.ts +2 -12
- package/dist/output/customization.d.ts.map +1 -1
- package/dist/output/customization.js +59 -32
- package/dist/output/customization.js.map +1 -1
- package/dist/output/output-manager.d.ts +16 -0
- package/dist/output/output-manager.d.ts.map +1 -0
- package/dist/output/output-manager.js +69 -0
- package/dist/output/output-manager.js.map +1 -0
- 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 +3 -2
- package/dist/output/send-web-page.js.map +1 -1
- package/package.json +6 -6
- package/src/account/account-manager.ts +2 -2
- package/src/account/account-store.ts +12 -6
- package/src/assets/app/components/accept-form.tsx +86 -83
- package/src/assets/app/components/account-picker.tsx +98 -79
- package/src/assets/app/components/button.tsx +34 -0
- package/src/assets/app/components/client-identifier.tsx +12 -13
- package/src/assets/app/components/fieldset.tsx +26 -0
- package/src/assets/app/components/form-card.tsx +47 -0
- package/src/assets/app/components/help-card.tsx +1 -1
- package/src/assets/app/components/icons/alert-icon.tsx +5 -0
- package/src/assets/app/components/icons/at-symbol-icon.tsx +5 -0
- package/src/assets/app/components/icons/caret-right-icon.tsx +5 -0
- package/src/assets/app/components/icons/lock-icon.tsx +5 -0
- package/src/assets/app/components/icons/token-icon.tsx +5 -0
- package/src/assets/app/components/icons/util.tsx +17 -0
- package/src/assets/app/components/info-card.tsx +45 -0
- package/src/assets/app/components/input-checkbox.tsx +47 -0
- package/src/assets/app/components/input-container.tsx +37 -0
- package/src/assets/app/components/input-layout.tsx +47 -0
- package/src/assets/app/components/input-text.tsx +69 -0
- package/src/assets/app/components/layout-title-page.tsx +33 -16
- package/src/assets/app/components/layout-welcome.tsx +30 -14
- package/src/assets/app/components/sign-in-form.tsx +214 -196
- package/src/assets/app/components/sign-up-account-form.tsx +101 -117
- package/src/assets/app/components/sign-up-disclaimer.tsx +1 -1
- package/src/assets/app/hooks/use-api.ts +2 -0
- package/src/assets/app/lib/api.ts +49 -14
- package/src/assets/app/lib/clsx.ts +6 -1
- package/src/assets/app/lib/util.ts +3 -0
- package/src/assets/app/main.css +2 -1
- package/src/assets/app/views/accept-view.tsx +4 -3
- package/src/assets/app/views/authorize-view.tsx +8 -4
- package/src/assets/app/views/error-view.tsx +24 -15
- package/src/assets/app/views/sign-in-view.tsx +5 -15
- package/src/assets/app/views/sign-up-view.tsx +3 -10
- package/src/assets/app/views/welcome-view.tsx +11 -18
- package/src/client/client-auth.ts +3 -2
- package/src/client/client-manager.ts +2 -1
- package/src/client/client.ts +3 -1
- package/src/dpop/dpop-manager.ts +3 -2
- package/src/errors/invalid-token-error.ts +3 -1
- package/src/errors/second-authentication-factor-required-error.ts +25 -0
- package/src/lib/util/authorization-header.ts +1 -1
- package/src/metadata/build-metadata.ts +3 -0
- package/src/oauth-errors.ts +1 -0
- package/src/oauth-provider.ts +110 -99
- package/src/output/{send-authorize-page.ts → build-authorize-data.ts} +3 -43
- package/src/output/build-error-payload.ts +3 -1
- package/src/output/customization.ts +67 -45
- package/src/output/output-manager.ts +87 -0
- package/src/output/send-web-page.ts +4 -3
- package/tailwind.config.js +14 -1
- package/dist/output/send-authorize-page.d.ts.map +0 -1
- package/dist/output/send-authorize-page.js +0 -49
- package/dist/output/send-authorize-page.js.map +0 -1
- package/dist/output/send-error-page.d.ts +0 -5
- package/dist/output/send-error-page.d.ts.map +0 -1
- package/dist/output/send-error-page.js +0 -31
- package/dist/output/send-error-page.js.map +0 -1
- package/src/output/send-error-page.ts +0 -41
package/src/client/client.ts
CHANGED
@@ -9,6 +9,7 @@ import {
|
|
9
9
|
UnsecuredJWT,
|
10
10
|
createLocalJWKSet,
|
11
11
|
createRemoteJWKSet,
|
12
|
+
errors,
|
12
13
|
jwtVerify,
|
13
14
|
type JWTPayload,
|
14
15
|
type JWTVerifyGetKey,
|
@@ -18,7 +19,6 @@ import {
|
|
18
19
|
type ResolvedKey,
|
19
20
|
type UnsecuredResult,
|
20
21
|
} from 'jose'
|
21
|
-
import { JOSEError } from 'jose/errors'
|
22
22
|
|
23
23
|
import { CLIENT_ASSERTION_MAX_AGE, JAR_MAX_AGE } from '../constants.js'
|
24
24
|
import { InvalidClientError } from '../errors/invalid-client-error.js'
|
@@ -28,6 +28,8 @@ import { ClientAuth, authJwkThumbprint } from './client-auth.js'
|
|
28
28
|
import { ClientId } from './client-id.js'
|
29
29
|
import { ClientInfo } from './client-info.js'
|
30
30
|
|
31
|
+
const { JOSEError } = errors
|
32
|
+
|
31
33
|
export class Client {
|
32
34
|
/**
|
33
35
|
* @see {@link https://www.iana.org/assignments/oauth-parameters/oauth-parameters.xhtml#token-endpoint-auth-method}
|
package/src/dpop/dpop-manager.ts
CHANGED
@@ -1,13 +1,14 @@
|
|
1
1
|
import { createHash } from 'node:crypto'
|
2
2
|
|
3
|
-
import { EmbeddedJWK, calculateJwkThumbprint, jwtVerify } from 'jose'
|
4
|
-
import { JOSEError } from 'jose/errors'
|
3
|
+
import { EmbeddedJWK, calculateJwkThumbprint, errors, jwtVerify } from 'jose'
|
5
4
|
|
6
5
|
import { DPOP_NONCE_MAX_AGE } from '../constants.js'
|
7
6
|
import { InvalidDpopProofError } from '../errors/invalid-dpop-proof-error.js'
|
8
7
|
import { UseDpopNonceError } from '../errors/use-dpop-nonce-error.js'
|
9
8
|
import { DpopNonce, DpopNonceInput } from './dpop-nonce.js'
|
10
9
|
|
10
|
+
const { JOSEError } = errors
|
11
|
+
|
11
12
|
export { DpopNonce, type DpopNonceInput }
|
12
13
|
export type DpopManagerOptions = {
|
13
14
|
/**
|
@@ -1,10 +1,12 @@
|
|
1
1
|
import { JwtVerifyError } from '@atproto/jwk'
|
2
|
-
import {
|
2
|
+
import { errors } from 'jose'
|
3
3
|
import { ZodError } from 'zod'
|
4
4
|
|
5
5
|
import { OAuthError } from './oauth-error.js'
|
6
6
|
import { WWWAuthenticateError } from './www-authenticate-error.js'
|
7
7
|
|
8
|
+
const { JOSEError } = errors
|
9
|
+
|
8
10
|
/**
|
9
11
|
* @see
|
10
12
|
* {@link https://datatracker.ietf.org/doc/html/rfc6750#section-3.1 | RFC6750 - The WWW-Authenticate Response Header Field }
|
@@ -0,0 +1,25 @@
|
|
1
|
+
import { OAuthError } from './oauth-error.js'
|
2
|
+
|
3
|
+
export class SecondAuthenticationFactorRequiredError extends OAuthError {
|
4
|
+
constructor(
|
5
|
+
public type: 'emailOtp',
|
6
|
+
public hint: string,
|
7
|
+
cause?: unknown,
|
8
|
+
) {
|
9
|
+
const error = 'second_authentication_factor_required'
|
10
|
+
super(
|
11
|
+
error,
|
12
|
+
`${type} authentication factor required (hint: ${hint})`,
|
13
|
+
401,
|
14
|
+
cause,
|
15
|
+
)
|
16
|
+
}
|
17
|
+
|
18
|
+
toJSON() {
|
19
|
+
return {
|
20
|
+
...super.toJSON(),
|
21
|
+
type: this.type,
|
22
|
+
hint: this.hint,
|
23
|
+
} as const
|
24
|
+
}
|
25
|
+
}
|
@@ -18,7 +18,7 @@ export const parseAuthorizationHeader = (header?: string) => {
|
|
18
18
|
)
|
19
19
|
}
|
20
20
|
|
21
|
-
const parsed = authorizationHeaderSchema.safeParse(header.split(' '
|
21
|
+
const parsed = authorizationHeaderSchema.safeParse(header.split(' '))
|
22
22
|
if (!parsed.success) {
|
23
23
|
throw new InvalidRequestError('Invalid authorization header')
|
24
24
|
}
|
@@ -161,5 +161,8 @@ export function buildMetadata(
|
|
161
161
|
|
162
162
|
// https://datatracker.ietf.org/doc/html/draft-ietf-oauth-resource-metadata-05#section-4
|
163
163
|
protected_resources: customMetadata?.protected_resources,
|
164
|
+
|
165
|
+
// https://drafts.aaronpk.com/draft-parecki-oauth-client-id-metadata-document/draft-parecki-oauth-client-id-metadata-document.html
|
166
|
+
client_id_metadata_document_supported: true,
|
164
167
|
}
|
165
168
|
}
|
package/src/oauth-errors.ts
CHANGED
@@ -16,6 +16,7 @@ export { InvalidRedirectUriError } from './errors/invalid-redirect-uri-error.js'
|
|
16
16
|
export { InvalidRequestError } from './errors/invalid-request-error.js'
|
17
17
|
export { InvalidTokenError } from './errors/invalid-token-error.js'
|
18
18
|
export { LoginRequiredError } from './errors/login-required-error.js'
|
19
|
+
export { SecondAuthenticationFactorRequiredError } from './errors/second-authentication-factor-required-error.js'
|
19
20
|
export { UnauthorizedClientError } from './errors/unauthorized-client-error.js'
|
20
21
|
export { UseDpopNonceError } from './errors/use-dpop-nonce-error.js'
|
21
22
|
export { WWWAuthenticateError } from './errors/www-authenticate-error.js'
|
package/src/oauth-provider.ts
CHANGED
@@ -24,8 +24,9 @@ import {
|
|
24
24
|
AccountInfo,
|
25
25
|
AccountStore,
|
26
26
|
DeviceAccountInfo,
|
27
|
-
|
27
|
+
SignInCredentials,
|
28
28
|
asAccountStore,
|
29
|
+
signInCredentialsSchema,
|
29
30
|
} from './account/account-store.js'
|
30
31
|
import { Account } from './account/account.js'
|
31
32
|
import { authorizeAssetsMiddleware } from './assets/assets-middleware.js'
|
@@ -75,20 +76,17 @@ import { CustomMetadata, buildMetadata } from './metadata/build-metadata.js'
|
|
75
76
|
import { OAuthHooks } from './oauth-hooks.js'
|
76
77
|
import { OAuthVerifier, OAuthVerifierOptions } from './oauth-verifier.js'
|
77
78
|
import { Userinfo } from './oidc/userinfo.js'
|
79
|
+
import { AuthorizationResultAuthorize } from './output/build-authorize-data.js'
|
78
80
|
import {
|
79
81
|
buildErrorPayload,
|
80
82
|
buildErrorStatus,
|
81
83
|
} from './output/build-error-payload.js'
|
82
84
|
import { Customization } from './output/customization.js'
|
83
|
-
import {
|
84
|
-
AuthorizationResultAuthorize,
|
85
|
-
sendAuthorizePage,
|
86
|
-
} from './output/send-authorize-page.js'
|
85
|
+
import { OutputManager } from './output/output-manager.js'
|
87
86
|
import {
|
88
87
|
AuthorizationResultRedirect,
|
89
88
|
sendAuthorizeRedirect,
|
90
89
|
} from './output/send-authorize-redirect.js'
|
91
|
-
import { sendErrorPage } from './output/send-error-page.js'
|
92
90
|
import { oidcPayload } from './parameters/oidc-payload.js'
|
93
91
|
import { ReplayStore, ifReplayStore } from './replay/replay-store.js'
|
94
92
|
import { RequestInfo } from './request/request-info.js'
|
@@ -312,7 +310,7 @@ export class OAuthProvider extends OAuthVerifier {
|
|
312
310
|
)
|
313
311
|
}
|
314
312
|
|
315
|
-
get jwks()
|
313
|
+
get jwks() {
|
316
314
|
return this.keyset.publicJwks
|
317
315
|
}
|
318
316
|
|
@@ -637,13 +635,26 @@ export class OAuthProvider extends OAuthVerifier {
|
|
637
635
|
> {
|
638
636
|
const accounts = await this.accountManager.list(deviceId)
|
639
637
|
|
638
|
+
const hint = parameters.login_hint
|
639
|
+
const matchesHint = (account: Account): boolean =>
|
640
|
+
(!!account.sub && account.sub === hint) ||
|
641
|
+
(!!account.preferred_username && account.preferred_username === hint)
|
642
|
+
|
640
643
|
return accounts.map(({ account, info }) => ({
|
641
644
|
account,
|
642
645
|
info,
|
643
646
|
|
644
647
|
selected:
|
645
648
|
parameters.prompt !== 'select_account' &&
|
646
|
-
|
649
|
+
matchesHint(account) &&
|
650
|
+
// If an account uses the sub of another account as preferred_username,
|
651
|
+
// there might be multiple accounts matching the hint. In that case,
|
652
|
+
// selecting the account automatically may have unexpected results (i.e.
|
653
|
+
// not able to login using desired account).
|
654
|
+
accounts.reduce(
|
655
|
+
(acc, a) => acc + (matchesHint(a.account) ? 1 : 0),
|
656
|
+
0,
|
657
|
+
) === 1,
|
647
658
|
loginRequired:
|
648
659
|
parameters.prompt === 'login' ||
|
649
660
|
this.loginRequired(client, parameters, info),
|
@@ -651,14 +662,13 @@ export class OAuthProvider extends OAuthVerifier {
|
|
651
662
|
parameters.prompt === 'consent' ||
|
652
663
|
!info.authorizedClients.includes(client.id),
|
653
664
|
|
654
|
-
matchesHint:
|
655
|
-
parameters.login_hint === account.sub || parameters.login_hint == null,
|
665
|
+
matchesHint: hint == null || matchesHint(account),
|
656
666
|
}))
|
657
667
|
}
|
658
668
|
|
659
669
|
protected async signIn(
|
660
670
|
deviceId: DeviceId,
|
661
|
-
credentials:
|
671
|
+
credentials: SignInCredentials,
|
662
672
|
): Promise<AccountInfo> {
|
663
673
|
return this.accountManager.signIn(credentials, deviceId)
|
664
674
|
}
|
@@ -962,6 +972,7 @@ export class OAuthProvider extends OAuthVerifier {
|
|
962
972
|
: undefined,
|
963
973
|
}: RouterOptions<Req, Res> = {}) {
|
964
974
|
const deviceManager = new DeviceManager(this.deviceStore)
|
975
|
+
const outputManager = new OutputManager(this.customization)
|
965
976
|
|
966
977
|
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
967
978
|
const server = this
|
@@ -990,7 +1001,7 @@ export class OAuthProvider extends OAuthVerifier {
|
|
990
1001
|
* Wrap an OAuth endpoint in a middleware that will set the appropriate
|
991
1002
|
* response headers and format the response as JSON.
|
992
1003
|
*/
|
993
|
-
const
|
1004
|
+
const jsonHandler = <T, TReq extends Req, TRes extends Res, Json>(
|
994
1005
|
buildJson: (this: T, req: TReq, res: TRes) => Json | Promise<Json>,
|
995
1006
|
status?: number,
|
996
1007
|
): Handler<T, TReq, TRes> =>
|
@@ -1034,6 +1045,37 @@ export class OAuthProvider extends OAuthVerifier {
|
|
1034
1045
|
}
|
1035
1046
|
}
|
1036
1047
|
|
1048
|
+
const navigationHandler = <T, TReq extends Req, TRes extends Res>(
|
1049
|
+
handler: (this: T, req: TReq, res: TRes) => void | Promise<void>,
|
1050
|
+
): Handler<T, TReq, TRes> =>
|
1051
|
+
async function (req, res) {
|
1052
|
+
res.setHeader('Cache-Control', 'no-store')
|
1053
|
+
res.setHeader('Pragma', 'no-cache')
|
1054
|
+
|
1055
|
+
try {
|
1056
|
+
validateFetchMode(req, res, ['navigate'])
|
1057
|
+
validateSameOrigin(req, res, issuerOrigin)
|
1058
|
+
|
1059
|
+
await handler.call(this, req, res)
|
1060
|
+
|
1061
|
+
// Should never happen (fool proofing)
|
1062
|
+
if (!res.headersSent) {
|
1063
|
+
throw new Error('Navigation handler did not send a response')
|
1064
|
+
}
|
1065
|
+
} catch (err) {
|
1066
|
+
await onError?.(
|
1067
|
+
req,
|
1068
|
+
res,
|
1069
|
+
err,
|
1070
|
+
`Failed to handle navigation request to "${req.url}"`,
|
1071
|
+
)
|
1072
|
+
|
1073
|
+
if (!res.headersSent) {
|
1074
|
+
await outputManager.sendErrorPage(res, err)
|
1075
|
+
}
|
1076
|
+
}
|
1077
|
+
}
|
1078
|
+
|
1037
1079
|
//- Public OAuth endpoints
|
1038
1080
|
|
1039
1081
|
/*
|
@@ -1077,7 +1119,7 @@ export class OAuthProvider extends OAuthVerifier {
|
|
1077
1119
|
|
1078
1120
|
router.post(
|
1079
1121
|
'/oauth/par',
|
1080
|
-
|
1122
|
+
jsonHandler(async function (req, _res) {
|
1081
1123
|
const input = await validateRequestPayload(
|
1082
1124
|
req,
|
1083
1125
|
pushedAuthorizationRequestSchema,
|
@@ -1100,7 +1142,7 @@ export class OAuthProvider extends OAuthVerifier {
|
|
1100
1142
|
|
1101
1143
|
router.post(
|
1102
1144
|
'/oauth/token',
|
1103
|
-
|
1145
|
+
jsonHandler(async function (req, _res) {
|
1104
1146
|
const input = await validateRequestPayload(req, tokenRequestSchema)
|
1105
1147
|
|
1106
1148
|
const dpopJkt = await server.checkDpopProof(
|
@@ -1115,7 +1157,7 @@ export class OAuthProvider extends OAuthVerifier {
|
|
1115
1157
|
|
1116
1158
|
router.post(
|
1117
1159
|
'/oauth/revoke',
|
1118
|
-
|
1160
|
+
jsonHandler(async function (req, res) {
|
1119
1161
|
const input = await validateRequestPayload(req, revokeSchema)
|
1120
1162
|
|
1121
1163
|
try {
|
@@ -1128,10 +1170,7 @@ export class OAuthProvider extends OAuthVerifier {
|
|
1128
1170
|
|
1129
1171
|
router.get(
|
1130
1172
|
'/oauth/revoke',
|
1131
|
-
|
1132
|
-
validateFetchMode(req, res, ['navigate'])
|
1133
|
-
validateSameOrigin(req, res, issuerOrigin)
|
1134
|
-
|
1173
|
+
navigationHandler(async function (req, res) {
|
1135
1174
|
const query = Object.fromEntries(this.url.searchParams)
|
1136
1175
|
const input = revokeSchema.parse(query, { path: ['query'] })
|
1137
1176
|
|
@@ -1152,7 +1191,7 @@ export class OAuthProvider extends OAuthVerifier {
|
|
1152
1191
|
|
1153
1192
|
router.post(
|
1154
1193
|
'/oauth/introspect',
|
1155
|
-
|
1194
|
+
jsonHandler(async function (req, _res) {
|
1156
1195
|
const input = await validateRequestPayload(req, introspectSchema)
|
1157
1196
|
return server.introspect(input)
|
1158
1197
|
}),
|
@@ -1204,10 +1243,10 @@ export class OAuthProvider extends OAuthVerifier {
|
|
1204
1243
|
},
|
1205
1244
|
{
|
1206
1245
|
'': 'application/json',
|
1207
|
-
'application/json':
|
1246
|
+
'application/json': jsonHandler(async function (_req, _res) {
|
1208
1247
|
return this.data
|
1209
1248
|
}),
|
1210
|
-
'application/jwt':
|
1249
|
+
'application/jwt': jsonHandler(async function (_req, res) {
|
1211
1250
|
const jwt = await server.signUserinfo(this.data)
|
1212
1251
|
res.writeHead(200, { 'Content-Type': 'application/jwt' }).end(jwt)
|
1213
1252
|
return undefined
|
@@ -1220,13 +1259,9 @@ export class OAuthProvider extends OAuthVerifier {
|
|
1220
1259
|
|
1221
1260
|
router.use(authorizeAssetsMiddleware())
|
1222
1261
|
|
1223
|
-
router.get(
|
1224
|
-
|
1225
|
-
|
1226
|
-
|
1227
|
-
validateFetchMode(req, res, ['navigate'])
|
1228
|
-
validateSameOrigin(req, res, issuerOrigin)
|
1229
|
-
|
1262
|
+
router.get(
|
1263
|
+
'/oauth/authorize',
|
1264
|
+
navigationHandler(async function (req, res) {
|
1230
1265
|
const query = Object.fromEntries(this.url.searchParams)
|
1231
1266
|
const input = await authorizationRequestQuerySchema.parseAsync(query, {
|
1232
1267
|
path: ['query'],
|
@@ -1237,66 +1272,62 @@ export class OAuthProvider extends OAuthVerifier {
|
|
1237
1272
|
|
1238
1273
|
switch (true) {
|
1239
1274
|
case 'redirect' in data: {
|
1240
|
-
return
|
1275
|
+
return sendAuthorizeRedirect(res, data)
|
1241
1276
|
}
|
1242
1277
|
case 'authorize' in data: {
|
1243
1278
|
await setupCsrfToken(req, res, csrfCookie(data.authorize.uri))
|
1244
|
-
return
|
1279
|
+
return outputManager.sendAuthorizePage(res, data)
|
1245
1280
|
}
|
1246
1281
|
default: {
|
1247
1282
|
// Should never happen
|
1248
1283
|
throw new Error('Unexpected authorization result')
|
1249
1284
|
}
|
1250
1285
|
}
|
1251
|
-
}
|
1252
|
-
|
1253
|
-
|
1254
|
-
if (!res.headersSent) {
|
1255
|
-
await sendErrorPage(res, err, server.customization)
|
1256
|
-
}
|
1257
|
-
}
|
1258
|
-
})
|
1286
|
+
}),
|
1287
|
+
)
|
1259
1288
|
|
1260
1289
|
const signInPayloadSchema = z.object({
|
1261
1290
|
csrf_token: z.string(),
|
1262
1291
|
request_uri: requestUriSchema,
|
1263
1292
|
client_id: clientIdSchema,
|
1264
|
-
credentials:
|
1265
|
-
username: z.string(),
|
1266
|
-
password: z.string(),
|
1267
|
-
remember: z.boolean().optional().default(false),
|
1268
|
-
}),
|
1293
|
+
credentials: signInCredentialsSchema,
|
1269
1294
|
})
|
1270
1295
|
|
1271
|
-
router.post(
|
1272
|
-
|
1273
|
-
|
1274
|
-
|
1275
|
-
|
1276
|
-
|
1277
|
-
validateReferer(req, res, {
|
1278
|
-
origin: issuerOrigin,
|
1279
|
-
pathname: '/oauth/authorize',
|
1280
|
-
})
|
1281
|
-
validateCsrfToken(
|
1282
|
-
req,
|
1283
|
-
res,
|
1284
|
-
input.csrf_token,
|
1285
|
-
csrfCookie(input.request_uri),
|
1286
|
-
)
|
1296
|
+
router.post(
|
1297
|
+
'/oauth/authorize/sign-in',
|
1298
|
+
jsonHandler(async function (req, res) {
|
1299
|
+
validateFetchMode(req, res, ['same-origin'])
|
1300
|
+
validateSameOrigin(req, res, issuerOrigin)
|
1287
1301
|
|
1288
|
-
|
1302
|
+
const input = await validateRequestPayload(req, signInPayloadSchema)
|
1289
1303
|
|
1290
|
-
|
1304
|
+
validateReferer(req, res, {
|
1305
|
+
origin: issuerOrigin,
|
1306
|
+
pathname: '/oauth/authorize',
|
1307
|
+
})
|
1308
|
+
validateCsrfToken(
|
1309
|
+
req,
|
1310
|
+
res,
|
1311
|
+
input.csrf_token,
|
1312
|
+
csrfCookie(input.request_uri),
|
1313
|
+
)
|
1291
1314
|
|
1292
|
-
|
1293
|
-
await deviceManager.rotate(req, res, deviceId)
|
1315
|
+
const { deviceId } = await deviceManager.load(req, res)
|
1294
1316
|
|
1295
|
-
|
1296
|
-
|
1297
|
-
|
1298
|
-
|
1299
|
-
|
1317
|
+
const { account, info } = await server.signIn(
|
1318
|
+
deviceId,
|
1319
|
+
input.credentials,
|
1320
|
+
)
|
1321
|
+
|
1322
|
+
// Prevent fixation attacks
|
1323
|
+
await deviceManager.rotate(req, res, deviceId)
|
1324
|
+
|
1325
|
+
return {
|
1326
|
+
account,
|
1327
|
+
consentRequired: !info.authorizedClients.includes(input.client_id),
|
1328
|
+
}
|
1329
|
+
}),
|
1330
|
+
)
|
1300
1331
|
|
1301
1332
|
const acceptQuerySchema = z.object({
|
1302
1333
|
csrf_token: z.string(),
|
@@ -1305,13 +1336,9 @@ export class OAuthProvider extends OAuthVerifier {
|
|
1305
1336
|
account_sub: z.string(),
|
1306
1337
|
})
|
1307
1338
|
|
1308
|
-
router.get(
|
1309
|
-
|
1310
|
-
|
1311
|
-
|
1312
|
-
validateFetchMode(req, res, ['navigate'])
|
1313
|
-
validateSameOrigin(req, res, issuerOrigin)
|
1314
|
-
|
1339
|
+
router.get(
|
1340
|
+
'/oauth/authorize/accept',
|
1341
|
+
navigationHandler(async function (req, res) {
|
1315
1342
|
const query = Object.fromEntries(this.url.searchParams)
|
1316
1343
|
const input = await acceptQuerySchema.parseAsync(query, {
|
1317
1344
|
path: ['query'],
|
@@ -1343,14 +1370,8 @@ export class OAuthProvider extends OAuthVerifier {
|
|
1343
1370
|
)
|
1344
1371
|
|
1345
1372
|
return await sendAuthorizeRedirect(res, data)
|
1346
|
-
}
|
1347
|
-
|
1348
|
-
|
1349
|
-
if (!res.headersSent) {
|
1350
|
-
await sendErrorPage(res, err, server.customization)
|
1351
|
-
}
|
1352
|
-
}
|
1353
|
-
})
|
1373
|
+
}),
|
1374
|
+
)
|
1354
1375
|
|
1355
1376
|
const rejectQuerySchema = z.object({
|
1356
1377
|
csrf_token: z.string(),
|
@@ -1358,13 +1379,9 @@ export class OAuthProvider extends OAuthVerifier {
|
|
1358
1379
|
client_id: clientIdSchema,
|
1359
1380
|
})
|
1360
1381
|
|
1361
|
-
router.get(
|
1362
|
-
|
1363
|
-
|
1364
|
-
|
1365
|
-
validateFetchMode(req, res, ['navigate'])
|
1366
|
-
validateSameOrigin(req, res, issuerOrigin)
|
1367
|
-
|
1382
|
+
router.get(
|
1383
|
+
'/oauth/authorize/reject',
|
1384
|
+
navigationHandler(async function (req, res) {
|
1368
1385
|
const query = Object.fromEntries(this.url.searchParams)
|
1369
1386
|
const input = await rejectQuerySchema.parseAsync(query, {
|
1370
1387
|
path: ['query'],
|
@@ -1395,14 +1412,8 @@ export class OAuthProvider extends OAuthVerifier {
|
|
1395
1412
|
)
|
1396
1413
|
|
1397
1414
|
return await sendAuthorizeRedirect(res, data)
|
1398
|
-
}
|
1399
|
-
|
1400
|
-
|
1401
|
-
if (!res.headersSent) {
|
1402
|
-
await sendErrorPage(res, err, server.customization)
|
1403
|
-
}
|
1404
|
-
}
|
1405
|
-
})
|
1415
|
+
}),
|
1416
|
+
)
|
1406
1417
|
|
1407
1418
|
return router
|
1408
1419
|
}
|
@@ -2,20 +2,11 @@ import {
|
|
2
2
|
OAuthAuthenticationRequestParameters,
|
3
3
|
OAuthClientMetadata,
|
4
4
|
} from '@atproto/oauth-types'
|
5
|
-
import { ServerResponse } from 'node:http'
|
6
5
|
|
7
6
|
import { DeviceAccountInfo } from '../account/account-store.js'
|
8
7
|
import { Account } from '../account/account.js'
|
9
|
-
import { getAsset } from '../assets/index.js'
|
10
8
|
import { Client } from '../client/client.js'
|
11
|
-
import { cssCode, html } from '../lib/html/index.js'
|
12
9
|
import { RequestUri } from '../request/request-uri.js'
|
13
|
-
import {
|
14
|
-
Customization,
|
15
|
-
buildCustomizationCss,
|
16
|
-
buildCustomizationData,
|
17
|
-
} from './customization.js'
|
18
|
-
import { declareBackendData, sendWebPage } from './send-web-page.js'
|
19
10
|
|
20
11
|
export type AuthorizationResultAuthorize = {
|
21
12
|
issuer: string
|
@@ -57,7 +48,9 @@ export type AuthorizeData = {
|
|
57
48
|
sessions: Session[]
|
58
49
|
}
|
59
50
|
|
60
|
-
function buildAuthorizeData(
|
51
|
+
export function buildAuthorizeData(
|
52
|
+
data: AuthorizationResultAuthorize,
|
53
|
+
): AuthorizeData {
|
61
54
|
return {
|
62
55
|
clientId: data.client.id,
|
63
56
|
clientMetadata: data.client.metadata,
|
@@ -76,36 +69,3 @@ function buildAuthorizeData(data: AuthorizationResultAuthorize): AuthorizeData {
|
|
76
69
|
),
|
77
70
|
}
|
78
71
|
}
|
79
|
-
|
80
|
-
export async function sendAuthorizePage(
|
81
|
-
res: ServerResponse,
|
82
|
-
data: AuthorizationResultAuthorize,
|
83
|
-
customization?: Customization,
|
84
|
-
): Promise<void> {
|
85
|
-
res.setHeader('Cache-Control', 'no-store')
|
86
|
-
res.setHeader('Permissions-Policy', 'otp-credentials=*, document-domain=()')
|
87
|
-
|
88
|
-
const [jsAsset, cssAsset] = await Promise.all([
|
89
|
-
getAsset('main.js'),
|
90
|
-
getAsset('main.css'),
|
91
|
-
])
|
92
|
-
|
93
|
-
return sendWebPage(res, {
|
94
|
-
scripts: [
|
95
|
-
declareBackendData(
|
96
|
-
'__customizationData',
|
97
|
-
buildCustomizationData(customization),
|
98
|
-
),
|
99
|
-
declareBackendData('__authorizeData', buildAuthorizeData(data)),
|
100
|
-
jsAsset, // Last (to be able to read the global variables)
|
101
|
-
],
|
102
|
-
styles: [
|
103
|
-
cssAsset, // First (to be overridden by customization)
|
104
|
-
cssCode(buildCustomizationCss(customization)),
|
105
|
-
],
|
106
|
-
links: customization?.links,
|
107
|
-
htmlAttrs: { lang: 'en' },
|
108
|
-
title: 'Authorize',
|
109
|
-
body: html`<div id="root"></div>`,
|
110
|
-
})
|
111
|
-
}
|
@@ -1,9 +1,11 @@
|
|
1
1
|
import { JwtVerifyError } from '@atproto/jwk'
|
2
|
-
import {
|
2
|
+
import { errors } from 'jose'
|
3
3
|
import { ZodError } from 'zod'
|
4
4
|
|
5
5
|
import { OAuthError } from '../errors/oauth-error.js'
|
6
6
|
|
7
|
+
const { JOSEError } = errors
|
8
|
+
|
7
9
|
const INVALID_REQUEST = 'invalid_request'
|
8
10
|
const SERVER_ERROR = 'server_error'
|
9
11
|
|