@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.
Files changed (124) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/dist/account/account-manager.d.ts +2 -2
  3. package/dist/account/account-manager.d.ts.map +1 -1
  4. package/dist/account/account-manager.js.map +1 -1
  5. package/dist/account/account-store.d.ts +19 -6
  6. package/dist/account/account-store.d.ts.map +1 -1
  7. package/dist/account/account-store.js +16 -1
  8. package/dist/account/account-store.js.map +1 -1
  9. package/dist/assets/app/bundle-manifest.json +3 -3
  10. package/dist/assets/app/main.css +1 -1
  11. package/dist/assets/app/main.js +3 -3
  12. package/dist/assets/app/main.js.map +1 -1
  13. package/dist/client/client-auth.d.ts.map +1 -1
  14. package/dist/client/client-auth.js +2 -2
  15. package/dist/client/client-auth.js.map +1 -1
  16. package/dist/client/client-manager.d.ts.map +1 -1
  17. package/dist/client/client-manager.js +3 -1
  18. package/dist/client/client-manager.js.map +1 -1
  19. package/dist/client/client.d.ts.map +1 -1
  20. package/dist/client/client.js +3 -3
  21. package/dist/client/client.js.map +1 -1
  22. package/dist/dpop/dpop-manager.d.ts.map +1 -1
  23. package/dist/dpop/dpop-manager.js +3 -3
  24. package/dist/dpop/dpop-manager.js.map +1 -1
  25. package/dist/errors/invalid-token-error.d.ts.map +1 -1
  26. package/dist/errors/invalid-token-error.js +3 -2
  27. package/dist/errors/invalid-token-error.js.map +1 -1
  28. package/dist/errors/second-authentication-factor-required-error.d.ts +13 -0
  29. package/dist/errors/second-authentication-factor-required-error.d.ts.map +1 -0
  30. package/dist/errors/second-authentication-factor-required-error.js +23 -0
  31. package/dist/errors/second-authentication-factor-required-error.js.map +1 -0
  32. package/dist/lib/util/authorization-header.js +1 -1
  33. package/dist/lib/util/authorization-header.js.map +1 -1
  34. package/dist/metadata/build-metadata.d.ts.map +1 -1
  35. package/dist/metadata/build-metadata.js +2 -0
  36. package/dist/metadata/build-metadata.js.map +1 -1
  37. package/dist/oauth-errors.d.ts +1 -0
  38. package/dist/oauth-errors.d.ts.map +1 -1
  39. package/dist/oauth-errors.js +3 -1
  40. package/dist/oauth-errors.js.map +1 -1
  41. package/dist/oauth-provider.d.ts +101 -4
  42. package/dist/oauth-provider.d.ts.map +1 -1
  43. package/dist/oauth-provider.js +98 -110
  44. package/dist/oauth-provider.js.map +1 -1
  45. package/dist/output/{send-authorize-page.d.ts → build-authorize-data.d.ts} +2 -5
  46. package/dist/output/build-authorize-data.d.ts.map +1 -0
  47. package/dist/output/build-authorize-data.js +22 -0
  48. package/dist/output/build-authorize-data.js.map +1 -0
  49. package/dist/output/build-error-payload.d.ts.map +1 -1
  50. package/dist/output/build-error-payload.js +4 -3
  51. package/dist/output/build-error-payload.js.map +1 -1
  52. package/dist/output/customization.d.ts +2 -12
  53. package/dist/output/customization.d.ts.map +1 -1
  54. package/dist/output/customization.js +59 -32
  55. package/dist/output/customization.js.map +1 -1
  56. package/dist/output/output-manager.d.ts +16 -0
  57. package/dist/output/output-manager.d.ts.map +1 -0
  58. package/dist/output/output-manager.js +69 -0
  59. package/dist/output/output-manager.js.map +1 -0
  60. package/dist/output/send-web-page.d.ts +1 -1
  61. package/dist/output/send-web-page.d.ts.map +1 -1
  62. package/dist/output/send-web-page.js +3 -2
  63. package/dist/output/send-web-page.js.map +1 -1
  64. package/package.json +6 -6
  65. package/src/account/account-manager.ts +2 -2
  66. package/src/account/account-store.ts +12 -6
  67. package/src/assets/app/components/accept-form.tsx +86 -83
  68. package/src/assets/app/components/account-picker.tsx +98 -79
  69. package/src/assets/app/components/button.tsx +34 -0
  70. package/src/assets/app/components/client-identifier.tsx +12 -13
  71. package/src/assets/app/components/fieldset.tsx +26 -0
  72. package/src/assets/app/components/form-card.tsx +47 -0
  73. package/src/assets/app/components/help-card.tsx +1 -1
  74. package/src/assets/app/components/icons/alert-icon.tsx +5 -0
  75. package/src/assets/app/components/icons/at-symbol-icon.tsx +5 -0
  76. package/src/assets/app/components/icons/caret-right-icon.tsx +5 -0
  77. package/src/assets/app/components/icons/lock-icon.tsx +5 -0
  78. package/src/assets/app/components/icons/token-icon.tsx +5 -0
  79. package/src/assets/app/components/icons/util.tsx +17 -0
  80. package/src/assets/app/components/info-card.tsx +45 -0
  81. package/src/assets/app/components/input-checkbox.tsx +47 -0
  82. package/src/assets/app/components/input-container.tsx +37 -0
  83. package/src/assets/app/components/input-layout.tsx +47 -0
  84. package/src/assets/app/components/input-text.tsx +69 -0
  85. package/src/assets/app/components/layout-title-page.tsx +33 -16
  86. package/src/assets/app/components/layout-welcome.tsx +30 -14
  87. package/src/assets/app/components/sign-in-form.tsx +214 -196
  88. package/src/assets/app/components/sign-up-account-form.tsx +101 -117
  89. package/src/assets/app/components/sign-up-disclaimer.tsx +1 -1
  90. package/src/assets/app/hooks/use-api.ts +2 -0
  91. package/src/assets/app/lib/api.ts +49 -14
  92. package/src/assets/app/lib/clsx.ts +6 -1
  93. package/src/assets/app/lib/util.ts +3 -0
  94. package/src/assets/app/main.css +2 -1
  95. package/src/assets/app/views/accept-view.tsx +4 -3
  96. package/src/assets/app/views/authorize-view.tsx +8 -4
  97. package/src/assets/app/views/error-view.tsx +24 -15
  98. package/src/assets/app/views/sign-in-view.tsx +5 -15
  99. package/src/assets/app/views/sign-up-view.tsx +3 -10
  100. package/src/assets/app/views/welcome-view.tsx +11 -18
  101. package/src/client/client-auth.ts +3 -2
  102. package/src/client/client-manager.ts +2 -1
  103. package/src/client/client.ts +3 -1
  104. package/src/dpop/dpop-manager.ts +3 -2
  105. package/src/errors/invalid-token-error.ts +3 -1
  106. package/src/errors/second-authentication-factor-required-error.ts +25 -0
  107. package/src/lib/util/authorization-header.ts +1 -1
  108. package/src/metadata/build-metadata.ts +3 -0
  109. package/src/oauth-errors.ts +1 -0
  110. package/src/oauth-provider.ts +110 -99
  111. package/src/output/{send-authorize-page.ts → build-authorize-data.ts} +3 -43
  112. package/src/output/build-error-payload.ts +3 -1
  113. package/src/output/customization.ts +67 -45
  114. package/src/output/output-manager.ts +87 -0
  115. package/src/output/send-web-page.ts +4 -3
  116. package/tailwind.config.js +14 -1
  117. package/dist/output/send-authorize-page.d.ts.map +0 -1
  118. package/dist/output/send-authorize-page.js +0 -49
  119. package/dist/output/send-authorize-page.js.map +0 -1
  120. package/dist/output/send-error-page.d.ts +0 -5
  121. package/dist/output/send-error-page.d.ts.map +0 -1
  122. package/dist/output/send-error-page.js +0 -31
  123. package/dist/output/send-error-page.js.map +0 -1
  124. package/src/output/send-error-page.ts +0 -41
