@zudojs/auth-oauth 1.1.1 → 1.2.1
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/README.md +65 -48
- package/dist/oauthClient/oauthConfig.resolve.js +5 -2
- package/dist/oauthClient/oauthHttp.core.js +3 -1
- package/dist/oauthClient/oauthUserInfo.core.js +1 -1
- package/dist/oauthErrors/oauthError.base.d.ts +8 -9
- package/dist/oauthErrors/oauthError.base.js +9 -15
- package/dist/oauthProviders/oauthProvider.presets.js +7 -1
- package/dist/oauthSecurity/oauthJson.sanitize.js +3 -1
- package/dist/oauthSecurity/oauthUrl.guard.js +16 -35
- package/package.json +5 -1
package/README.md
CHANGED
|
@@ -5,7 +5,15 @@ default, mandatory anti-CSRF `state` with a timing-safe check, an SSRF guard on
|
|
|
5
5
|
every endpoint URL, a redirect-URI allowlist, and size- and time-bounded
|
|
6
6
|
requests to the provider.
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
<!-- zudo-docs:start -->
|
|
9
|
+
|
|
10
|
+
**Documentation:** [zudojs.oyinlola.site/docs/packages-auth-oauth](https://zudojs.oyinlola.site/docs/packages-auth-oauth) · **For AI agents:** [Markdown version](https://zudojs.oyinlola.site/docs/packages-auth-oauth.md), [llms.txt](https://zudojs.oyinlola.site/llms.txt)
|
|
11
|
+
|
|
12
|
+
<!-- zudo-docs:end -->
|
|
13
|
+
|
|
14
|
+
Built on Node built-ins (`node:crypto` and the global `fetch`), plus
|
|
15
|
+
`@zudojs/errors` (the error base classes) and `@zudojs/security` (the IPv6
|
|
16
|
+
helpers behind the SSRF guard).
|
|
9
17
|
|
|
10
18
|
```bash
|
|
11
19
|
pnpm add @zudojs/auth-oauth
|
|
@@ -84,7 +92,9 @@ app.get("/auth/callback", async (req, res) => {
|
|
|
84
92
|
const profile = await fetchUserInfo(config, tokens.accessToken);
|
|
85
93
|
|
|
86
94
|
if (profile.email === undefined) {
|
|
87
|
-
return res
|
|
95
|
+
return res
|
|
96
|
+
.status(400)
|
|
97
|
+
.send("This provider did not release an email address.");
|
|
88
98
|
}
|
|
89
99
|
|
|
90
100
|
// Now it is your application's turn: find or create the local account keyed
|
|
@@ -107,29 +117,29 @@ const fresh = await refreshAccessToken(config, storedRefreshToken);
|
|
|
107
117
|
|
|
108
118
|
## API
|
|
109
119
|
|
|
110
|
-
| Export
|
|
111
|
-
|
|
|
112
|
-
| `createAuthorizationUrl(config, options)`
|
|
113
|
-
| `exchangeCodeForToken(config, options)`
|
|
114
|
-
| `refreshAccessToken(config, refreshToken)`
|
|
115
|
-
| `fetchUserInfo(config, accessToken)`
|
|
116
|
-
| `generateState()` / `verifyState(expected, received)`
|
|
117
|
-
| `generateCodeVerifier()` / `deriveCodeChallenge(verifier)`
|
|
118
|
-
| `parseTokenResponse(payload)`
|
|
119
|
-
| `normalizeUserInfo(provider, payload)`
|
|
120
|
-
| `assertSafeUrl(url, label, use)` / `isBlockedFetchHost(host)` | The URL and SSRF guards, exposed for your own checks.
|
|
121
|
-
| `PROVIDER_PRESETS`
|
|
120
|
+
| Export | What it does |
|
|
121
|
+
| ------------------------------------------------------------- | -------------------------------------------------------------------------------- |
|
|
122
|
+
| `createAuthorizationUrl(config, options)` | Builds the authorize URL. Returns `{ url, state, codeVerifier, codeChallenge }`. |
|
|
123
|
+
| `exchangeCodeForToken(config, options)` | Authorization-code grant. Returns a validated `OAuthTokenSet`. |
|
|
124
|
+
| `refreshAccessToken(config, refreshToken)` | Refresh-token grant, where the provider supports one. |
|
|
125
|
+
| `fetchUserInfo(config, accessToken)` | Bearer GET of the user-info endpoint, normalised to `OAuthUserInfo`. |
|
|
126
|
+
| `generateState()` / `verifyState(expected, received)` | 256-bit state, timing-safe comparison. |
|
|
127
|
+
| `generateCodeVerifier()` / `deriveCodeChallenge(verifier)` | PKCE primitives (`S256`). |
|
|
128
|
+
| `parseTokenResponse(payload)` | Validate a token payload you obtained elsewhere. |
|
|
129
|
+
| `normalizeUserInfo(provider, payload)` | Normalise a profile payload you obtained elsewhere. |
|
|
130
|
+
| `assertSafeUrl(url, label, use)` / `isBlockedFetchHost(host)` | The URL and SSRF guards, exposed for your own checks. |
|
|
131
|
+
| `PROVIDER_PRESETS` | Endpoint defaults per provider. |
|
|
122
132
|
|
|
123
133
|
## Providers
|
|
124
134
|
|
|
125
|
-
| Provider
|
|
126
|
-
|
|
|
127
|
-
| `google`
|
|
128
|
-
| `github`
|
|
129
|
-
| `microsoft` | preset (`common` tenant)
|
|
130
|
-
| `apple`
|
|
131
|
-
| `discord`
|
|
132
|
-
| `custom`
|
|
135
|
+
| Provider | Endpoints | Client auth | Refresh | Notes |
|
|
136
|
+
| ----------- | ------------------------- | ----------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
137
|
+
| `google` | preset | body | yes | `access_type=offline` + `prompt=consent` are sent so a refresh token is actually issued. |
|
|
138
|
+
| `github` | preset | body | **no** | Classic OAuth App tokens do not expire and no refresh token is issued; `refreshAccessToken` throws rather than making a pointless request. |
|
|
139
|
+
| `microsoft` | preset (`common` tenant) | body | yes | Override `authorizeUrl`/`tokenUrl` for a single-tenant app. |
|
|
140
|
+
| `apple` | preset | body | yes | `clientSecret` is the ES256 JWT you mint from your private key — this package does not generate it. Apple has **no user-info endpoint**; the profile is in the `id_token`, so `fetchUserInfo` throws for `apple`. |
|
|
141
|
+
| `discord` | preset | **basic** | yes | |
|
|
142
|
+
| `custom` | you supply all three URLs | body | yes | |
|
|
133
143
|
|
|
134
144
|
Any preset URL can be overridden on the config; every override goes through the
|
|
135
145
|
same validation.
|
|
@@ -140,7 +150,7 @@ same validation.
|
|
|
140
150
|
|
|
141
151
|
- **GitHub** omits `email` from `/user` whenever the address is private, which
|
|
142
152
|
is the default. When the `user:email` scope was granted, this package asks
|
|
143
|
-
`/user/emails` and uses the primary
|
|
153
|
+
`/user/emails` and uses the primary _verified_ address. Without the scope, or
|
|
144
154
|
without a verified primary, the profile simply comes back with no email.
|
|
145
155
|
- **Discord** returns no email unless the `email` scope was granted.
|
|
146
156
|
|
|
@@ -165,11 +175,15 @@ Every one of these is covered by a test in `tests/`.
|
|
|
165
175
|
arbitrary redirect target is never reflected.
|
|
166
176
|
- **URL validation and SSRF guard.** Every URL must be `https` — `http` is
|
|
167
177
|
tolerated only for `localhost` / `127.0.0.1` / `[::1]` and only on
|
|
168
|
-
browser-facing URLs — and must not embed credentials. The
|
|
178
|
+
browser-facing URLs — and must not embed credentials. The _server-fetched_
|
|
169
179
|
endpoints (token, user-info) additionally may not point at a loopback,
|
|
170
180
|
private, CGNAT, link-local, unique-local, multicast or reserved address, at
|
|
171
181
|
`169.254.169.254` and friends, or at a `localhost` / `*.local` / `*.internal`
|
|
172
|
-
/ `metadata.google.internal` name.
|
|
182
|
+
/ `metadata.google.internal` name. IPv6 literals that embed an IPv4
|
|
183
|
+
address (`[::127.0.0.1]`, `[::ffff:0:a9fe:a9fe]`, NAT64
|
|
184
|
+
`[64:ff9b::169.254.169.254]`, 6to4 `2002::/16`) are judged as that IPv4
|
|
185
|
+
address, and the local-use NAT64 prefix `64:ff9b:1::/48` is refused.
|
|
186
|
+
Redirects are not followed
|
|
173
187
|
(`redirect: "manual"`), so a 3xx cannot walk the request somewhere that never
|
|
174
188
|
passed the guard.
|
|
175
189
|
**Limit:** the check is on the literal host; DNS is not resolved, so DNS
|
|
@@ -179,7 +193,7 @@ Every one of these is covered by a test in `tests/`.
|
|
|
179
193
|
`AbortSignal.timeout(timeoutMs)` (default 10s) and the body is streamed and
|
|
180
194
|
abandoned the moment it passes `maxResponseBytes` (default 256 KiB); an
|
|
181
195
|
oversized `Content-Length` is refused before a byte is read.
|
|
182
|
-
- **Defensive parsing.** A token response must be a JSON
|
|
196
|
+
- **Defensive parsing.** A token response must be a JSON _object_ with a
|
|
183
197
|
non-blank string `access_token`; `expires_in` must be a non-negative integer
|
|
184
198
|
(or its decimal string); `refresh_token`, `id_token`, `scope` and `token_type`
|
|
185
199
|
must be strings when present. `__proto__`, `constructor` and `prototype` are
|
|
@@ -198,31 +212,34 @@ Every one of these is covered by a test in `tests/`.
|
|
|
198
212
|
|
|
199
213
|
Every failure is an `OAuthError` with a machine-readable `code`, a suggested
|
|
200
214
|
`statusCode`, and `expose` saying whether the message is safe to show a user.
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
|
206
|
-
|
|
|
207
|
-
| `
|
|
208
|
-
| `
|
|
209
|
-
| `
|
|
210
|
-
| `
|
|
211
|
-
| `
|
|
215
|
+
`OAuthError` extends the shared `OAuthError` from `@zudojs/errors`, so every
|
|
216
|
+
class below is also a `BaseError`, and its codes equal the shared
|
|
217
|
+
`ErrorCode.OAUTH_*` members.
|
|
218
|
+
|
|
219
|
+
| Class | Code | Status | Exposed |
|
|
220
|
+
| ------------------------------ | --------------------------------- | ------ | ------- |
|
|
221
|
+
| `OAuthConfigurationError` | `OAUTH_CONFIGURATION_INVALID` | 500 | no |
|
|
222
|
+
| `OAuthEndpointNotAllowedError` | `OAUTH_ENDPOINT_NOT_ALLOWED` | 500 | no |
|
|
223
|
+
| `OAuthRedirectUriError` | `OAUTH_REDIRECT_URI_NOT_ALLOWED` | 400 | yes |
|
|
224
|
+
| `OAuthStateMismatchError` | `OAUTH_STATE_MISMATCH` | 400 | yes |
|
|
225
|
+
| `OAuthProviderError` | `OAUTH_PROVIDER_REJECTED` | 502 | yes |
|
|
226
|
+
| `OAuthResponseError` | `OAUTH_PROVIDER_RESPONSE_INVALID` | 502 | yes |
|
|
227
|
+
| `OAuthResponseTooLargeError` | `OAUTH_RESPONSE_TOO_LARGE` | 502 | yes |
|
|
228
|
+
| `OAuthNetworkError` | `OAUTH_NETWORK` | 504 | yes |
|
|
212
229
|
|
|
213
230
|
## Configuration reference
|
|
214
231
|
|
|
215
|
-
| Field
|
|
216
|
-
|
|
|
217
|
-
| `provider`
|
|
218
|
-
| `clientId`, `clientSecret`
|
|
219
|
-
| `allowedRedirectUris`
|
|
220
|
-
| `authorizeUrl`, `tokenUrl`, `userInfoUrl` | only for `custom` | the preset's
|
|
221
|
-
| `scopes`
|
|
222
|
-
| `clientAuthMethod`
|
|
223
|
-
| `timeoutMs`
|
|
224
|
-
| `maxResponseBytes`
|
|
225
|
-
| `fetch`
|
|
232
|
+
| Field | Required | Default |
|
|
233
|
+
| ----------------------------------------- | ----------------- | ------------------------- |
|
|
234
|
+
| `provider` | yes | — |
|
|
235
|
+
| `clientId`, `clientSecret` | yes | — |
|
|
236
|
+
| `allowedRedirectUris` | yes, non-empty | — |
|
|
237
|
+
| `authorizeUrl`, `tokenUrl`, `userInfoUrl` | only for `custom` | the preset's |
|
|
238
|
+
| `scopes` | no | the preset's |
|
|
239
|
+
| `clientAuthMethod` | no | the preset's |
|
|
240
|
+
| `timeoutMs` | no | `10000` (1 - 120000) |
|
|
241
|
+
| `maxResponseBytes` | no | `262144` (1024 - 5242880) |
|
|
242
|
+
| `fetch` | no | global `fetch` |
|
|
226
243
|
|
|
227
244
|
Out-of-range values are rejected, not clamped.
|
|
228
245
|
|
|
@@ -28,7 +28,9 @@ function requireString(value, field) {
|
|
|
28
28
|
function boundedInt(value, fallback, min, max, field) {
|
|
29
29
|
if (value === undefined)
|
|
30
30
|
return fallback;
|
|
31
|
-
if (typeof value !== "number" ||
|
|
31
|
+
if (typeof value !== "number" ||
|
|
32
|
+
!Number.isFinite(value) ||
|
|
33
|
+
!Number.isInteger(value)) {
|
|
32
34
|
throw new OAuthConfigurationError(`${field} must be an integer.`);
|
|
33
35
|
}
|
|
34
36
|
if (value < min || value > max) {
|
|
@@ -50,7 +52,8 @@ export function assertScopes(scopes) {
|
|
|
50
52
|
throw new OAuthConfigurationError("scopes must be an array of scope-tokens.");
|
|
51
53
|
}
|
|
52
54
|
for (const scope of scopes) {
|
|
53
|
-
if (typeof scope !== "string" ||
|
|
55
|
+
if (typeof scope !== "string" ||
|
|
56
|
+
!/^[\x21\x23-\x5B\x5D-\x7E]+$/.test(scope)) {
|
|
54
57
|
throw new OAuthConfigurationError("Each scope must be a non-empty RFC 6749 scope-token.");
|
|
55
58
|
}
|
|
56
59
|
}
|
|
@@ -20,7 +20,9 @@ const SAFE_ERROR_CODE = /^[A-Za-z0-9_.:-]{1,64}$/;
|
|
|
20
20
|
/** Read a response body, refusing to buffer more than `maxBytes`. */
|
|
21
21
|
async function readCappedText(response, maxBytes) {
|
|
22
22
|
const declared = response.headers.get("content-length");
|
|
23
|
-
if (declared !== null &&
|
|
23
|
+
if (declared !== null &&
|
|
24
|
+
/^\d+$/.test(declared) &&
|
|
25
|
+
Number(declared) > maxBytes) {
|
|
24
26
|
throw new OAuthResponseTooLargeError(maxBytes);
|
|
25
27
|
}
|
|
26
28
|
const body = response.body;
|
|
@@ -7,7 +7,7 @@ import { OAuthResponseError } from "../oauthErrors/index.js";
|
|
|
7
7
|
import { normalizeUserInfo } from "../oauthProviders/index.js";
|
|
8
8
|
import { assertSafeUrl } from "../oauthSecurity/index.js";
|
|
9
9
|
import { resolveConfig, resolveUserInfoUrl, } from "./oauthConfig.resolve.js";
|
|
10
|
-
import { requestProviderJson, requestProviderValue
|
|
10
|
+
import { requestProviderJson, requestProviderValue } from "./oauthHttp.core.js";
|
|
11
11
|
/**
|
|
12
12
|
* GitHub's `/user` omits `email` whenever the address is private — which is
|
|
13
13
|
* the default for new accounts. When `user:email` was granted we ask
|
|
@@ -3,8 +3,10 @@
|
|
|
3
3
|
*
|
|
4
4
|
* @module oauthErrors/oauthError
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
6
|
+
* `OAuthError` builds on the shared `OAuthError` in `@zudojs/errors`, so
|
|
7
|
+
* every class here is a `BaseError` (structured `toJSON`, metadata
|
|
8
|
+
* redaction, `category`/`severity`). `OAuthErrorCode` values equal the
|
|
9
|
+
* shared `ErrorCode.OAUTH_*` members.
|
|
8
10
|
*
|
|
9
11
|
* **Secret hygiene.** No constructor here ever interpolates a client secret,
|
|
10
12
|
* an access token, a refresh token or a code verifier into `message`. The
|
|
@@ -13,6 +15,7 @@
|
|
|
13
15
|
* so a provider cannot echo a secret back into your logs. `message` is the
|
|
14
16
|
* first line of `stack`, so keeping it clean keeps the stack clean.
|
|
15
17
|
*/
|
|
18
|
+
import { OAuthError as SharedOAuthError } from "@zudojs/errors";
|
|
16
19
|
/** Stable, machine-readable error codes. */
|
|
17
20
|
export declare const OAuthErrorCode: {
|
|
18
21
|
/** The config is unusable: missing field, bad URL, unsupported operation. */
|
|
@@ -48,16 +51,12 @@ export interface OAuthErrorOptions {
|
|
|
48
51
|
*
|
|
49
52
|
* `expose` says whether the message is safe to hand to an end user; it is
|
|
50
53
|
* `true` for request-caused failures and `false` for configuration ones
|
|
51
|
-
* (which describe your deployment, not the request).
|
|
54
|
+
* (which describe your deployment, not the request). Defaults: 400,
|
|
55
|
+
* exposed, code `OAUTH_PROVIDER_REJECTED`.
|
|
52
56
|
*/
|
|
53
|
-
export declare class OAuthError extends
|
|
54
|
-
readonly name: string;
|
|
57
|
+
export declare class OAuthError extends SharedOAuthError {
|
|
55
58
|
/** Machine-readable code. */
|
|
56
59
|
readonly code: OAuthErrorCode;
|
|
57
|
-
/** Suggested HTTP status for a handler that surfaces this. */
|
|
58
|
-
readonly statusCode: number;
|
|
59
|
-
/** Whether `message` is safe to return to a client verbatim. */
|
|
60
|
-
readonly expose: boolean;
|
|
61
60
|
constructor(message: string, options?: OAuthErrorOptions);
|
|
62
61
|
}
|
|
63
62
|
/** The configuration is missing something or is structurally unusable. */
|
|
@@ -3,8 +3,10 @@
|
|
|
3
3
|
*
|
|
4
4
|
* @module oauthErrors/oauthError
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
6
|
+
* `OAuthError` builds on the shared `OAuthError` in `@zudojs/errors`, so
|
|
7
|
+
* every class here is a `BaseError` (structured `toJSON`, metadata
|
|
8
|
+
* redaction, `category`/`severity`). `OAuthErrorCode` values equal the
|
|
9
|
+
* shared `ErrorCode.OAUTH_*` members.
|
|
8
10
|
*
|
|
9
11
|
* **Secret hygiene.** No constructor here ever interpolates a client secret,
|
|
10
12
|
* an access token, a refresh token or a code verifier into `message`. The
|
|
@@ -13,6 +15,7 @@
|
|
|
13
15
|
* so a provider cannot echo a secret back into your logs. `message` is the
|
|
14
16
|
* first line of `stack`, so keeping it clean keeps the stack clean.
|
|
15
17
|
*/
|
|
18
|
+
import { OAuthError as SharedOAuthError } from "@zudojs/errors";
|
|
16
19
|
/** Stable, machine-readable error codes. */
|
|
17
20
|
export const OAuthErrorCode = {
|
|
18
21
|
/** The config is unusable: missing field, bad URL, unsupported operation. */
|
|
@@ -39,21 +42,12 @@ export const OAuthErrorCode = {
|
|
|
39
42
|
*
|
|
40
43
|
* `expose` says whether the message is safe to hand to an end user; it is
|
|
41
44
|
* `true` for request-caused failures and `false` for configuration ones
|
|
42
|
-
* (which describe your deployment, not the request).
|
|
45
|
+
* (which describe your deployment, not the request). Defaults: 400,
|
|
46
|
+
* exposed, code `OAUTH_PROVIDER_REJECTED`.
|
|
43
47
|
*/
|
|
44
|
-
export class OAuthError extends
|
|
45
|
-
name = "OAuthError";
|
|
46
|
-
/** Machine-readable code. */
|
|
47
|
-
code;
|
|
48
|
-
/** Suggested HTTP status for a handler that surfaces this. */
|
|
49
|
-
statusCode;
|
|
50
|
-
/** Whether `message` is safe to return to a client verbatim. */
|
|
51
|
-
expose;
|
|
48
|
+
export class OAuthError extends SharedOAuthError {
|
|
52
49
|
constructor(message, options) {
|
|
53
|
-
super(message, options
|
|
54
|
-
this.code = options?.code ?? OAuthErrorCode.PROVIDER_REJECTED;
|
|
55
|
-
this.statusCode = options?.statusCode ?? 400;
|
|
56
|
-
this.expose = options?.expose ?? true;
|
|
50
|
+
super(message, options);
|
|
57
51
|
}
|
|
58
52
|
}
|
|
59
53
|
/** The configuration is missing something or is structurally unusable. */
|
|
@@ -62,7 +62,13 @@ export const PROVIDER_PRESETS = {
|
|
|
62
62
|
authorizeUrl: "https://login.microsoftonline.com/common/oauth2/v2.0/authorize",
|
|
63
63
|
tokenUrl: "https://login.microsoftonline.com/common/oauth2/v2.0/token",
|
|
64
64
|
userInfoUrl: "https://graph.microsoft.com/v1.0/me",
|
|
65
|
-
defaultScopes: [
|
|
65
|
+
defaultScopes: [
|
|
66
|
+
"openid",
|
|
67
|
+
"email",
|
|
68
|
+
"profile",
|
|
69
|
+
"offline_access",
|
|
70
|
+
"User.Read",
|
|
71
|
+
],
|
|
66
72
|
clientAuth: "body",
|
|
67
73
|
supportsRefresh: true,
|
|
68
74
|
},
|
|
@@ -59,7 +59,9 @@ export function sanitizeJsonValue(value) {
|
|
|
59
59
|
*/
|
|
60
60
|
export function parseJsonObject(text, label) {
|
|
61
61
|
const sanitized = parseJsonValue(text, label);
|
|
62
|
-
if (sanitized === null ||
|
|
62
|
+
if (sanitized === null ||
|
|
63
|
+
typeof sanitized !== "object" ||
|
|
64
|
+
Array.isArray(sanitized)) {
|
|
63
65
|
throw new OAuthResponseError(`${label} did not return a JSON object.`);
|
|
64
66
|
}
|
|
65
67
|
return sanitized;
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
* rebinding) is not caught here. Pair this with network egress controls if
|
|
27
27
|
* you accept endpoint URLs from untrusted operators.
|
|
28
28
|
*/
|
|
29
|
+
import { embeddedIpv4, expandIpv6, isNonPublicIpv6Range, } from "@zudojs/security";
|
|
29
30
|
import { OAuthEndpointNotAllowedError } from "../oauthErrors/index.js";
|
|
30
31
|
/** Hostnames that are always refused for a server-fetched endpoint. */
|
|
31
32
|
const BLOCKED_HOST_NAMES = new Set([
|
|
@@ -90,40 +91,22 @@ function isNonPublicIpv4(octets) {
|
|
|
90
91
|
return true; // multicast + reserved + broadcast
|
|
91
92
|
return false;
|
|
92
93
|
}
|
|
93
|
-
/**
|
|
94
|
+
/**
|
|
95
|
+
* Whether an IPv6 literal (already stripped of brackets) is non-public.
|
|
96
|
+
*
|
|
97
|
+
* Any form embedding an IPv4 address (compatible `::/96`, mapped, translated,
|
|
98
|
+
* NAT64 `64:ff9b::/96`, 6to4) is judged as that IPv4 address. The WHATWG
|
|
99
|
+
* parser serialises `[::127.0.0.1]` as `[::7f00:1]`, which the old
|
|
100
|
+
* dotted-form regex never matched. An unparseable literal fails closed.
|
|
101
|
+
*/
|
|
94
102
|
function isNonPublicIpv6(raw) {
|
|
95
|
-
const
|
|
96
|
-
if (
|
|
103
|
+
const groups = expandIpv6(raw);
|
|
104
|
+
if (!groups)
|
|
97
105
|
return true;
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
const octets = parseIpv4(embedded);
|
|
103
|
-
return octets === undefined ? true : isNonPublicIpv4(octets);
|
|
104
|
-
}
|
|
105
|
-
// The WHATWG URL parser rewrites `::ffff:127.0.0.1` as `::ffff:7f00:1`,
|
|
106
|
-
// so the hex form has to be decoded back to its embedded IPv4 address.
|
|
107
|
-
const hexMapped = /^::ffff:([0-9a-f]{1,4}):([0-9a-f]{1,4})$/.exec(host);
|
|
108
|
-
const high = hexMapped?.[1];
|
|
109
|
-
const low = hexMapped?.[2];
|
|
110
|
-
if (high !== undefined && low !== undefined) {
|
|
111
|
-
const word1 = Number.parseInt(high, 16);
|
|
112
|
-
const word2 = Number.parseInt(low, 16);
|
|
113
|
-
return isNonPublicIpv4([
|
|
114
|
-
(word1 >> 8) & 0xff,
|
|
115
|
-
word1 & 0xff,
|
|
116
|
-
(word2 >> 8) & 0xff,
|
|
117
|
-
word2 & 0xff,
|
|
118
|
-
]);
|
|
119
|
-
}
|
|
120
|
-
if (/^f[cd][0-9a-f]{2}:/.test(host))
|
|
121
|
-
return true; // fc00::/7 unique local
|
|
122
|
-
if (/^fe[89ab][0-9a-f]:/.test(host))
|
|
123
|
-
return true; // fe80::/10 link-local
|
|
124
|
-
if (/^ff[0-9a-f]{2}:/.test(host))
|
|
125
|
-
return true; // ff00::/8 multicast
|
|
126
|
-
return false;
|
|
106
|
+
const embedded = embeddedIpv4(groups);
|
|
107
|
+
if (embedded)
|
|
108
|
+
return isNonPublicIpv4(embedded);
|
|
109
|
+
return isNonPublicIpv6Range(groups);
|
|
127
110
|
}
|
|
128
111
|
/**
|
|
129
112
|
* Whether a host literal is one this package refuses to fetch from.
|
|
@@ -132,9 +115,7 @@ function isNonPublicIpv6(raw) {
|
|
|
132
115
|
*/
|
|
133
116
|
export function isBlockedFetchHost(hostname) {
|
|
134
117
|
const host = hostname.toLowerCase();
|
|
135
|
-
const unbracketed = host.startsWith("[") && host.endsWith("]")
|
|
136
|
-
? host.slice(1, -1)
|
|
137
|
-
: host;
|
|
118
|
+
const unbracketed = host.startsWith("[") && host.endsWith("]") ? host.slice(1, -1) : host;
|
|
138
119
|
// A trailing dot marks a fully-qualified name (`localhost.`,
|
|
139
120
|
// `metadata.google.internal.`). DNS resolves it to the same address as
|
|
140
121
|
// the undotted form, but the WHATWG parser keeps the dot on domain
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zudojs/auth-oauth",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.1",
|
|
4
4
|
"description": "OAuth2 authorization-code client for the Zudojs framework — PKCE S256, mandatory state, SSRF-guarded endpoints, and provider presets for Google, GitHub, Microsoft, Apple and Discord.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": {
|
|
@@ -52,6 +52,10 @@
|
|
|
52
52
|
"url": "https://github.com/oyinlola-tech/zudo",
|
|
53
53
|
"directory": "packages/auth-oauth"
|
|
54
54
|
},
|
|
55
|
+
"dependencies": {
|
|
56
|
+
"@zudojs/errors": "1.2.0",
|
|
57
|
+
"@zudojs/security": "1.2.0"
|
|
58
|
+
},
|
|
55
59
|
"scripts": {
|
|
56
60
|
"build": "tsc -p tsconfig.json",
|
|
57
61
|
"typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.test.json --noEmit",
|