@zudojs/auth-oauth 1.1.1 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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
|
|
@@ -169,7 +177,11 @@ Every one of these is covered by a test in `tests/`.
|
|
|
169
177
|
endpoints (token, user-info) additionally may not point at a loopback,
|
|
170
178
|
private, CGNAT, link-local, unique-local, multicast or reserved address, at
|
|
171
179
|
`169.254.169.254` and friends, or at a `localhost` / `*.local` / `*.internal`
|
|
172
|
-
/ `metadata.google.internal` name.
|
|
180
|
+
/ `metadata.google.internal` name. IPv6 literals that embed an IPv4
|
|
181
|
+
address (`[::127.0.0.1]`, `[::ffff:0:a9fe:a9fe]`, NAT64
|
|
182
|
+
`[64:ff9b::169.254.169.254]`, 6to4 `2002::/16`) are judged as that IPv4
|
|
183
|
+
address, and the local-use NAT64 prefix `64:ff9b:1::/48` is refused.
|
|
184
|
+
Redirects are not followed
|
|
173
185
|
(`redirect: "manual"`), so a 3xx cannot walk the request somewhere that never
|
|
174
186
|
passed the guard.
|
|
175
187
|
**Limit:** the check is on the literal host; DNS is not resolved, so DNS
|
|
@@ -198,6 +210,9 @@ Every one of these is covered by a test in `tests/`.
|
|
|
198
210
|
|
|
199
211
|
Every failure is an `OAuthError` with a machine-readable `code`, a suggested
|
|
200
212
|
`statusCode`, and `expose` saying whether the message is safe to show a user.
|
|
213
|
+
`OAuthError` extends the shared `OAuthError` from `@zudojs/errors`, so every
|
|
214
|
+
class below is also a `BaseError`, and its codes equal the shared
|
|
215
|
+
`ErrorCode.OAUTH_*` members.
|
|
201
216
|
|
|
202
217
|
| Class | Code | Status | Exposed |
|
|
203
218
|
| --- | --- | --- | --- |
|
|
@@ -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. */
|
|
@@ -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.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zudojs/auth-oauth",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
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.1.0",
|
|
57
|
+
"@zudojs/security": "1.1.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",
|