@@ -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}
@@ -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 { JOSEError } from 'jose/errors'
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(' ', 2))
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
  }
@@ -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'
@@ -24,8 +24,9 @@ import {
24
24
  AccountInfo,
25
25
  AccountStore,
26
26
  DeviceAccountInfo,
27
- LoginCredentials,
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(): 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
- parameters.login_hint === account.sub,
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: LoginCredentials,
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 dynamicJson = <T, TReq extends Req, TRes extends Res, Json>(
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
- dynamicJson(async function (req, _res) {
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
- dynamicJson(async function (req, _res) {
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
- dynamicJson(async function (req, res) {
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
- dynamicJson(async function (req, res) {
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
- dynamicJson(async function (req, _res) {
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': dynamicJson(async function (_req, _res) {
1246
+ 'application/json': jsonHandler(async function (_req, _res) {
1208
1247
  return this.data
1209
1248
  }),
1210
- 'application/jwt': dynamicJson(async function (_req, res) {
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('/oauth/authorize', async function (req, res) {
1224
- try {
1225
- res.setHeader('Cache-Control', 'no-store')
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 await sendAuthorizeRedirect(res, data)
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 await sendAuthorizePage(res, data, server.customization)
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
- } catch (err) {
1252
- await onError?.(req, res, err, 'Failed to setup authorize')
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: z.object({
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('/oauth/authorize/sign-in', async function (req, res) {
1272
- validateFetchMode(req, res, ['same-origin'])
1273
- validateSameOrigin(req, res, issuerOrigin)
1274
-
1275
- const input = await validateRequestPayload(req, signInPayloadSchema)
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
- const { deviceId } = await deviceManager.load(req, res)
1302
+ const input = await validateRequestPayload(req, signInPayloadSchema)
1289
1303
 
1290
- const { account, info } = await server.signIn(deviceId, input.credentials)
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
- // Prevent fixation attacks
1293
- await deviceManager.rotate(req, res, deviceId)
1315
+ const { deviceId } = await deviceManager.load(req, res)
1294
1316
 
1295
- return writeJson(res, {
1296
- account,
1297
- consentRequired: !info.authorizedClients.includes(input.client_id),
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('/oauth/authorize/accept', async function (req, res) {
1309
- try {
1310
- res.setHeader('Cache-Control', 'no-store')
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
- } catch (err) {
1347
- await onError?.(req, res, err, 'Failed to accept authorization request')
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('/oauth/authorize/reject', async function (req, res) {
1362
- try {
1363
- res.setHeader('Cache-Control', 'no-store')
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
- } catch (err) {
1399
- await onError?.(req, res, err, 'Failed to reject authorization request')
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(data: AuthorizationResultAuthorize): AuthorizeData {
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 { JOSEError } from 'jose/errors'
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