@atproto/oauth-provider 0.1.0 → 0.1.2-rc.0
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 +20 -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/build-authorize-data.d.ts +40 -0
- 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 +7 -7
- 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/src/assets/app/components/error-card.tsx +0 -41
- 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
|
|