@atproto/oauth-provider 0.19.7 → 0.19.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +26 -0
- package/dist/errors/error-parser.js +5 -5
- package/dist/errors/error-parser.js.map +1 -1
- package/package.json +22 -18
- package/src/access-token/access-token-mode.ts +0 -4
- package/src/account/account-manager.ts +0 -591
- package/src/account/account-store.ts +0 -305
- package/src/account/sign-in-data.ts +0 -15
- package/src/account/sign-up-input.ts +0 -20
- package/src/client/client-auth.ts +0 -64
- package/src/client/client-data.ts +0 -9
- package/src/client/client-id.ts +0 -4
- package/src/client/client-info.ts +0 -13
- package/src/client/client-manager.ts +0 -772
- package/src/client/client-store.ts +0 -35
- package/src/client/client-utils.ts +0 -37
- package/src/client/client.ts +0 -394
- package/src/constants.ts +0 -80
- package/src/customization/branding.ts +0 -12
- package/src/customization/build-customization-css.ts +0 -55
- package/src/customization/build-customization-data.ts +0 -24
- package/src/customization/colors.ts +0 -48
- package/src/customization/customization.ts +0 -29
- package/src/customization/links.ts +0 -10
- package/src/device/device-data.ts +0 -11
- package/src/device/device-id.ts +0 -27
- package/src/device/device-manager.ts +0 -292
- package/src/device/device-store.ts +0 -31
- package/src/device/session-id.ts +0 -21
- package/src/dpop/dpop-manager.ts +0 -214
- package/src/dpop/dpop-nonce.ts +0 -121
- package/src/dpop/dpop-proof.ts +0 -6
- package/src/errors/access-denied-error.ts +0 -12
- package/src/errors/account-selection-required-error.ts +0 -12
- package/src/errors/authorization-error.ts +0 -45
- package/src/errors/consent-required-error.ts +0 -12
- package/src/errors/error-parser.ts +0 -147
- package/src/errors/handle-unavailable-error.ts +0 -23
- package/src/errors/invalid-authorization-details-error.ts +0 -27
- package/src/errors/invalid-client-error.ts +0 -20
- package/src/errors/invalid-client-id-error.ts +0 -31
- package/src/errors/invalid-client-metadata-error.ts +0 -68
- package/src/errors/invalid-credentials-error.ts +0 -29
- package/src/errors/invalid-dpop-key-binding-error.ts +0 -21
- package/src/errors/invalid-dpop-proof-error.ts +0 -13
- package/src/errors/invalid-grant-error.ts +0 -21
- package/src/errors/invalid-invite-code-error.ts +0 -10
- package/src/errors/invalid-redirect-uri-error.ts +0 -17
- package/src/errors/invalid-request-error.ts +0 -32
- package/src/errors/invalid-scope-error.ts +0 -15
- package/src/errors/invalid-token-error.ts +0 -60
- package/src/errors/login-required-error.ts +0 -12
- package/src/errors/oauth-error.ts +0 -28
- package/src/errors/second-authentication-factor-required-error.ts +0 -25
- package/src/errors/unauthorized-client-error.ts +0 -20
- package/src/errors/use-dpop-nonce-error.ts +0 -32
- package/src/errors/www-authenticate-error.ts +0 -64
- package/src/index.ts +0 -16
- package/src/lexicon/lexicon-data.ts +0 -11
- package/src/lexicon/lexicon-getter.ts +0 -55
- package/src/lexicon/lexicon-manager.ts +0 -116
- package/src/lexicon/lexicon-store.ts +0 -35
- package/src/lib/csp/index.ts +0 -101
- package/src/lib/hcaptcha.ts +0 -228
- package/src/lib/html/README.md +0 -9
- package/src/lib/html/build-document.ts +0 -151
- package/src/lib/html/escapers.ts +0 -66
- package/src/lib/html/html.ts +0 -56
- package/src/lib/html/hydration-data.ts +0 -19
- package/src/lib/html/index.ts +0 -5
- package/src/lib/html/tags.ts +0 -67
- package/src/lib/html/util.ts +0 -17
- package/src/lib/http/README.md +0 -11
- package/src/lib/http/accept.ts +0 -90
- package/src/lib/http/context.ts +0 -42
- package/src/lib/http/headers.ts +0 -15
- package/src/lib/http/index.ts +0 -10
- package/src/lib/http/method.ts +0 -18
- package/src/lib/http/middleware.ts +0 -169
- package/src/lib/http/parser.ts +0 -101
- package/src/lib/http/path.ts +0 -82
- package/src/lib/http/request.ts +0 -256
- package/src/lib/http/response.ts +0 -122
- package/src/lib/http/route.ts +0 -60
- package/src/lib/http/router.ts +0 -117
- package/src/lib/http/security-headers.ts +0 -100
- package/src/lib/http/stream.ts +0 -57
- package/src/lib/http/types.ts +0 -16
- package/src/lib/http/url.ts +0 -23
- package/src/lib/nsid.ts +0 -10
- package/src/lib/redis.ts +0 -25
- package/src/lib/util/authorization-header.ts +0 -28
- package/src/lib/util/cast.ts +0 -18
- package/src/lib/util/color.ts +0 -168
- package/src/lib/util/crypto.ts +0 -32
- package/src/lib/util/date.ts +0 -7
- package/src/lib/util/error.ts +0 -7
- package/src/lib/util/function.ts +0 -39
- package/src/lib/util/locale.ts +0 -12
- package/src/lib/util/object.ts +0 -23
- package/src/lib/util/redirect-uri.ts +0 -46
- package/src/lib/util/time.ts +0 -49
- package/src/lib/util/type.ts +0 -182
- package/src/lib/util/ui8.ts +0 -14
- package/src/lib/util/well-known.ts +0 -8
- package/src/lib/util/zod-error.ts +0 -26
- package/src/lib/write-form-redirect.ts +0 -58
- package/src/lib/write-html.ts +0 -70
- package/src/metadata/build-metadata.ts +0 -141
- package/src/oauth-client.ts +0 -3
- package/src/oauth-dpop.ts +0 -2
- package/src/oauth-errors.ts +0 -26
- package/src/oauth-hooks.ts +0 -519
- package/src/oauth-middleware.ts +0 -53
- package/src/oauth-provider.ts +0 -1110
- package/src/oauth-store.ts +0 -12
- package/src/oauth-verifier.ts +0 -260
- package/src/replay/replay-manager.ts +0 -51
- package/src/replay/replay-store-memory.ts +0 -36
- package/src/replay/replay-store-redis.ts +0 -30
- package/src/replay/replay-store.ts +0 -44
- package/src/request/code.ts +0 -23
- package/src/request/request-data.ts +0 -36
- package/src/request/request-id.ts +0 -22
- package/src/request/request-manager.ts +0 -521
- package/src/request/request-store.ts +0 -60
- package/src/request/request-uri.ts +0 -42
- package/src/result/authorization-redirect-parameters.ts +0 -24
- package/src/result/authorization-result-authorize-page.ts +0 -16
- package/src/result/authorization-result-redirect.ts +0 -8
- package/src/router/assets/assets-manifest.ts +0 -117
- package/src/router/assets/assets.ts +0 -124
- package/src/router/assets/csrf.ts +0 -79
- package/src/router/assets/send-account-page.ts +0 -23
- package/src/router/assets/send-authorization-page.ts +0 -42
- package/src/router/assets/send-cookie-error-page.ts +0 -21
- package/src/router/assets/send-error-page.ts +0 -25
- package/src/router/assets/send-redirect.ts +0 -140
- package/src/router/create-account-page-middleware.ts +0 -88
- package/src/router/create-api-middleware.ts +0 -1051
- package/src/router/create-authorization-page-middleware.ts +0 -281
- package/src/router/create-oauth-middleware.ts +0 -257
- package/src/router/error-handler.ts +0 -6
- package/src/router/middleware-options.ts +0 -9
- package/src/signer/access-token-payload.ts +0 -25
- package/src/signer/api-token-payload.ts +0 -18
- package/src/signer/signer.ts +0 -121
- package/src/token/refresh-token.ts +0 -30
- package/src/token/token-claims.ts +0 -22
- package/src/token/token-data.ts +0 -40
- package/src/token/token-id.ts +0 -25
- package/src/token/token-manager.ts +0 -427
- package/src/token/token-store.ts +0 -88
- package/src/types/authorization-response-error.ts +0 -27
- package/src/types/color-hue.ts +0 -3
- package/src/types/email-otp.ts +0 -3
- package/src/types/email.ts +0 -26
- package/src/types/handle.ts +0 -23
- package/src/types/invite-code.ts +0 -4
- package/src/types/par-response-error.ts +0 -25
- package/src/types/password.ts +0 -4
- package/src/types/rgb-color.ts +0 -21
- package/tsconfig.build.json +0 -8
- package/tsconfig.build.tsbuildinfo +0 -1
- package/tsconfig.json +0 -4
|
@@ -1,281 +0,0 @@
|
|
|
1
|
-
import type { IncomingMessage, ServerResponse } from 'node:http'
|
|
2
|
-
import {
|
|
3
|
-
OAuthAuthorizationRequestQuery,
|
|
4
|
-
oauthAuthorizationRequestQuerySchema,
|
|
5
|
-
} from '@atproto/oauth-types'
|
|
6
|
-
import { AuthorizationError } from '../errors/authorization-error.js'
|
|
7
|
-
import { InvalidRequestError } from '../errors/invalid-request-error.js'
|
|
8
|
-
import {
|
|
9
|
-
Middleware,
|
|
10
|
-
Router,
|
|
11
|
-
RouterCtx,
|
|
12
|
-
getCookie,
|
|
13
|
-
setCookie,
|
|
14
|
-
validateFetchDest,
|
|
15
|
-
validateFetchMode,
|
|
16
|
-
validateFetchSite,
|
|
17
|
-
validateOrigin,
|
|
18
|
-
validateReferrer,
|
|
19
|
-
} from '../lib/http/index.js'
|
|
20
|
-
import { SecurityHeadersOptions } from '../lib/http/security-headers.js'
|
|
21
|
-
import { formatError } from '../lib/util/error.js'
|
|
22
|
-
import type { Awaitable } from '../lib/util/type.js'
|
|
23
|
-
import { writeFormRedirect } from '../lib/write-form-redirect.js'
|
|
24
|
-
import type { OAuthProvider } from '../oauth-provider.js'
|
|
25
|
-
import { parseRequestUri, requestUriSchema } from '../request/request-uri.js'
|
|
26
|
-
import { sendAuthorizePageFactory } from './assets/send-authorization-page.js'
|
|
27
|
-
import { sendCookieErrorPageFactory } from './assets/send-cookie-error-page.js'
|
|
28
|
-
import { sendErrorPageFactory } from './assets/send-error-page.js'
|
|
29
|
-
import {
|
|
30
|
-
sendAuthorizationResultRedirect,
|
|
31
|
-
sendRedirect,
|
|
32
|
-
} from './assets/send-redirect.js'
|
|
33
|
-
import { parseRedirectUrl } from './create-api-middleware.js'
|
|
34
|
-
import type { MiddlewareOptions } from './middleware-options.js'
|
|
35
|
-
|
|
36
|
-
export function createAuthorizationPageMiddleware<
|
|
37
|
-
Ctx extends object | void = void,
|
|
38
|
-
Req extends IncomingMessage = IncomingMessage,
|
|
39
|
-
Res extends ServerResponse = ServerResponse,
|
|
40
|
-
>(
|
|
41
|
-
server: OAuthProvider,
|
|
42
|
-
{ onError }: MiddlewareOptions<Req, Res>,
|
|
43
|
-
): Middleware<Ctx, Req, Res> {
|
|
44
|
-
const issuerUrl = new URL(server.issuer)
|
|
45
|
-
const issuerOrigin = issuerUrl.origin
|
|
46
|
-
|
|
47
|
-
const securityOptions: SecurityHeadersOptions = {
|
|
48
|
-
hsts: issuerUrl.protocol === 'http:' ? false : undefined,
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
const sendAuthorizePage = sendAuthorizePageFactory(
|
|
52
|
-
server.customization,
|
|
53
|
-
securityOptions,
|
|
54
|
-
)
|
|
55
|
-
const sendErrorPage = sendErrorPageFactory(
|
|
56
|
-
server.customization,
|
|
57
|
-
securityOptions,
|
|
58
|
-
)
|
|
59
|
-
const sendCookieErrorPage = sendCookieErrorPageFactory(
|
|
60
|
-
server.customization,
|
|
61
|
-
securityOptions,
|
|
62
|
-
)
|
|
63
|
-
|
|
64
|
-
const router = new Router<Ctx, Req, Res>(issuerUrl)
|
|
65
|
-
|
|
66
|
-
router.get(
|
|
67
|
-
'/oauth/authorize',
|
|
68
|
-
withErrorHandler(async function (req, res) {
|
|
69
|
-
res.setHeader('Cache-Control', 'no-store')
|
|
70
|
-
res.setHeader('Pragma', 'no-cache')
|
|
71
|
-
|
|
72
|
-
// "same-origin" is required to support the redirect test logic below (as
|
|
73
|
-
// well as refreshing the authorization page).
|
|
74
|
-
// "same-site" supports hosting PDS and app on the same site but different
|
|
75
|
-
// origins (different subdomains).
|
|
76
|
-
validateFetchSite(req, ['same-origin', 'same-site', 'cross-site', 'none'])
|
|
77
|
-
validateFetchMode(req, ['navigate'])
|
|
78
|
-
validateFetchDest(req, ['document'])
|
|
79
|
-
validateOrigin(req, issuerOrigin)
|
|
80
|
-
|
|
81
|
-
// Do not perform any of the following logic if the request is invalid
|
|
82
|
-
const query = parseOAuthAuthorizationRequestQuery(this.url)
|
|
83
|
-
|
|
84
|
-
// @NOTE For some reason, even when loaded through a
|
|
85
|
-
// ASWebAuthenticationSession, iOS will sometimes fail to properly save
|
|
86
|
-
// cookies set during the rendering of the page. When this happens, the
|
|
87
|
-
// authorization page logic, which relies on cookies to maintain the session,
|
|
88
|
-
// will fail. To work around this, we perform an initial redirect to ourselves
|
|
89
|
-
// using a form GET submit, in an attempt to verify if the browser saves
|
|
90
|
-
// cookies on redirect or not. If it does, we proceed as normal. If it
|
|
91
|
-
// doesn't, we redirect the user back to the client with an error message.
|
|
92
|
-
if (
|
|
93
|
-
// Only for iOS users
|
|
94
|
-
req.headers['user-agent']?.includes('iPhone OS') &&
|
|
95
|
-
// Disabled if the user already passed the test, which means their browser preserves cookies on redirect
|
|
96
|
-
!(getCookie(req, 'cookie-test') === 'succeeded') &&
|
|
97
|
-
// Disabled if the user already has a session
|
|
98
|
-
!(await server.deviceManager.hasSession(req))
|
|
99
|
-
) {
|
|
100
|
-
// @TODO Another possible solution would be to avoid relying on cookies if we
|
|
101
|
-
// detect that they are not being preserved. This would mean that preserving
|
|
102
|
-
// sessions (SSO) would not be possible for browsers that don't preserve
|
|
103
|
-
// cookies on redirect, but at least the authorization request could still be
|
|
104
|
-
// completed. This was not implemented yet due to the extra complexity
|
|
105
|
-
// involved in supporting this.
|
|
106
|
-
|
|
107
|
-
// 1) When the user first comes here, we will test if their browser
|
|
108
|
-
// preserves cookies by redirecting back to ourselves
|
|
109
|
-
if (!this.url.searchParams.has('redirect-test')) {
|
|
110
|
-
// 2) Set a testing cookie
|
|
111
|
-
setCookie(res, 'cookie-test', 'testing', {
|
|
112
|
-
sameSite: 'lax',
|
|
113
|
-
httpOnly: true,
|
|
114
|
-
})
|
|
115
|
-
|
|
116
|
-
// 3) And send an auto-submit form redirecting back to ourselves
|
|
117
|
-
return writeFormRedirect(
|
|
118
|
-
res,
|
|
119
|
-
'get',
|
|
120
|
-
this.url.href,
|
|
121
|
-
// 4) We add an extra query parameter to trigger the test logic after
|
|
122
|
-
// the redirect occurred.
|
|
123
|
-
[...this.url.searchParams, ['redirect-test', '1']],
|
|
124
|
-
securityOptions,
|
|
125
|
-
)
|
|
126
|
-
} else {
|
|
127
|
-
// 5) We just got redirected back to ourselves. Verify that the
|
|
128
|
-
// browser preserved cookies during the redirect
|
|
129
|
-
if (getCookie(req, 'cookie-test')) {
|
|
130
|
-
// 6) Success! The browser preserved cookies. Proceed with the
|
|
131
|
-
// normal authorization flow.
|
|
132
|
-
|
|
133
|
-
// 7) Set a long lasting cookie to skip the test next time
|
|
134
|
-
setCookie(res, 'cookie-test', 'succeeded', {
|
|
135
|
-
sameSite: 'lax',
|
|
136
|
-
maxAge: 31 * 24 * 60 * 60,
|
|
137
|
-
httpOnly: true,
|
|
138
|
-
})
|
|
139
|
-
} else {
|
|
140
|
-
// The browser did NOT preserve cookies. We have to abort the
|
|
141
|
-
// authorization request.
|
|
142
|
-
|
|
143
|
-
if (this.url.searchParams.get('redirect-test') === '1') {
|
|
144
|
-
// 8) Show an error page to the user explaining the situation
|
|
145
|
-
|
|
146
|
-
// Give the browser another chance to save cookies after the use
|
|
147
|
-
// pressed "Continue"
|
|
148
|
-
setCookie(res, 'cookie-test', 'testing', {
|
|
149
|
-
sameSite: 'lax',
|
|
150
|
-
httpOnly: true,
|
|
151
|
-
})
|
|
152
|
-
|
|
153
|
-
// Make sure next time we reach the other branch and redirect back
|
|
154
|
-
// to the client
|
|
155
|
-
const continueUrl = new URL(this.url.href)
|
|
156
|
-
continueUrl.searchParams.set('redirect-test', '2')
|
|
157
|
-
return sendCookieErrorPage(req, res, { continueUrl })
|
|
158
|
-
} else {
|
|
159
|
-
// 9) Once the use acknowledges the error, redirect them back to
|
|
160
|
-
// the client with an error message.
|
|
161
|
-
|
|
162
|
-
// Allow the client to understand what happened (the `error`
|
|
163
|
-
// response parameter value is constrained by the OAuth2 spec)
|
|
164
|
-
const message = 'ERR_COOKIES_UNSUPPORTED'
|
|
165
|
-
|
|
166
|
-
// @NOTE AuthorizationError thrown here will be caught by the
|
|
167
|
-
// error handler middleware defined below, and cause a redirect
|
|
168
|
-
// back to the client with the error parameters.
|
|
169
|
-
if ('request_uri' in query) {
|
|
170
|
-
// Load and delete the authorization request
|
|
171
|
-
const requestUri = parseRequestUri(query.request_uri, {
|
|
172
|
-
path: ['query', 'request_uri'],
|
|
173
|
-
})
|
|
174
|
-
const data = await server.requestManager.get(
|
|
175
|
-
requestUri,
|
|
176
|
-
undefined,
|
|
177
|
-
query.client_id,
|
|
178
|
-
)
|
|
179
|
-
await server.requestManager.delete(requestUri)
|
|
180
|
-
throw new AuthorizationError(data.parameters, message)
|
|
181
|
-
} else if ('request' in query) {
|
|
182
|
-
const client = await server.clientManager.getClient(
|
|
183
|
-
query.client_id,
|
|
184
|
-
)
|
|
185
|
-
const parameters = await server.decodeJAR(client, query)
|
|
186
|
-
throw new AuthorizationError(parameters, message)
|
|
187
|
-
} else {
|
|
188
|
-
throw new AuthorizationError(query, message)
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
// Normal authorization flow
|
|
196
|
-
const device = await server.deviceManager.load(req, res)
|
|
197
|
-
|
|
198
|
-
const result = await server.authorize(query, device)
|
|
199
|
-
|
|
200
|
-
if ('redirect' in result) {
|
|
201
|
-
return sendAuthorizationResultRedirect(res, result, securityOptions)
|
|
202
|
-
} else {
|
|
203
|
-
return sendAuthorizePage(req, res, result)
|
|
204
|
-
}
|
|
205
|
-
}),
|
|
206
|
-
)
|
|
207
|
-
|
|
208
|
-
// This is a private endpoint that will be called by the user after the
|
|
209
|
-
// authorization request was either approved or denied. The logic performed
|
|
210
|
-
// here **could** be performed directly in the frontend. We decided to
|
|
211
|
-
// implement it here to avoid duplicating the logic.
|
|
212
|
-
router.get(
|
|
213
|
-
'/oauth/authorize/redirect',
|
|
214
|
-
withErrorHandler(async function (req, res) {
|
|
215
|
-
// Ensure we come from the authorization page
|
|
216
|
-
validateFetchSite(req, ['same-origin'])
|
|
217
|
-
validateFetchMode(req, ['navigate'])
|
|
218
|
-
validateFetchDest(req, ['document'])
|
|
219
|
-
validateOrigin(req, issuerOrigin)
|
|
220
|
-
|
|
221
|
-
const referrer = validateReferrer(req, {
|
|
222
|
-
origin: issuerOrigin,
|
|
223
|
-
pathname: '/oauth/authorize',
|
|
224
|
-
})
|
|
225
|
-
|
|
226
|
-
// Ensure we are coming from the authorization page
|
|
227
|
-
requestUriSchema.parse(referrer.searchParams.get('request_uri'))
|
|
228
|
-
|
|
229
|
-
return sendRedirect(res, parseRedirectUrl(this.url), securityOptions)
|
|
230
|
-
}),
|
|
231
|
-
)
|
|
232
|
-
|
|
233
|
-
return router.buildMiddleware()
|
|
234
|
-
|
|
235
|
-
function withErrorHandler<T extends RouterCtx>(
|
|
236
|
-
handler: (this: T, req: Req, res: Res) => Awaitable<void>,
|
|
237
|
-
): Middleware<T, Req, Res> {
|
|
238
|
-
return async function (req, res) {
|
|
239
|
-
try {
|
|
240
|
-
await handler.call(this, req, res)
|
|
241
|
-
} catch (err) {
|
|
242
|
-
onError?.(req, res, err, `Authorization Request Error`)
|
|
243
|
-
|
|
244
|
-
if (!res.headersSent) {
|
|
245
|
-
if (err instanceof AuthorizationError) {
|
|
246
|
-
return sendAuthorizationResultRedirect(
|
|
247
|
-
res,
|
|
248
|
-
{
|
|
249
|
-
issuer: server.issuer,
|
|
250
|
-
parameters: err.parameters,
|
|
251
|
-
redirect: err.toJSON(),
|
|
252
|
-
},
|
|
253
|
-
securityOptions,
|
|
254
|
-
)
|
|
255
|
-
} else {
|
|
256
|
-
return sendErrorPage(req, res, err)
|
|
257
|
-
}
|
|
258
|
-
} else if (!res.destroyed) {
|
|
259
|
-
res.end()
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
function parseOAuthAuthorizationRequestQuery(
|
|
267
|
-
url: URL,
|
|
268
|
-
): OAuthAuthorizationRequestQuery {
|
|
269
|
-
const query = Object.fromEntries(url.searchParams)
|
|
270
|
-
const result = oauthAuthorizationRequestQuerySchema.safeParse(query, {
|
|
271
|
-
path: ['query'],
|
|
272
|
-
})
|
|
273
|
-
|
|
274
|
-
if (!result.success) {
|
|
275
|
-
const message = 'Invalid request parameters'
|
|
276
|
-
const err = result.error
|
|
277
|
-
throw new InvalidRequestError(formatError(err, message), err)
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
return result.data
|
|
281
|
-
}
|
|
@@ -1,257 +0,0 @@
|
|
|
1
|
-
import type { IncomingMessage, ServerResponse } from 'node:http'
|
|
2
|
-
import {
|
|
3
|
-
oauthAuthorizationRequestParSchema,
|
|
4
|
-
oauthClientCredentialsSchema,
|
|
5
|
-
oauthTokenIdentificationSchema,
|
|
6
|
-
oauthTokenRequestSchema,
|
|
7
|
-
} from '@atproto/oauth-types'
|
|
8
|
-
import { buildErrorPayload, buildErrorStatus } from '../errors/error-parser.js'
|
|
9
|
-
import { InvalidClientError } from '../errors/invalid-client-error.js'
|
|
10
|
-
import { InvalidGrantError } from '../errors/invalid-grant-error.js'
|
|
11
|
-
import { InvalidRequestError } from '../errors/invalid-request-error.js'
|
|
12
|
-
import { WWWAuthenticateError } from '../errors/www-authenticate-error.js'
|
|
13
|
-
import {
|
|
14
|
-
Middleware,
|
|
15
|
-
Router,
|
|
16
|
-
cacheControlMiddleware,
|
|
17
|
-
combineMiddlewares,
|
|
18
|
-
jsonHandler,
|
|
19
|
-
parseHttpRequest,
|
|
20
|
-
staticJsonMiddleware,
|
|
21
|
-
} from '../lib/http/index.js'
|
|
22
|
-
import { formatError } from '../lib/util/error.js'
|
|
23
|
-
import { OAuthError } from '../oauth-errors.js'
|
|
24
|
-
import type { OAuthProvider } from '../oauth-provider.js'
|
|
25
|
-
import type { MiddlewareOptions } from './middleware-options.js'
|
|
26
|
-
|
|
27
|
-
// CORS preflight
|
|
28
|
-
const corsHeaders: Middleware = function (req, res, next) {
|
|
29
|
-
res.setHeader('Access-Control-Max-Age', '86400') // 1 day
|
|
30
|
-
|
|
31
|
-
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin
|
|
32
|
-
//
|
|
33
|
-
// > For requests without credentials, the literal value "*" can be
|
|
34
|
-
// > specified as a wildcard; the value tells browsers to allow
|
|
35
|
-
// > requesting code from any origin to access the resource.
|
|
36
|
-
// > Attempting to use the wildcard with credentials results in an
|
|
37
|
-
// > error.
|
|
38
|
-
//
|
|
39
|
-
// A "*" is safer to use than reflecting the request origin.
|
|
40
|
-
res.setHeader('Access-Control-Allow-Origin', '*')
|
|
41
|
-
|
|
42
|
-
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Methods
|
|
43
|
-
// > The value "*" only counts as a special wildcard value for
|
|
44
|
-
// > requests without credentials (requests without HTTP cookies or
|
|
45
|
-
// > HTTP authentication information). In requests with credentials,
|
|
46
|
-
// > it is treated as the literal method name "*" without special
|
|
47
|
-
// > semantics.
|
|
48
|
-
res.setHeader('Access-Control-Allow-Methods', '*')
|
|
49
|
-
|
|
50
|
-
res.setHeader('Access-Control-Allow-Headers', 'Content-Type,DPoP')
|
|
51
|
-
|
|
52
|
-
next()
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
const corsPreflight: Middleware = combineMiddlewares([
|
|
56
|
-
corsHeaders,
|
|
57
|
-
(req, res) => {
|
|
58
|
-
res.writeHead(200).end()
|
|
59
|
-
},
|
|
60
|
-
])
|
|
61
|
-
|
|
62
|
-
export function createOAuthMiddleware<
|
|
63
|
-
Ctx extends object | void = void,
|
|
64
|
-
Req extends IncomingMessage = IncomingMessage,
|
|
65
|
-
Res extends ServerResponse = ServerResponse,
|
|
66
|
-
>(
|
|
67
|
-
server: OAuthProvider,
|
|
68
|
-
{ onError }: MiddlewareOptions<Req, Res>,
|
|
69
|
-
): Middleware<Ctx, Req, Res> {
|
|
70
|
-
const router = new Router<Ctx, Req, Res>(new URL(server.issuer))
|
|
71
|
-
|
|
72
|
-
//- Public OAuth endpoints
|
|
73
|
-
|
|
74
|
-
router.options('/.well-known/oauth-authorization-server', corsPreflight)
|
|
75
|
-
router.get(
|
|
76
|
-
'/.well-known/oauth-authorization-server',
|
|
77
|
-
corsHeaders,
|
|
78
|
-
cacheControlMiddleware(300),
|
|
79
|
-
staticJsonMiddleware(server.metadata),
|
|
80
|
-
)
|
|
81
|
-
|
|
82
|
-
router.options('/oauth/jwks', corsPreflight)
|
|
83
|
-
router.get(
|
|
84
|
-
'/oauth/jwks',
|
|
85
|
-
corsHeaders,
|
|
86
|
-
cacheControlMiddleware(300),
|
|
87
|
-
staticJsonMiddleware(server.jwks),
|
|
88
|
-
)
|
|
89
|
-
|
|
90
|
-
router.options('/oauth/par', corsPreflight)
|
|
91
|
-
router.post(
|
|
92
|
-
'/oauth/par',
|
|
93
|
-
corsHeaders,
|
|
94
|
-
oauthHandler(async function (req) {
|
|
95
|
-
const payload = await parseHttpRequest(req, ['json', 'urlencoded'])
|
|
96
|
-
|
|
97
|
-
// https://datatracker.ietf.org/doc/html/rfc9126#name-error-response
|
|
98
|
-
// https://datatracker.ietf.org/doc/html/rfc6749#autoid-56
|
|
99
|
-
|
|
100
|
-
const credentials = await oauthClientCredentialsSchema
|
|
101
|
-
.parseAsync(payload, { path: ['body'] })
|
|
102
|
-
.catch((err) => throwInvalidClient(err, 'Client credentials missing'))
|
|
103
|
-
|
|
104
|
-
const authorizationRequest = await oauthAuthorizationRequestParSchema
|
|
105
|
-
.parseAsync(payload, { path: ['body'] })
|
|
106
|
-
.catch((err) =>
|
|
107
|
-
throwInvalidRequest(err, 'Invalid authorization request'),
|
|
108
|
-
)
|
|
109
|
-
|
|
110
|
-
const dpopProof = await server.checkDpopProof(
|
|
111
|
-
req.method!,
|
|
112
|
-
this.url,
|
|
113
|
-
req.headers,
|
|
114
|
-
)
|
|
115
|
-
|
|
116
|
-
return server.pushedAuthorizationRequest(
|
|
117
|
-
credentials,
|
|
118
|
-
authorizationRequest,
|
|
119
|
-
dpopProof,
|
|
120
|
-
)
|
|
121
|
-
}, 201),
|
|
122
|
-
)
|
|
123
|
-
// https://datatracker.ietf.org/doc/html/rfc9126#section-2.3
|
|
124
|
-
// > If the request did not use the POST method, the authorization server
|
|
125
|
-
// > responds with an HTTP 405 (Method Not Allowed) status code.
|
|
126
|
-
router.all('/oauth/par', (req, res) => {
|
|
127
|
-
res.writeHead(405).end()
|
|
128
|
-
})
|
|
129
|
-
|
|
130
|
-
router.options('/oauth/token', corsPreflight)
|
|
131
|
-
router.post(
|
|
132
|
-
'/oauth/token',
|
|
133
|
-
corsHeaders,
|
|
134
|
-
oauthHandler(async function (req) {
|
|
135
|
-
const payload = await parseHttpRequest(req, ['json', 'urlencoded'])
|
|
136
|
-
|
|
137
|
-
const clientMetadata = await server.deviceManager.getRequestMetadata(req)
|
|
138
|
-
|
|
139
|
-
const clientCredentials = await oauthClientCredentialsSchema
|
|
140
|
-
.parseAsync(payload, { path: ['body'] })
|
|
141
|
-
.catch((err) => throwInvalidGrant(err, 'Client credentials missing'))
|
|
142
|
-
|
|
143
|
-
const tokenRequest = await oauthTokenRequestSchema
|
|
144
|
-
.parseAsync(payload, { path: ['body'] })
|
|
145
|
-
.catch((err) => throwInvalidGrant(err, 'Invalid request payload'))
|
|
146
|
-
|
|
147
|
-
const dpopProof = await server.checkDpopProof(
|
|
148
|
-
req.method!,
|
|
149
|
-
this.url,
|
|
150
|
-
req.headers,
|
|
151
|
-
)
|
|
152
|
-
|
|
153
|
-
return server.token(
|
|
154
|
-
clientCredentials,
|
|
155
|
-
clientMetadata,
|
|
156
|
-
tokenRequest,
|
|
157
|
-
dpopProof,
|
|
158
|
-
)
|
|
159
|
-
}),
|
|
160
|
-
)
|
|
161
|
-
|
|
162
|
-
router.options('/oauth/revoke', corsPreflight)
|
|
163
|
-
router.post(
|
|
164
|
-
'/oauth/revoke',
|
|
165
|
-
corsHeaders,
|
|
166
|
-
oauthHandler(async function (req, res) {
|
|
167
|
-
const payload = await parseHttpRequest(req, ['json', 'urlencoded'])
|
|
168
|
-
|
|
169
|
-
const credentials = await oauthClientCredentialsSchema
|
|
170
|
-
.parseAsync(payload, { path: ['body'] })
|
|
171
|
-
.catch((err) => throwInvalidRequest(err, 'Client credentials missing'))
|
|
172
|
-
|
|
173
|
-
const tokenIdentification = await oauthTokenIdentificationSchema
|
|
174
|
-
.parseAsync(payload, { path: ['body'] })
|
|
175
|
-
.catch((err) => throwInvalidRequest(err, 'Invalid request payload'))
|
|
176
|
-
|
|
177
|
-
const dpopProof = await server.checkDpopProof(
|
|
178
|
-
req.method!,
|
|
179
|
-
this.url,
|
|
180
|
-
req.headers,
|
|
181
|
-
)
|
|
182
|
-
|
|
183
|
-
try {
|
|
184
|
-
await server.revoke(credentials, tokenIdentification, dpopProof)
|
|
185
|
-
} catch (err) {
|
|
186
|
-
// > Note: invalid tokens do not cause an error response since the
|
|
187
|
-
// > client cannot handle such an error in a reasonable way. Moreover,
|
|
188
|
-
// > the purpose of the revocation request, invalidating the particular
|
|
189
|
-
// > token, is already achieved.
|
|
190
|
-
//
|
|
191
|
-
// https://datatracker.ietf.org/doc/html/rfc7009#section-2.2
|
|
192
|
-
|
|
193
|
-
onError?.(req, res, err, 'Failed to revoke token')
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
return {}
|
|
197
|
-
}),
|
|
198
|
-
)
|
|
199
|
-
|
|
200
|
-
return router.buildMiddleware()
|
|
201
|
-
|
|
202
|
-
function oauthHandler<T>(
|
|
203
|
-
buildOAuthResponse: (this: T, req: Req, res: Res) => unknown,
|
|
204
|
-
status?: number,
|
|
205
|
-
): Middleware<T, Req, Res> {
|
|
206
|
-
return jsonHandler<T, Req, Res>(async function (req, res) {
|
|
207
|
-
try {
|
|
208
|
-
// https://www.rfc-editor.org/rfc/rfc6749.html#section-5.1
|
|
209
|
-
res.setHeader('Cache-Control', 'no-store')
|
|
210
|
-
res.setHeader('Pragma', 'no-cache')
|
|
211
|
-
|
|
212
|
-
// https://datatracker.ietf.org/doc/html/rfc9449#section-8.2
|
|
213
|
-
const dpopNonce = server.nextDpopNonce()
|
|
214
|
-
if (dpopNonce) {
|
|
215
|
-
const name = 'DPoP-Nonce'
|
|
216
|
-
res.setHeader(name, dpopNonce)
|
|
217
|
-
res.appendHeader('Access-Control-Expose-Headers', name)
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
const json = await buildOAuthResponse.call(this, req, res)
|
|
221
|
-
return { json, status }
|
|
222
|
-
} catch (err) {
|
|
223
|
-
onError?.(
|
|
224
|
-
req,
|
|
225
|
-
res,
|
|
226
|
-
err,
|
|
227
|
-
err instanceof OAuthError
|
|
228
|
-
? `OAuth "${err.error}" error`
|
|
229
|
-
: 'Unexpected error',
|
|
230
|
-
)
|
|
231
|
-
|
|
232
|
-
if (!res.headersSent && err instanceof WWWAuthenticateError) {
|
|
233
|
-
const name = 'WWW-Authenticate'
|
|
234
|
-
res.setHeader(name, err.wwwAuthenticateHeader)
|
|
235
|
-
res.appendHeader('Access-Control-Expose-Headers', name)
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
const status = buildErrorStatus(err)
|
|
239
|
-
const json = buildErrorPayload(err)
|
|
240
|
-
|
|
241
|
-
return { json, status }
|
|
242
|
-
}
|
|
243
|
-
})
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
function throwInvalidGrant(err: unknown, prefix: string): never {
|
|
248
|
-
throw new InvalidGrantError(formatError(err, prefix), err)
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
function throwInvalidClient(err: unknown, prefix: string): never {
|
|
252
|
-
throw new InvalidClientError(formatError(err, prefix), err)
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
function throwInvalidRequest(err: unknown, prefix: string): never {
|
|
256
|
-
throw new InvalidRequestError(formatError(err, prefix), err)
|
|
257
|
-
}
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import type { IncomingMessage, ServerResponse } from 'node:http'
|
|
2
|
-
import type { ErrorHandler } from './error-handler.js'
|
|
3
|
-
|
|
4
|
-
export type MiddlewareOptions<
|
|
5
|
-
Req extends IncomingMessage = IncomingMessage,
|
|
6
|
-
Res extends ServerResponse = ServerResponse,
|
|
7
|
-
> = {
|
|
8
|
-
onError?: ErrorHandler<Req, Res>
|
|
9
|
-
}
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import { z } from 'zod'
|
|
2
|
-
import { didSchema } from '@atproto/did'
|
|
3
|
-
import { jwtPayloadSchema } from '@atproto/jwk'
|
|
4
|
-
import { clientIdSchema } from '../client/client-id.js'
|
|
5
|
-
import { tokenIdSchema } from '../token/token-id.js'
|
|
6
|
-
|
|
7
|
-
export const accessTokenPayloadSchema = jwtPayloadSchema
|
|
8
|
-
.partial()
|
|
9
|
-
.extend({
|
|
10
|
-
// Following are required
|
|
11
|
-
jti: tokenIdSchema,
|
|
12
|
-
sub: didSchema,
|
|
13
|
-
exp: z.number().int(),
|
|
14
|
-
iat: z.number().int(),
|
|
15
|
-
iss: z.string().min(1),
|
|
16
|
-
|
|
17
|
-
// @NOTE "aud", "scope", "client_id" are not required, as are stored in the
|
|
18
|
-
// DB in 'light' access token mode.
|
|
19
|
-
|
|
20
|
-
// Restrict type of following
|
|
21
|
-
client_id: clientIdSchema.optional(),
|
|
22
|
-
})
|
|
23
|
-
.passthrough()
|
|
24
|
-
|
|
25
|
-
export type AccessTokenPayload = z.infer<typeof accessTokenPayloadSchema>
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import { z } from 'zod'
|
|
2
|
-
import { didSchema } from '@atproto/did'
|
|
3
|
-
import { jwtPayloadSchema } from '@atproto/jwk'
|
|
4
|
-
import { deviceIdSchema } from '../oauth-store.js'
|
|
5
|
-
import { requestUriSchema } from '../request/request-uri.js'
|
|
6
|
-
|
|
7
|
-
export const apiTokenPayloadSchema = jwtPayloadSchema
|
|
8
|
-
.extend({
|
|
9
|
-
sub: didSchema,
|
|
10
|
-
|
|
11
|
-
deviceId: deviceIdSchema,
|
|
12
|
-
// If the token is bound to a particular authorization request, it can only
|
|
13
|
-
// be used in the context of that request.
|
|
14
|
-
requestUri: requestUriSchema.optional(),
|
|
15
|
-
})
|
|
16
|
-
.passthrough()
|
|
17
|
-
|
|
18
|
-
export type ApiTokenPayload = z.infer<typeof apiTokenPayloadSchema>
|