@becklyn/deployment-protection 0.2.0 → 0.3.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 +81 -12
- package/dist/cjs/auth-proxy.d.ts +15 -0
- package/dist/cjs/auth-proxy.d.ts.map +1 -0
- package/dist/cjs/auth-proxy.js +135 -0
- package/dist/cjs/config.d.ts +7 -0
- package/dist/cjs/config.d.ts.map +1 -1
- package/dist/cjs/config.js +20 -1
- package/dist/cjs/constants.d.ts +1 -0
- package/dist/cjs/constants.d.ts.map +1 -1
- package/dist/cjs/constants.js +2 -1
- package/dist/cjs/handler.d.ts.map +1 -1
- package/dist/cjs/handler.js +38 -1
- package/dist/cjs/handoff.d.ts +46 -0
- package/dist/cjs/handoff.d.ts.map +1 -0
- package/dist/cjs/handoff.js +199 -0
- package/dist/cjs/index.d.ts +2 -1
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/index.js +8 -1
- package/dist/cjs/types.d.ts +15 -0
- package/dist/cjs/types.d.ts.map +1 -1
- package/dist/cjs/vercel-oauth.d.ts +9 -2
- package/dist/cjs/vercel-oauth.d.ts.map +1 -1
- package/dist/cjs/vercel-oauth.js +8 -4
- package/dist/es/auth-proxy.d.ts +15 -0
- package/dist/es/auth-proxy.d.ts.map +1 -0
- package/dist/es/auth-proxy.js +130 -0
- package/dist/es/config.d.ts +7 -0
- package/dist/es/config.d.ts.map +1 -1
- package/dist/es/config.js +18 -1
- package/dist/es/constants.d.ts +1 -0
- package/dist/es/constants.d.ts.map +1 -1
- package/dist/es/constants.js +1 -0
- package/dist/es/handler.d.ts.map +1 -1
- package/dist/es/handler.js +39 -2
- package/dist/es/handoff.d.ts +46 -0
- package/dist/es/handoff.d.ts.map +1 -0
- package/dist/es/handoff.js +187 -0
- package/dist/es/index.d.ts +2 -1
- package/dist/es/index.d.ts.map +1 -1
- package/dist/es/index.js +2 -1
- package/dist/es/types.d.ts +15 -0
- package/dist/es/types.d.ts.map +1 -1
- package/dist/es/vercel-oauth.d.ts +9 -2
- package/dist/es/vercel-oauth.d.ts.map +1 -1
- package/dist/es/vercel-oauth.js +9 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -70,33 +70,97 @@ export default withDeploymentProtection(async request => {
|
|
|
70
70
|
| `DEPLOYMENT_PROTECTION_ENABLED` | no | `true` | Set `false` / `0` / `off` to disable |
|
|
71
71
|
| `DEPLOYMENT_PROTECTION_USERNAME` | for password auth | — | Shared username |
|
|
72
72
|
| `DEPLOYMENT_PROTECTION_PASSWORD` | for password auth | — | Shared password |
|
|
73
|
-
| `DEPLOYMENT_PROTECTION_SECRET` | recommended | derived from user+pass | HMAC secret for session cookies
|
|
73
|
+
| `DEPLOYMENT_PROTECTION_SECRET` | recommended | derived from user+pass | HMAC secret for session cookies (+ handoff fallback) |
|
|
74
|
+
| `DEPLOYMENT_PROTECTION_AUTH_PROXY_URL` | for proxy SSO | — | Base URL of the central auth-proxy app |
|
|
75
|
+
| `DEPLOYMENT_PROTECTION_HANDOFF_SECRET` | no | same as `…_SECRET` | Shared secret between apps and the auth-proxy |
|
|
74
76
|
| `VERCEL_AUTOMATION_BYPASS_SECRET` | no | — | Same secret as Vercel Protection Bypass for Automation |
|
|
75
|
-
| `DEPLOYMENT_PROTECTION_VERCEL_CLIENT_ID` |
|
|
76
|
-
| `DEPLOYMENT_PROTECTION_VERCEL_CLIENT_SECRET` |
|
|
77
|
+
| `DEPLOYMENT_PROTECTION_VERCEL_CLIENT_ID` | direct Vercel SSO | — | Or `NEXT_PUBLIC_VERCEL_APP_CLIENT_ID` (legacy) |
|
|
78
|
+
| `DEPLOYMENT_PROTECTION_VERCEL_CLIENT_SECRET` | direct Vercel SSO | — | Or `VERCEL_APP_CLIENT_SECRET` (legacy) |
|
|
77
79
|
| `DEPLOYMENT_PROTECTION_FORM_TITLE` | no | `Authentication required` | Login heading |
|
|
78
80
|
| `DEPLOYMENT_PROTECTION_FORM_DESCRIPTION` | no | … | Login description |
|
|
79
81
|
|
|
80
|
-
At least one of password auth, Vercel OAuth, or a bypass secret must be configured while enabled. If nothing is configured, the gate stays inactive (fail-open) so local dev is not bricked.
|
|
82
|
+
At least one of password auth, Vercel OAuth (proxy or direct), or a bypass secret must be configured while enabled. If nothing is configured, the gate stays inactive (fail-open) so local dev is not bricked.
|
|
81
83
|
|
|
82
|
-
### 3. Sign in with Vercel (
|
|
84
|
+
### 3. Sign in with Vercel via auth-proxy (recommended)
|
|
83
85
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
86
|
+
Register **one** OAuth callback URL on a shared [Sign in with Vercel](https://vercel.com/docs/sign-in-with-vercel) app:
|
|
87
|
+
|
|
88
|
+
```text
|
|
89
|
+
https://<auth-proxy-host>/callback
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Deploy the `deployment-protection-auth-proxy` app from this monorepo (or any tiny Next app that wires the handlers below) and set:
|
|
93
|
+
|
|
94
|
+
**On the auth-proxy**
|
|
95
|
+
|
|
96
|
+
| Variable | Description |
|
|
97
|
+
| ---------------------------------------------------- | -------------------------------------- |
|
|
98
|
+
| `DEPLOYMENT_PROTECTION_VERCEL_CLIENT_ID` | Vercel OAuth client id |
|
|
99
|
+
| `DEPLOYMENT_PROTECTION_VERCEL_CLIENT_SECRET` | Vercel OAuth client secret |
|
|
100
|
+
| `DEPLOYMENT_PROTECTION_SECRET` or `…_HANDOFF_SECRET` | Shared HMAC secret with protected apps |
|
|
101
|
+
| `DEPLOYMENT_PROTECTION_ALLOWED_ORIGINS` | Optional allowlist (see below) |
|
|
102
|
+
|
|
103
|
+
**On every protected app**
|
|
104
|
+
|
|
105
|
+
| Variable | Description |
|
|
106
|
+
| -------------------------------------- | -------------------------------------- |
|
|
107
|
+
| `DEPLOYMENT_PROTECTION_AUTH_PROXY_URL` | e.g. `https://dp-auth.example.com` |
|
|
108
|
+
| `DEPLOYMENT_PROTECTION_SECRET` | Session cookie secret |
|
|
109
|
+
| `DEPLOYMENT_PROTECTION_HANDOFF_SECRET` | Same as proxy (defaults to `…_SECRET`) |
|
|
110
|
+
|
|
111
|
+
No per-project / per-preview callback URLs.
|
|
112
|
+
|
|
113
|
+
#### Flow
|
|
114
|
+
|
|
115
|
+
1. App redirects to `{AUTH_PROXY_URL}/start` with a **signed** `return_origin` + `return_path`.
|
|
116
|
+
2. Proxy runs Vercel OAuth against its fixed `/callback`.
|
|
117
|
+
3. Proxy redirects to
|
|
118
|
+
`{return_origin}/_becklyn/deployment-protection/vercel/callback?handoff=…&return_to=…`
|
|
119
|
+
4. App verifies the short-lived handoff token (`aud` must match its origin), sets `__becklyn_dp_session`, continues.
|
|
120
|
+
|
|
121
|
+
#### Allowlist (proxy)
|
|
122
|
+
|
|
123
|
+
`DEPLOYMENT_PROTECTION_ALLOWED_ORIGINS` is a comma-separated list:
|
|
124
|
+
|
|
125
|
+
- full origins: `https://app.example.com`
|
|
126
|
+
- host suffixes: `.vercel.app`, `example.com`
|
|
127
|
+
- `localhost` (http(s) localhost / 127.0.0.1)
|
|
128
|
+
|
|
129
|
+
Empty allowlist → any origin that passes https (or local http) validation. Prefer an allowlist in production.
|
|
130
|
+
|
|
131
|
+
#### Minimal proxy route handlers
|
|
132
|
+
|
|
133
|
+
```ts
|
|
134
|
+
// app/start/route.ts
|
|
135
|
+
import { handleAuthProxyStart } from "@becklyn/deployment-protection";
|
|
136
|
+
|
|
137
|
+
export const GET = (request: Request) => handleAuthProxyStart(request);
|
|
138
|
+
|
|
139
|
+
// app/callback/route.ts
|
|
140
|
+
import { handleAuthProxyCallback } from "@becklyn/deployment-protection";
|
|
141
|
+
|
|
142
|
+
export const GET = (request: Request) => handleAuthProxyCallback(request);
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### 4. Sign in with Vercel (legacy direct mode)
|
|
146
|
+
|
|
147
|
+
Still supported when `DEPLOYMENT_PROTECTION_AUTH_PROXY_URL` is unset:
|
|
148
|
+
|
|
149
|
+
1. Create a Sign in with Vercel app.
|
|
150
|
+
2. Add authorization callback URLs for **each** project host:
|
|
87
151
|
|
|
88
152
|
```text
|
|
89
153
|
https://<your-host>/_becklyn/deployment-protection/vercel/callback
|
|
90
154
|
http://localhost:3000/_becklyn/deployment-protection/vercel/callback
|
|
91
155
|
```
|
|
92
156
|
|
|
93
|
-
|
|
157
|
+
3. Set client id/secret on the project(s).
|
|
94
158
|
|
|
95
|
-
|
|
159
|
+
When both proxy URL and direct client credentials are set, **proxy mode wins** for the authorize step.
|
|
96
160
|
|
|
97
161
|
Scopes used: `openid email profile`.
|
|
98
162
|
|
|
99
|
-
###
|
|
163
|
+
### 5. Automation bypass
|
|
100
164
|
|
|
101
165
|
Matches Vercel’s Protection Bypass for Automation:
|
|
102
166
|
|
|
@@ -116,7 +180,7 @@ When platform Deployment Protection is disabled, **this package** enforces the b
|
|
|
116
180
|
## Behaviour
|
|
117
181
|
|
|
118
182
|
1. Disabled / misconfigured → pass through
|
|
119
|
-
2. Internal Vercel OAuth routes → handled by the package
|
|
183
|
+
2. Internal Vercel OAuth routes → handled by the package (proxy start or direct OAuth / handoff)
|
|
120
184
|
3. Login form `POST` → validate username/password, set signed HttpOnly session cookie
|
|
121
185
|
4. Valid bypass header/query/cookie → allow (optionally set cookies)
|
|
122
186
|
5. Valid session cookie → allow
|
|
@@ -136,6 +200,8 @@ Redeploy. Middleware stays installed but is a no-op.
|
|
|
136
200
|
|
|
137
201
|
```ts
|
|
138
202
|
import {
|
|
203
|
+
handleAuthProxyCallback,
|
|
204
|
+
handleAuthProxyStart,
|
|
139
205
|
handleDeploymentProtection,
|
|
140
206
|
resolveConfig,
|
|
141
207
|
withDeploymentProtection,
|
|
@@ -144,6 +210,7 @@ import {
|
|
|
144
210
|
|
|
145
211
|
- `withDeploymentProtection(options?)` / `withDeploymentProtection(next, options?)` — middleware/proxy factory
|
|
146
212
|
- `handleDeploymentProtection(request, options?)` — framework-agnostic core (`null` = continue)
|
|
213
|
+
- `handleAuthProxyStart` / `handleAuthProxyCallback` — central auth-proxy routes
|
|
147
214
|
|
|
148
215
|
Recommended matcher (must be pasted as a string literal in your middleware/proxy file — see above):
|
|
149
216
|
|
|
@@ -155,6 +222,8 @@ Recommended matcher (must be pasted as a string literal in your middleware/proxy
|
|
|
155
222
|
|
|
156
223
|
- This is a **shared-secret gate**, not per-user product auth.
|
|
157
224
|
- Prefer a dedicated `DEPLOYMENT_PROTECTION_SECRET` in production.
|
|
225
|
+
- Auth-proxy start requests are HMAC-signed; handoff tokens are short-lived and audience-bound.
|
|
226
|
+
- Do not put the raw handoff secret in query strings — only signatures/tokens.
|
|
158
227
|
- Turn off paid Vercel Password Protection / seat-heavy sharing once this is live; keep `VERCEL_AUTOMATION_BYPASS_SECRET` configured for tooling.
|
|
159
228
|
- Middleware is a convenience edge check — do not treat it as the only control for highly sensitive data.
|
|
160
229
|
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { DeploymentProtectionOptions } from "./types";
|
|
2
|
+
/** Fixed OAuth callback path registered once on the Vercel OAuth app. */
|
|
3
|
+
export declare const AUTH_PROXY_CALLBACK_PATH = "/callback";
|
|
4
|
+
export declare const AUTH_PROXY_START_PATH = "/start";
|
|
5
|
+
/**
|
|
6
|
+
* Auth-proxy entry: verify the signed start request from a protected app, then
|
|
7
|
+
* begin Sign in with Vercel against this proxy's fixed callback URL.
|
|
8
|
+
*/
|
|
9
|
+
export declare function handleAuthProxyStart(request: Request, options?: DeploymentProtectionOptions): Promise<Response>;
|
|
10
|
+
/**
|
|
11
|
+
* Auth-proxy Vercel OAuth callback: exchange the code, mint a short-lived handoff
|
|
12
|
+
* token, and redirect back to the protected app callback.
|
|
13
|
+
*/
|
|
14
|
+
export declare function handleAuthProxyCallback(request: Request, options?: DeploymentProtectionOptions): Promise<Response>;
|
|
15
|
+
//# sourceMappingURL=auth-proxy.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth-proxy.d.ts","sourceRoot":"","sources":["../../src/auth-proxy.ts"],"names":[],"mappings":"AAiBA,OAAO,KAAK,EAAE,2BAA2B,EAAE,MAAM,SAAS,CAAC;AAY3D,yEAAyE;AACzE,eAAO,MAAM,wBAAwB,cAAc,CAAC;AACpD,eAAO,MAAM,qBAAqB,WAAW,CAAC;AA0B9C;;;GAGG;AACH,wBAAsB,oBAAoB,CACtC,OAAO,EAAE,OAAO,EAChB,OAAO,GAAE,2BAAgC,GAC1C,OAAO,CAAC,QAAQ,CAAC,CAwCnB;AAED;;;GAGG;AACH,wBAAsB,uBAAuB,CACzC,OAAO,EAAE,OAAO,EAChB,OAAO,GAAE,2BAAgC,GAC1C,OAAO,CAAC,QAAQ,CAAC,CAqEnB"}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.AUTH_PROXY_START_PATH = exports.AUTH_PROXY_CALLBACK_PATH = void 0;
|
|
4
|
+
exports.handleAuthProxyStart = handleAuthProxyStart;
|
|
5
|
+
exports.handleAuthProxyCallback = handleAuthProxyCallback;
|
|
6
|
+
const config_1 = require("./config");
|
|
7
|
+
const constants_1 = require("./constants");
|
|
8
|
+
const crypto_1 = require("./crypto");
|
|
9
|
+
const handoff_1 = require("./handoff");
|
|
10
|
+
const safe_return_to_1 = require("./safe-return-to");
|
|
11
|
+
const vercel_oauth_1 = require("./vercel-oauth");
|
|
12
|
+
/** Fixed OAuth callback path registered once on the Vercel OAuth app. */
|
|
13
|
+
exports.AUTH_PROXY_CALLBACK_PATH = "/callback";
|
|
14
|
+
exports.AUTH_PROXY_START_PATH = "/start";
|
|
15
|
+
function isSecureRequest(request) {
|
|
16
|
+
return new URL(request.url).protocol === "https:";
|
|
17
|
+
}
|
|
18
|
+
function textResponse(message, status) {
|
|
19
|
+
return new Response(message, {
|
|
20
|
+
status,
|
|
21
|
+
headers: {
|
|
22
|
+
"Content-Type": "text/plain; charset=utf-8",
|
|
23
|
+
"Cache-Control": "no-store",
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
function cookieOptions(secure) {
|
|
28
|
+
return {
|
|
29
|
+
httpOnly: true,
|
|
30
|
+
sameSite: "lax",
|
|
31
|
+
secure,
|
|
32
|
+
path: "/",
|
|
33
|
+
maxAge: 10 * 60,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Auth-proxy entry: verify the signed start request from a protected app, then
|
|
38
|
+
* begin Sign in with Vercel against this proxy's fixed callback URL.
|
|
39
|
+
*/
|
|
40
|
+
async function handleAuthProxyStart(request, options = {}) {
|
|
41
|
+
const config = (0, config_1.resolveConfig)(options);
|
|
42
|
+
if (!(0, config_1.hasVercelDirectAuth)(config) || !config.handoffSecret) {
|
|
43
|
+
return textResponse("Auth proxy is not configured", 500);
|
|
44
|
+
}
|
|
45
|
+
const url = new URL(request.url);
|
|
46
|
+
const returnOrigin = (0, handoff_1.normalizeReturnOrigin)(url.searchParams.get("return_origin"));
|
|
47
|
+
const returnPath = (0, safe_return_to_1.safeReturnTo)(url.searchParams.get("return_path"));
|
|
48
|
+
const expRaw = url.searchParams.get("exp");
|
|
49
|
+
const nonce = url.searchParams.get("nonce");
|
|
50
|
+
const sig = url.searchParams.get("sig");
|
|
51
|
+
const exp = expRaw ? Number(expRaw) : NaN;
|
|
52
|
+
if (!returnOrigin || !nonce || !Number.isFinite(exp)) {
|
|
53
|
+
return textResponse("Invalid start request", 400);
|
|
54
|
+
}
|
|
55
|
+
if (!(0, handoff_1.isReturnOriginAllowed)(returnOrigin, config.allowedReturnOrigins)) {
|
|
56
|
+
return textResponse("Return origin is not allowed", 403);
|
|
57
|
+
}
|
|
58
|
+
const params = {
|
|
59
|
+
returnOrigin,
|
|
60
|
+
returnPath,
|
|
61
|
+
exp,
|
|
62
|
+
nonce,
|
|
63
|
+
};
|
|
64
|
+
if (!(await (0, handoff_1.verifyProxyStartSignature)(config.handoffSecret, params, sig))) {
|
|
65
|
+
return textResponse("Invalid or expired start signature", 403);
|
|
66
|
+
}
|
|
67
|
+
const response = await (0, vercel_oauth_1.buildVercelAuthorizeRedirect)(request, config, returnPath, {
|
|
68
|
+
callbackPath: exports.AUTH_PROXY_CALLBACK_PATH,
|
|
69
|
+
});
|
|
70
|
+
const secure = isSecureRequest(request);
|
|
71
|
+
(0, vercel_oauth_1.appendSetCookie)(response, constants_1.OAUTH_RETURN_ORIGIN_COOKIE, returnOrigin, cookieOptions(secure));
|
|
72
|
+
return response;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Auth-proxy Vercel OAuth callback: exchange the code, mint a short-lived handoff
|
|
76
|
+
* token, and redirect back to the protected app callback.
|
|
77
|
+
*/
|
|
78
|
+
async function handleAuthProxyCallback(request, options = {}) {
|
|
79
|
+
const config = (0, config_1.resolveConfig)(options);
|
|
80
|
+
if (!(0, config_1.hasVercelDirectAuth)(config) || !config.handoffSecret) {
|
|
81
|
+
return textResponse("Auth proxy is not configured", 500);
|
|
82
|
+
}
|
|
83
|
+
const url = new URL(request.url);
|
|
84
|
+
const code = url.searchParams.get("code");
|
|
85
|
+
const state = url.searchParams.get("state");
|
|
86
|
+
const secure = isSecureRequest(request);
|
|
87
|
+
const returnOrigin = (0, handoff_1.normalizeReturnOrigin)((0, vercel_oauth_1.readCookie)(request, constants_1.OAUTH_RETURN_ORIGIN_COOKIE));
|
|
88
|
+
const returnPath = (0, safe_return_to_1.safeReturnTo)((0, vercel_oauth_1.readCookie)(request, constants_1.OAUTH_RETURN_COOKIE));
|
|
89
|
+
const fail = (message, status = 401) => {
|
|
90
|
+
const response = textResponse(message, status);
|
|
91
|
+
(0, vercel_oauth_1.clearOAuthCookies)(response, secure);
|
|
92
|
+
return response;
|
|
93
|
+
};
|
|
94
|
+
if (!returnOrigin) {
|
|
95
|
+
return fail("Missing return origin", 400);
|
|
96
|
+
}
|
|
97
|
+
if (!(0, handoff_1.isReturnOriginAllowed)(returnOrigin, config.allowedReturnOrigins)) {
|
|
98
|
+
return fail("Return origin is not allowed", 403);
|
|
99
|
+
}
|
|
100
|
+
if (!code || !(await (0, vercel_oauth_1.assertOAuthState)(request, state))) {
|
|
101
|
+
return fail("Vercel sign-in failed (invalid state)");
|
|
102
|
+
}
|
|
103
|
+
try {
|
|
104
|
+
const codeVerifier = (0, vercel_oauth_1.readCookie)(request, constants_1.OAUTH_VERIFIER_COOKIE);
|
|
105
|
+
if (!codeVerifier) {
|
|
106
|
+
throw new Error("Missing PKCE verifier");
|
|
107
|
+
}
|
|
108
|
+
const tokenData = await (0, vercel_oauth_1.exchangeVercelCode)(request, config, code, codeVerifier, {
|
|
109
|
+
callbackPath: exports.AUTH_PROXY_CALLBACK_PATH,
|
|
110
|
+
});
|
|
111
|
+
const storedNonce = (0, vercel_oauth_1.readCookie)(request, constants_1.OAUTH_NONCE_COOKIE);
|
|
112
|
+
const tokenNonce = (0, vercel_oauth_1.decodeIdTokenNonce)(tokenData.id_token);
|
|
113
|
+
if (storedNonce && tokenNonce && !(await (0, crypto_1.timingSafeEqualString)(storedNonce, tokenNonce))) {
|
|
114
|
+
throw new Error("Nonce mismatch");
|
|
115
|
+
}
|
|
116
|
+
const user = await (0, vercel_oauth_1.fetchVercelUserInfo)(tokenData.access_token);
|
|
117
|
+
const subject = user.preferred_username || user.email || user.sub || "vercel-user";
|
|
118
|
+
const handoff = await (0, handoff_1.createHandoffToken)(config.handoffSecret, returnOrigin, subject);
|
|
119
|
+
const target = new URL(constants_1.VERCEL_CALLBACK_PATH, `${returnOrigin}/`);
|
|
120
|
+
target.searchParams.set("handoff", handoff);
|
|
121
|
+
target.searchParams.set("return_to", returnPath);
|
|
122
|
+
const response = new Response(null, {
|
|
123
|
+
status: 302,
|
|
124
|
+
headers: {
|
|
125
|
+
Location: target.toString(),
|
|
126
|
+
"Cache-Control": "no-store",
|
|
127
|
+
},
|
|
128
|
+
});
|
|
129
|
+
(0, vercel_oauth_1.clearOAuthCookies)(response, secure);
|
|
130
|
+
return response;
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
return fail("Vercel sign-in failed");
|
|
134
|
+
}
|
|
135
|
+
}
|
package/dist/cjs/config.d.ts
CHANGED
|
@@ -4,6 +4,13 @@ import type { DeploymentProtectionConfig, DeploymentProtectionOptions } from "./
|
|
|
4
4
|
*/
|
|
5
5
|
export declare function resolveConfig(options?: DeploymentProtectionOptions): DeploymentProtectionConfig;
|
|
6
6
|
export declare function hasPasswordAuth(config: DeploymentProtectionConfig): boolean;
|
|
7
|
+
/** Direct Vercel OAuth on the protected app (legacy / single-project setup). */
|
|
8
|
+
export declare function hasVercelDirectAuth(config: DeploymentProtectionConfig): boolean;
|
|
9
|
+
/**
|
|
10
|
+
* Central auth-proxy mode: apps only need the proxy URL + shared handoff secret.
|
|
11
|
+
* Vercel client credentials live solely on the proxy.
|
|
12
|
+
*/
|
|
13
|
+
export declare function hasVercelProxyAuth(config: DeploymentProtectionConfig): boolean;
|
|
7
14
|
export declare function hasVercelAuth(config: DeploymentProtectionConfig): boolean;
|
|
8
15
|
/**
|
|
9
16
|
* Protection only runs when enabled and at least one auth method is configured.
|
package/dist/cjs/config.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/config.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/config.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,0BAA0B,EAAE,2BAA2B,EAAU,MAAM,SAAS,CAAC;AA0B/F;;GAEG;AACH,wBAAgB,aAAa,CACzB,OAAO,GAAE,2BAAgC,GAC1C,0BAA0B,CA0D5B;AAED,wBAAgB,eAAe,CAAC,MAAM,EAAE,0BAA0B,GAAG,OAAO,CAE3E;AAED,gFAAgF;AAChF,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,0BAA0B,GAAG,OAAO,CAE/E;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,0BAA0B,GAAG,OAAO,CAE9E;AAED,wBAAgB,aAAa,CAAC,MAAM,EAAE,0BAA0B,GAAG,OAAO,CAEzE;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,0BAA0B,GAAG,OAAO,CAK9E"}
|
package/dist/cjs/config.js
CHANGED
|
@@ -2,8 +2,11 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.resolveConfig = resolveConfig;
|
|
4
4
|
exports.hasPasswordAuth = hasPasswordAuth;
|
|
5
|
+
exports.hasVercelDirectAuth = hasVercelDirectAuth;
|
|
6
|
+
exports.hasVercelProxyAuth = hasVercelProxyAuth;
|
|
5
7
|
exports.hasVercelAuth = hasVercelAuth;
|
|
6
8
|
exports.isProtectionActive = isProtectionActive;
|
|
9
|
+
const handoff_1 = require("./handoff");
|
|
7
10
|
const FALSEY = new Set(["0", "false", "no", "off"]);
|
|
8
11
|
function read(env, ...keys) {
|
|
9
12
|
for (const key of keys) {
|
|
@@ -35,12 +38,17 @@ function resolveConfig(options = {}) {
|
|
|
35
38
|
// Prefer an explicit secret; otherwise derive a stable key from credentials so
|
|
36
39
|
// projects can stay zero-config beyond username/password.
|
|
37
40
|
const secret = explicitSecret ?? (username && password ? `dp:${username}:${password}` : null);
|
|
41
|
+
const handoffSecret = read(env, "DEPLOYMENT_PROTECTION_HANDOFF_SECRET") ?? explicitSecret ?? secret;
|
|
42
|
+
const authProxyUrl = read(env, "DEPLOYMENT_PROTECTION_AUTH_PROXY_URL")?.replace(/\/$/, "") ?? null;
|
|
38
43
|
return {
|
|
39
44
|
enabled: isEnabled(env),
|
|
40
45
|
username,
|
|
41
46
|
password,
|
|
42
47
|
secret,
|
|
48
|
+
handoffSecret,
|
|
43
49
|
bypassSecret: read(env, "VERCEL_AUTOMATION_BYPASS_SECRET", "DEPLOYMENT_PROTECTION_BYPASS_SECRET"),
|
|
50
|
+
authProxyUrl,
|
|
51
|
+
allowedReturnOrigins: (0, handoff_1.parseAllowlist)(read(env, "DEPLOYMENT_PROTECTION_ALLOWED_ORIGINS", "DEPLOYMENT_PROTECTION_AUTH_PROXY_ALLOWED_ORIGINS")),
|
|
44
52
|
vercelClientId: read(env, "DEPLOYMENT_PROTECTION_VERCEL_CLIENT_ID", "NEXT_PUBLIC_VERCEL_APP_CLIENT_ID", "VERCEL_APP_CLIENT_ID"),
|
|
45
53
|
vercelClientSecret: read(env, "DEPLOYMENT_PROTECTION_VERCEL_CLIENT_SECRET", "VERCEL_APP_CLIENT_SECRET"),
|
|
46
54
|
sessionTtlSeconds: options.sessionTtlSeconds ?? 60 * 60 * 24 * 14,
|
|
@@ -55,9 +63,20 @@ function resolveConfig(options = {}) {
|
|
|
55
63
|
function hasPasswordAuth(config) {
|
|
56
64
|
return Boolean(config.username && config.password && config.secret);
|
|
57
65
|
}
|
|
58
|
-
|
|
66
|
+
/** Direct Vercel OAuth on the protected app (legacy / single-project setup). */
|
|
67
|
+
function hasVercelDirectAuth(config) {
|
|
59
68
|
return Boolean(config.vercelClientId && config.vercelClientSecret && config.secret);
|
|
60
69
|
}
|
|
70
|
+
/**
|
|
71
|
+
* Central auth-proxy mode: apps only need the proxy URL + shared handoff secret.
|
|
72
|
+
* Vercel client credentials live solely on the proxy.
|
|
73
|
+
*/
|
|
74
|
+
function hasVercelProxyAuth(config) {
|
|
75
|
+
return Boolean(config.authProxyUrl && config.handoffSecret && config.secret);
|
|
76
|
+
}
|
|
77
|
+
function hasVercelAuth(config) {
|
|
78
|
+
return hasVercelDirectAuth(config) || hasVercelProxyAuth(config);
|
|
79
|
+
}
|
|
61
80
|
/**
|
|
62
81
|
* Protection only runs when enabled and at least one auth method is configured.
|
|
63
82
|
* Misconfigured+enabled is treated as inactive (fail-open) so local/dev isn't bricked.
|
package/dist/cjs/constants.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ export declare const OAUTH_STATE_COOKIE = "__becklyn_dp_oauth_state";
|
|
|
4
4
|
export declare const OAUTH_NONCE_COOKIE = "__becklyn_dp_oauth_nonce";
|
|
5
5
|
export declare const OAUTH_VERIFIER_COOKIE = "__becklyn_dp_oauth_verifier";
|
|
6
6
|
export declare const OAUTH_RETURN_COOKIE = "__becklyn_dp_oauth_return";
|
|
7
|
+
export declare const OAUTH_RETURN_ORIGIN_COOKIE = "__becklyn_dp_oauth_return_origin";
|
|
7
8
|
export declare const INTERNAL_PATH_PREFIX = "/_becklyn/deployment-protection";
|
|
8
9
|
export declare const VERCEL_AUTHORIZE_PATH = "/_becklyn/deployment-protection/vercel";
|
|
9
10
|
export declare const VERCEL_CALLBACK_PATH = "/_becklyn/deployment-protection/vercel/callback";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../../src/constants.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,mBAAmB,yBAAyB,CAAC;AAC1D,eAAO,MAAM,kBAAkB,wBAAwB,CAAC;AACxD,eAAO,MAAM,kBAAkB,6BAA6B,CAAC;AAC7D,eAAO,MAAM,kBAAkB,6BAA6B,CAAC;AAC7D,eAAO,MAAM,qBAAqB,gCAAgC,CAAC;AACnE,eAAO,MAAM,mBAAmB,8BAA8B,CAAC;
|
|
1
|
+
{"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../../src/constants.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,mBAAmB,yBAAyB,CAAC;AAC1D,eAAO,MAAM,kBAAkB,wBAAwB,CAAC;AACxD,eAAO,MAAM,kBAAkB,6BAA6B,CAAC;AAC7D,eAAO,MAAM,kBAAkB,6BAA6B,CAAC;AAC7D,eAAO,MAAM,qBAAqB,gCAAgC,CAAC;AACnE,eAAO,MAAM,mBAAmB,8BAA8B,CAAC;AAC/D,eAAO,MAAM,0BAA0B,qCAAqC,CAAC;AAE7E,eAAO,MAAM,oBAAoB,oCAAoC,CAAC;AACtE,eAAO,MAAM,qBAAqB,2CAAmC,CAAC;AACtE,eAAO,MAAM,oBAAoB,oDAA4C,CAAC;AAE9E,eAAO,MAAM,aAAa,+BAA+B,CAAC;AAC1D,eAAO,MAAM,wBAAwB,+BAA+B,CAAC;AAErE,eAAO,MAAM,2BAA2B,QAAoB,CAAC"}
|
package/dist/cjs/constants.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.DEFAULT_SESSION_TTL_SECONDS = exports.SET_BYPASS_COOKIE_HEADER = exports.BYPASS_HEADER = exports.VERCEL_CALLBACK_PATH = exports.VERCEL_AUTHORIZE_PATH = exports.INTERNAL_PATH_PREFIX = exports.OAUTH_RETURN_COOKIE = exports.OAUTH_VERIFIER_COOKIE = exports.OAUTH_NONCE_COOKIE = exports.OAUTH_STATE_COOKIE = exports.BYPASS_COOKIE_NAME = exports.SESSION_COOKIE_NAME = void 0;
|
|
3
|
+
exports.DEFAULT_SESSION_TTL_SECONDS = exports.SET_BYPASS_COOKIE_HEADER = exports.BYPASS_HEADER = exports.VERCEL_CALLBACK_PATH = exports.VERCEL_AUTHORIZE_PATH = exports.INTERNAL_PATH_PREFIX = exports.OAUTH_RETURN_ORIGIN_COOKIE = exports.OAUTH_RETURN_COOKIE = exports.OAUTH_VERIFIER_COOKIE = exports.OAUTH_NONCE_COOKIE = exports.OAUTH_STATE_COOKIE = exports.BYPASS_COOKIE_NAME = exports.SESSION_COOKIE_NAME = void 0;
|
|
4
4
|
exports.SESSION_COOKIE_NAME = "__becklyn_dp_session";
|
|
5
5
|
exports.BYPASS_COOKIE_NAME = "__becklyn_dp_bypass";
|
|
6
6
|
exports.OAUTH_STATE_COOKIE = "__becklyn_dp_oauth_state";
|
|
7
7
|
exports.OAUTH_NONCE_COOKIE = "__becklyn_dp_oauth_nonce";
|
|
8
8
|
exports.OAUTH_VERIFIER_COOKIE = "__becklyn_dp_oauth_verifier";
|
|
9
9
|
exports.OAUTH_RETURN_COOKIE = "__becklyn_dp_oauth_return";
|
|
10
|
+
exports.OAUTH_RETURN_ORIGIN_COOKIE = "__becklyn_dp_oauth_return_origin";
|
|
10
11
|
exports.INTERNAL_PATH_PREFIX = "/_becklyn/deployment-protection";
|
|
11
12
|
exports.VERCEL_AUTHORIZE_PATH = `${exports.INTERNAL_PATH_PREFIX}/vercel`;
|
|
12
13
|
exports.VERCEL_CALLBACK_PATH = `${exports.INTERNAL_PATH_PREFIX}/vercel/callback`;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"handler.d.ts","sourceRoot":"","sources":["../../src/handler.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"handler.d.ts","sourceRoot":"","sources":["../../src/handler.ts"],"names":[],"mappings":"AAyBA,OAAO,KAAK,EAA8B,2BAA2B,EAAE,MAAM,SAAS,CAAC;AA4RvF;;GAEG;AACH,wBAAsB,0BAA0B,CAC5C,OAAO,EAAE,OAAO,EAChB,OAAO,GAAE,2BAAgC,GAC1C,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC,CAqD1B"}
|
package/dist/cjs/handler.js
CHANGED
|
@@ -5,6 +5,7 @@ const bypass_1 = require("./bypass");
|
|
|
5
5
|
const config_1 = require("./config");
|
|
6
6
|
const constants_1 = require("./constants");
|
|
7
7
|
const crypto_1 = require("./crypto");
|
|
8
|
+
const handoff_1 = require("./handoff");
|
|
8
9
|
const login_page_1 = require("./login-page");
|
|
9
10
|
const password_1 = require("./password");
|
|
10
11
|
const safe_return_to_1 = require("./safe-return-to");
|
|
@@ -117,6 +118,19 @@ async function handleVercelAuthorize(request, config) {
|
|
|
117
118
|
return new Response("Sign in with Vercel is not configured", { status: 500 });
|
|
118
119
|
}
|
|
119
120
|
const returnTo = (0, safe_return_to_1.safeReturnTo)(new URL(request.url).searchParams.get("return_to"));
|
|
121
|
+
// Prefer central auth-proxy so apps never register per-host OAuth callbacks.
|
|
122
|
+
if ((0, config_1.hasVercelProxyAuth)(config) && config.authProxyUrl && config.handoffSecret) {
|
|
123
|
+
const startUrl = await (0, handoff_1.buildProxyStartUrl)({
|
|
124
|
+
authProxyUrl: config.authProxyUrl,
|
|
125
|
+
secret: config.handoffSecret,
|
|
126
|
+
returnOrigin: new URL(request.url).origin,
|
|
127
|
+
returnPath: returnTo,
|
|
128
|
+
});
|
|
129
|
+
return redirect(request, startUrl);
|
|
130
|
+
}
|
|
131
|
+
if (!(0, config_1.hasVercelDirectAuth)(config)) {
|
|
132
|
+
return new Response("Sign in with Vercel is not configured", { status: 500 });
|
|
133
|
+
}
|
|
120
134
|
return (0, vercel_oauth_1.buildVercelAuthorizeRedirect)(request, config, returnTo);
|
|
121
135
|
}
|
|
122
136
|
async function handleVercelCallback(request, config) {
|
|
@@ -127,9 +141,32 @@ async function handleVercelCallback(request, config) {
|
|
|
127
141
|
return new Response("Sign in with Vercel is not configured", { status: 500 });
|
|
128
142
|
}
|
|
129
143
|
const url = new URL(request.url);
|
|
144
|
+
const handoff = url.searchParams.get("handoff");
|
|
145
|
+
const secure = isSecureRequest(request);
|
|
146
|
+
// Auth-proxy handoff: short-lived signed token, no Vercel client secret on the app.
|
|
147
|
+
if (handoff) {
|
|
148
|
+
if (!config.handoffSecret) {
|
|
149
|
+
return new Response("Sign in with Vercel is not configured", { status: 500 });
|
|
150
|
+
}
|
|
151
|
+
const returnTo = (0, safe_return_to_1.safeReturnTo)(url.searchParams.get("return_to"));
|
|
152
|
+
const payload = await (0, handoff_1.verifyHandoffToken)(config.handoffSecret, handoff, url.origin);
|
|
153
|
+
if (!payload) {
|
|
154
|
+
return htmlResponse((0, login_page_1.renderLoginPage)(config, {
|
|
155
|
+
error: "Vercel sign-in failed (invalid handoff).",
|
|
156
|
+
returnTo,
|
|
157
|
+
showPassword: (0, config_1.hasPasswordAuth)(config),
|
|
158
|
+
showVercel: true,
|
|
159
|
+
}));
|
|
160
|
+
}
|
|
161
|
+
const response = redirect(request, returnTo);
|
|
162
|
+
return attachSession(response, config, "vercel", payload.subject, secure);
|
|
163
|
+
}
|
|
164
|
+
// Legacy direct OAuth callback on the protected app.
|
|
165
|
+
if (!(0, config_1.hasVercelDirectAuth)(config)) {
|
|
166
|
+
return new Response("Sign in with Vercel is not configured", { status: 500 });
|
|
167
|
+
}
|
|
130
168
|
const code = url.searchParams.get("code");
|
|
131
169
|
const state = url.searchParams.get("state");
|
|
132
|
-
const secure = isSecureRequest(request);
|
|
133
170
|
const returnTo = (0, safe_return_to_1.safeReturnTo)((0, vercel_oauth_1.readCookie)(request, constants_1.OAUTH_RETURN_COOKIE));
|
|
134
171
|
if (!code || !(await (0, vercel_oauth_1.assertOAuthState)(request, state))) {
|
|
135
172
|
const response = htmlResponse((0, login_page_1.renderLoginPage)(config, {
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
export declare const HANDOFF_TTL_SECONDS = 60;
|
|
2
|
+
export declare const PROXY_START_TTL_SECONDS: number;
|
|
3
|
+
export interface ProxyStartParams {
|
|
4
|
+
returnOrigin: string;
|
|
5
|
+
returnPath: string;
|
|
6
|
+
exp: number;
|
|
7
|
+
nonce: string;
|
|
8
|
+
}
|
|
9
|
+
export interface HandoffPayload {
|
|
10
|
+
exp: number;
|
|
11
|
+
aud: string;
|
|
12
|
+
subject: string;
|
|
13
|
+
method: "vercel";
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Canonical payload for HMAC over the proxy start request.
|
|
17
|
+
* Field order is fixed so app and proxy produce the same signature.
|
|
18
|
+
*/
|
|
19
|
+
export declare function canonicalProxyStartPayload(params: ProxyStartParams): string;
|
|
20
|
+
export declare function signProxyStart(secret: string, params: ProxyStartParams): Promise<string>;
|
|
21
|
+
export declare function verifyProxyStartSignature(secret: string, params: ProxyStartParams, signature: string | null | undefined): Promise<boolean>;
|
|
22
|
+
export declare function buildProxyStartUrl(options: {
|
|
23
|
+
authProxyUrl: string;
|
|
24
|
+
secret: string;
|
|
25
|
+
returnOrigin: string;
|
|
26
|
+
returnPath: string;
|
|
27
|
+
ttlSeconds?: number;
|
|
28
|
+
}): Promise<string>;
|
|
29
|
+
export declare function createHandoffToken(secret: string, audienceOrigin: string, subject: string, ttlSeconds?: number): Promise<string>;
|
|
30
|
+
export declare function verifyHandoffToken(secret: string, token: string | null | undefined, expectedAudienceOrigin: string): Promise<HandoffPayload | null>;
|
|
31
|
+
/**
|
|
32
|
+
* Validate a post-login return origin for the auth proxy.
|
|
33
|
+
* Only https (and http://localhost / 127.0.0.1) are accepted.
|
|
34
|
+
*/
|
|
35
|
+
export declare function normalizeReturnOrigin(value: string | null | undefined): string | null;
|
|
36
|
+
/**
|
|
37
|
+
* Optional allowlist. Entries may be:
|
|
38
|
+
* - full origins (`https://app.example.com`)
|
|
39
|
+
* - hostname suffixes (`.vercel.app`, `example.com`)
|
|
40
|
+
* - `localhost` (any localhost / 127.0.0.1 http(s) origin)
|
|
41
|
+
*
|
|
42
|
+
* Empty allowlist → any origin that passes {@link normalizeReturnOrigin}.
|
|
43
|
+
*/
|
|
44
|
+
export declare function isReturnOriginAllowed(origin: string, allowlist: string[]): boolean;
|
|
45
|
+
export declare function parseAllowlist(value: string | null | undefined): string[];
|
|
46
|
+
//# sourceMappingURL=handoff.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"handoff.d.ts","sourceRoot":"","sources":["../../src/handoff.ts"],"names":[],"mappings":"AASA,eAAO,MAAM,mBAAmB,KAAK,CAAC;AACtC,eAAO,MAAM,uBAAuB,QAAS,CAAC;AAE9C,MAAM,WAAW,gBAAgB;IAC7B,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,cAAc;IAC3B,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,QAAQ,CAAC;CACpB;AAeD;;;GAGG;AACH,wBAAgB,0BAA0B,CAAC,MAAM,EAAE,gBAAgB,GAAG,MAAM,CAE3E;AAED,wBAAsB,cAAc,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,gBAAgB,GAAG,OAAO,CAAC,MAAM,CAAC,CAE9F;AAED,wBAAsB,yBAAyB,CAC3C,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,gBAAgB,EACxB,SAAS,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GACrC,OAAO,CAAC,OAAO,CAAC,CAWlB;AAED,wBAAsB,kBAAkB,CAAC,OAAO,EAAE;IAC9C,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,MAAM,CAAC;IACf,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;CACvB,GAAG,OAAO,CAAC,MAAM,CAAC,CAgBlB;AAWD,wBAAsB,kBAAkB,CACpC,MAAM,EAAE,MAAM,EACd,cAAc,EAAE,MAAM,EACtB,OAAO,EAAE,MAAM,EACf,UAAU,SAAsB,GACjC,OAAO,CAAC,MAAM,CAAC,CAUjB;AAED,wBAAsB,kBAAkB,CACpC,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EAChC,sBAAsB,EAAE,MAAM,GAC/B,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC,CAwChC;AAED;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,GAAG,IAAI,CA4BrF;AAED;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,GAAG,OAAO,CAgDlF;AAED,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,MAAM,EAAE,CASzE"}
|