@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.
Files changed (118) hide show
  1. package/CHANGELOG.md +20 -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/build-authorize-data.d.ts +40 -0
  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 +7 -7
  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/src/assets/app/components/error-card.tsx +0 -41
  118. 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