@baseworks/auth 0.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/dist/chunk-3NJASEF4.js +69 -0
- package/dist/chunk-6TUNNS2B.js +25 -0
- package/dist/chunk-BL74TFCV.js +23 -0
- package/dist/chunk-BMPRMOI7.js +23 -0
- package/dist/chunk-C4V5LCFA.js +47 -0
- package/dist/chunk-M7EACPIB.js +33 -0
- package/dist/chunk-VBIQJKUU.js +20 -0
- package/dist/chunk-VUB4GTMI.js +402 -0
- package/dist/cli-auth.d.ts +34 -0
- package/dist/cli-auth.js +8 -0
- package/dist/edge.d.ts +17 -0
- package/dist/edge.js +155 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +54 -0
- package/dist/oidc-human.d.ts +41 -0
- package/dist/oidc-human.js +7 -0
- package/dist/oidc.d.ts +29 -0
- package/dist/oidc.js +6 -0
- package/dist/pkce.d.ts +35 -0
- package/dist/pkce.js +8 -0
- package/dist/session.d.ts +42 -0
- package/dist/session.js +25 -0
- package/dist/token.d.ts +10 -0
- package/dist/token.js +10 -0
- package/dist/url-helpers.d.ts +11 -0
- package/dist/url-helpers.js +22 -0
- package/dist/zitadel.d.ts +28 -0
- package/dist/zitadel.js +6 -0
- package/package.json +43 -0
- package/src/cli-auth.ts +103 -0
- package/src/edge.ts +189 -0
- package/src/index.ts +39 -0
- package/src/oidc-human.ts +60 -0
- package/src/oidc.ts +57 -0
- package/src/pkce.ts +73 -0
- package/src/session.ts +538 -0
- package/src/token.ts +21 -0
- package/src/url-helpers.ts +66 -0
- package/src/zitadel.ts +56 -0
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// src/url-helpers.ts
|
|
2
|
+
function normalizeUrlLike(value) {
|
|
3
|
+
if (value === "/") return "";
|
|
4
|
+
return value.replace(/\/+$/, "");
|
|
5
|
+
}
|
|
6
|
+
function joinPublicUrl(base, path = "") {
|
|
7
|
+
const normalizedBase = normalizeUrlLike(base);
|
|
8
|
+
const normalizedPath = path.startsWith("/") ? path : `/${path}`;
|
|
9
|
+
if (!normalizedBase) return normalizedPath;
|
|
10
|
+
if (/^https?:\/\//.test(normalizedBase)) {
|
|
11
|
+
return new URL(normalizedPath, `${normalizedBase}/`).toString();
|
|
12
|
+
}
|
|
13
|
+
return `${normalizedBase}${normalizedPath}`;
|
|
14
|
+
}
|
|
15
|
+
function getAuthPublicUrl() {
|
|
16
|
+
return normalizeUrlLike(process.env.NEXT_PUBLIC_AUTH_URL ?? "/auth");
|
|
17
|
+
}
|
|
18
|
+
function getAccountPublicUrl() {
|
|
19
|
+
return normalizeUrlLike(process.env.NEXT_PUBLIC_ACCOUNT_URL ?? "/account");
|
|
20
|
+
}
|
|
21
|
+
function getAppBasePath(defaultPath = "") {
|
|
22
|
+
return normalizeUrlLike(
|
|
23
|
+
process.env.NEXT_PUBLIC_APP_BASE_PATH ?? process.env.APP_BASE_PATH ?? defaultPath
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
function getPublicSiteUrl() {
|
|
27
|
+
return normalizeUrlLike(process.env.APP_PUBLIC_URL ?? process.env.NEXT_PUBLIC_SITE_URL ?? "");
|
|
28
|
+
}
|
|
29
|
+
function buildPublicUrl(pathOrUrl, fallbackOrigin) {
|
|
30
|
+
if (/^https?:\/\//.test(pathOrUrl)) return pathOrUrl;
|
|
31
|
+
const origin = getPublicSiteUrl() || fallbackOrigin;
|
|
32
|
+
if (!origin) return pathOrUrl;
|
|
33
|
+
return new URL(pathOrUrl, `${normalizeUrlLike(origin)}/`).toString();
|
|
34
|
+
}
|
|
35
|
+
function buildAuthUrl(path, redirectTo, origin) {
|
|
36
|
+
const target = joinPublicUrl(getAuthPublicUrl(), path);
|
|
37
|
+
const targetIsAbsolute = /^https?:\/\//.test(target);
|
|
38
|
+
const fallbackOrigin = "http://localhost";
|
|
39
|
+
const url = new URL(target, origin ?? fallbackOrigin);
|
|
40
|
+
url.searchParams.set("redirectTo", redirectTo);
|
|
41
|
+
const href = url.toString();
|
|
42
|
+
if (origin || targetIsAbsolute) return href;
|
|
43
|
+
return href.replace(fallbackOrigin, "");
|
|
44
|
+
}
|
|
45
|
+
function buildAuthLoginUrl(redirectTo, origin) {
|
|
46
|
+
return buildAuthUrl("/login", redirectTo, origin);
|
|
47
|
+
}
|
|
48
|
+
function buildAuthLogoutUrl(redirectTo, origin) {
|
|
49
|
+
return buildAuthUrl(
|
|
50
|
+
"/logout",
|
|
51
|
+
process.env.NEXT_PUBLIC_SIGN_OUT_REDIRECT_URL ?? redirectTo,
|
|
52
|
+
origin
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
function buildAccountProfileUrl() {
|
|
56
|
+
return joinPublicUrl(getAccountPublicUrl(), "/profile");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export {
|
|
60
|
+
normalizeUrlLike,
|
|
61
|
+
getAuthPublicUrl,
|
|
62
|
+
getAccountPublicUrl,
|
|
63
|
+
getAppBasePath,
|
|
64
|
+
getPublicSiteUrl,
|
|
65
|
+
buildPublicUrl,
|
|
66
|
+
buildAuthLoginUrl,
|
|
67
|
+
buildAuthLogoutUrl,
|
|
68
|
+
buildAccountProfileUrl
|
|
69
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import {
|
|
2
|
+
verifyOidcToken
|
|
3
|
+
} from "./chunk-BL74TFCV.js";
|
|
4
|
+
|
|
5
|
+
// src/oidc-human.ts
|
|
6
|
+
import { decodeJwt } from "jose";
|
|
7
|
+
function createOidcHumanResolver(config) {
|
|
8
|
+
const issuer = config.issuer.replace(/\/+$/, "");
|
|
9
|
+
return async (token) => {
|
|
10
|
+
let iss;
|
|
11
|
+
try {
|
|
12
|
+
iss = decodeJwt(token).iss;
|
|
13
|
+
} catch {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
if (!iss || !iss.startsWith(issuer)) return null;
|
|
17
|
+
const identity = await verifyOidcToken(token, { issuer, audience: config.audience });
|
|
18
|
+
if (!identity) return null;
|
|
19
|
+
return config.findOrCreate(identity, token);
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export {
|
|
24
|
+
createOidcHumanResolver
|
|
25
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// src/oidc.ts
|
|
2
|
+
import { createRemoteJWKSet, jwtVerify } from "jose";
|
|
3
|
+
function withoutTrailingSlash(v) {
|
|
4
|
+
return v.replace(/\/+$/, "");
|
|
5
|
+
}
|
|
6
|
+
async function verifyOidcToken(token, config) {
|
|
7
|
+
const issuer = withoutTrailingSlash(config.issuer);
|
|
8
|
+
const jwks = createRemoteJWKSet(new URL(`${issuer}/oauth/v2/keys`));
|
|
9
|
+
const opts = config.audience ? { issuer, audience: config.audience } : { issuer };
|
|
10
|
+
const result = await jwtVerify(token, jwks, opts).catch(() => null);
|
|
11
|
+
if (!result) return null;
|
|
12
|
+
const payload = result.payload;
|
|
13
|
+
const subject = String(payload.sub ?? "");
|
|
14
|
+
if (!subject) return null;
|
|
15
|
+
const email = String(payload.email ?? payload.preferred_username ?? `${subject}@unknown`);
|
|
16
|
+
const name = String(payload.name ?? payload.preferred_username ?? email);
|
|
17
|
+
const picture = payload.picture ? String(payload.picture) : void 0;
|
|
18
|
+
return { subject, issuer, email, name, picture };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export {
|
|
22
|
+
verifyOidcToken
|
|
23
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// src/zitadel.ts
|
|
2
|
+
import { createRemoteJWKSet, jwtVerify } from "jose";
|
|
3
|
+
function withoutTrailingSlash(v) {
|
|
4
|
+
return v.replace(/\/+$/, "");
|
|
5
|
+
}
|
|
6
|
+
async function verifyZitadelToken(token, config) {
|
|
7
|
+
const issuer = withoutTrailingSlash(config.issuer);
|
|
8
|
+
const jwks = createRemoteJWKSet(new URL(`${issuer}/oauth/v2/keys`));
|
|
9
|
+
const opts = config.audience ? { issuer, audience: config.audience } : { issuer };
|
|
10
|
+
const result = await jwtVerify(token, jwks, opts).catch(() => null);
|
|
11
|
+
if (!result) return null;
|
|
12
|
+
const payload = result.payload;
|
|
13
|
+
const subject = String(payload.sub ?? "");
|
|
14
|
+
if (!subject) return null;
|
|
15
|
+
const email = String(payload.email ?? payload.preferred_username ?? `${subject}@zitadel.local`);
|
|
16
|
+
const name = String(payload.name ?? payload.preferred_username ?? email);
|
|
17
|
+
const picture = payload.picture ? String(payload.picture) : void 0;
|
|
18
|
+
return { subject, issuer, email, name, picture };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export {
|
|
22
|
+
verifyZitadelToken
|
|
23
|
+
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// src/cli-auth.ts
|
|
2
|
+
async function startCliAuth(apiBase) {
|
|
3
|
+
const url = `${apiBase.replace(/\/+$/, "")}/v1/auth/start`;
|
|
4
|
+
const res = await fetch(url);
|
|
5
|
+
if (!res.ok) {
|
|
6
|
+
const text = await res.text().catch(() => res.statusText);
|
|
7
|
+
throw new Error(`Auth start failed (${res.status}): ${text}`);
|
|
8
|
+
}
|
|
9
|
+
const data = await res.json();
|
|
10
|
+
if (!data.state || !data.url) {
|
|
11
|
+
throw new Error("Invalid response from auth/start");
|
|
12
|
+
}
|
|
13
|
+
return {
|
|
14
|
+
state: data.state,
|
|
15
|
+
url: data.url,
|
|
16
|
+
expiresIn: data.expires_in ?? 600
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
async function pollCliAuth(apiBase, state, opts = {}) {
|
|
20
|
+
const intervalMs = opts.intervalMs ?? 2e3;
|
|
21
|
+
const timeoutMs = opts.timeoutMs ?? 3e5;
|
|
22
|
+
const base = apiBase.replace(/\/+$/, "");
|
|
23
|
+
const deadline = Date.now() + timeoutMs;
|
|
24
|
+
while (Date.now() < deadline) {
|
|
25
|
+
await sleep(intervalMs);
|
|
26
|
+
const res = await fetch(`${base}/v1/auth/poll/${state}`);
|
|
27
|
+
if (!res.ok) {
|
|
28
|
+
throw new Error(`Poll failed (${res.status})`);
|
|
29
|
+
}
|
|
30
|
+
const data = await res.json();
|
|
31
|
+
if (data.status === "done" && data.token) {
|
|
32
|
+
return { token: data.token };
|
|
33
|
+
}
|
|
34
|
+
if (data.status === "expired") {
|
|
35
|
+
throw new Error("Auth session expired \u2014 run login again");
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
throw new Error("Auth timed out \u2014 run login again");
|
|
39
|
+
}
|
|
40
|
+
function sleep(ms) {
|
|
41
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export {
|
|
45
|
+
startCliAuth,
|
|
46
|
+
pollCliAuth
|
|
47
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// src/pkce.ts
|
|
2
|
+
async function generatePkce() {
|
|
3
|
+
const array = new Uint8Array(32);
|
|
4
|
+
crypto.getRandomValues(array);
|
|
5
|
+
const verifier = base64url(array);
|
|
6
|
+
const encoded = new TextEncoder().encode(verifier);
|
|
7
|
+
const digest = await crypto.subtle.digest("SHA-256", encoded);
|
|
8
|
+
const challenge = base64url(new Uint8Array(digest));
|
|
9
|
+
return { verifier, challenge, method: "S256" };
|
|
10
|
+
}
|
|
11
|
+
function buildOidcAuthUrl(config) {
|
|
12
|
+
const issuer = config.issuer.replace(/\/+$/, "");
|
|
13
|
+
const scopes = (config.scopes ?? ["openid", "email", "profile"]).join(" ");
|
|
14
|
+
const params = new URLSearchParams({
|
|
15
|
+
client_id: config.clientId,
|
|
16
|
+
redirect_uri: config.redirectUri,
|
|
17
|
+
response_type: "code",
|
|
18
|
+
scope: scopes,
|
|
19
|
+
code_challenge: config.challenge,
|
|
20
|
+
code_challenge_method: "S256"
|
|
21
|
+
});
|
|
22
|
+
if (config.state) params.set("state", config.state);
|
|
23
|
+
if (config.prompt) params.set("prompt", config.prompt);
|
|
24
|
+
return `${issuer}/oauth/v2/authorize?${params.toString()}`;
|
|
25
|
+
}
|
|
26
|
+
function base64url(buf) {
|
|
27
|
+
return btoa(Array.from(buf, (b) => String.fromCharCode(b)).join("")).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export {
|
|
31
|
+
generatePkce,
|
|
32
|
+
buildOidcAuthUrl
|
|
33
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// src/token.ts
|
|
2
|
+
async function hashToken(token, pepper) {
|
|
3
|
+
const input = pepper ? `${pepper}:${token}` : token;
|
|
4
|
+
const buf = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(input));
|
|
5
|
+
return Array.from(new Uint8Array(buf), (b) => b.toString(16).padStart(2, "0")).join("");
|
|
6
|
+
}
|
|
7
|
+
function looksLikeJwt(token) {
|
|
8
|
+
return token.split(".").length === 3;
|
|
9
|
+
}
|
|
10
|
+
function stripBearer(header) {
|
|
11
|
+
if (!header?.startsWith("Bearer ")) return null;
|
|
12
|
+
const token = header.slice(7).trim();
|
|
13
|
+
return token || null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export {
|
|
17
|
+
hashToken,
|
|
18
|
+
looksLikeJwt,
|
|
19
|
+
stripBearer
|
|
20
|
+
};
|
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getAuthPublicUrl,
|
|
3
|
+
normalizeUrlLike
|
|
4
|
+
} from "./chunk-3NJASEF4.js";
|
|
5
|
+
|
|
6
|
+
// src/session.ts
|
|
7
|
+
import { cookies } from "next/headers";
|
|
8
|
+
import { NextResponse } from "next/server";
|
|
9
|
+
var oidcCookies = {
|
|
10
|
+
codeVerifier: "oidc_code_verifier",
|
|
11
|
+
state: "oidc_state",
|
|
12
|
+
nonce: "oidc_nonce",
|
|
13
|
+
returnTo: "oidc_return_to",
|
|
14
|
+
idToken: "oidc_id_token",
|
|
15
|
+
accessToken: "oidc_access_token",
|
|
16
|
+
refreshToken: "oidc_refresh_token",
|
|
17
|
+
expiresAt: "oidc_expires_at",
|
|
18
|
+
session: "oidc_session"
|
|
19
|
+
};
|
|
20
|
+
function getIssuer() {
|
|
21
|
+
return (process.env.OIDC_ISSUER ?? "https://nesskey.com").replace(/\/+$/, "");
|
|
22
|
+
}
|
|
23
|
+
function getClientId() {
|
|
24
|
+
const clientId = process.env.OIDC_CLIENT_ID;
|
|
25
|
+
if (!clientId) throw new Error("OIDC_CLIENT_ID is required.");
|
|
26
|
+
return clientId;
|
|
27
|
+
}
|
|
28
|
+
function getOptionalClientSecret() {
|
|
29
|
+
return process.env.OIDC_CLIENT_SECRET;
|
|
30
|
+
}
|
|
31
|
+
function getCookieDomain() {
|
|
32
|
+
const domain = process.env.OIDC_COOKIE_DOMAIN?.trim();
|
|
33
|
+
return domain || void 0;
|
|
34
|
+
}
|
|
35
|
+
function getScope() {
|
|
36
|
+
return process.env.OIDC_SCOPE ?? "openid profile email offline_access";
|
|
37
|
+
}
|
|
38
|
+
function getPublicAuthBasePath() {
|
|
39
|
+
return normalizeUrlLike(process.env.OIDC_PUBLIC_BASE_PATH ?? getAuthPublicUrl() ?? "/auth");
|
|
40
|
+
}
|
|
41
|
+
function getRedirectUriOverride() {
|
|
42
|
+
return process.env.OIDC_REDIRECT_URI ?? null;
|
|
43
|
+
}
|
|
44
|
+
function getDefaultSignedOutRedirect() {
|
|
45
|
+
return process.env.OIDC_DEFAULT_REDIRECT ?? "/";
|
|
46
|
+
}
|
|
47
|
+
function getPublicSiteUrlFromEnv(request) {
|
|
48
|
+
const explicit = process.env.APP_PUBLIC_URL ?? process.env.NEXT_PUBLIC_SITE_URL ?? process.env.OIDC_PUBLIC_SITE_URL;
|
|
49
|
+
if (explicit) return normalizeUrlLike(explicit);
|
|
50
|
+
if (request) return normalizeUrlLike(request.nextUrl.origin);
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
function resolvePublicRedirect(target, request) {
|
|
54
|
+
if (/^https?:\/\//.test(target)) return new URL(target);
|
|
55
|
+
const publicSiteUrl = getPublicSiteUrlFromEnv(request);
|
|
56
|
+
if (publicSiteUrl) return new URL(target, publicSiteUrl);
|
|
57
|
+
return new URL(target, request.url);
|
|
58
|
+
}
|
|
59
|
+
function base64UrlEncode(input) {
|
|
60
|
+
const buffer = typeof input === "string" ? Buffer.from(input, "utf8") : Buffer.from(input instanceof Uint8Array ? input : new Uint8Array(input));
|
|
61
|
+
return buffer.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
|
|
62
|
+
}
|
|
63
|
+
function base64UrlDecode(input) {
|
|
64
|
+
const padded = input.replace(/-/g, "+").replace(/_/g, "/");
|
|
65
|
+
const remainder = padded.length % 4;
|
|
66
|
+
const normalized = remainder === 0 ? padded : `${padded}${"=".repeat(4 - remainder)}`;
|
|
67
|
+
return Buffer.from(normalized, "base64").toString("utf8");
|
|
68
|
+
}
|
|
69
|
+
function sha256(input) {
|
|
70
|
+
return crypto.subtle.digest("SHA-256", new TextEncoder().encode(input));
|
|
71
|
+
}
|
|
72
|
+
function randomString() {
|
|
73
|
+
return base64UrlEncode(crypto.getRandomValues(new Uint8Array(32)));
|
|
74
|
+
}
|
|
75
|
+
async function createPkcePair() {
|
|
76
|
+
const verifier = randomString();
|
|
77
|
+
const challenge = base64UrlEncode(await sha256(verifier));
|
|
78
|
+
return { challenge, verifier };
|
|
79
|
+
}
|
|
80
|
+
async function discoverOidc() {
|
|
81
|
+
const issuer = getIssuer();
|
|
82
|
+
const response = await fetch(`${issuer}/.well-known/openid-configuration`);
|
|
83
|
+
if (!response.ok) throw new Error(`Failed to load OIDC discovery document from ${issuer}.`);
|
|
84
|
+
return await response.json();
|
|
85
|
+
}
|
|
86
|
+
function parseJwtPayload(token) {
|
|
87
|
+
const [, payload = ""] = token.split(".");
|
|
88
|
+
return JSON.parse(base64UrlDecode(payload));
|
|
89
|
+
}
|
|
90
|
+
function encodeSessionCookie(session) {
|
|
91
|
+
return base64UrlEncode(JSON.stringify(session));
|
|
92
|
+
}
|
|
93
|
+
function parseSessionCookie(value) {
|
|
94
|
+
try {
|
|
95
|
+
const session = JSON.parse(base64UrlDecode(value));
|
|
96
|
+
return session.isAuthenticated ? session : null;
|
|
97
|
+
} catch {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
function encodeTransactionCookie(transaction) {
|
|
102
|
+
return base64UrlEncode(JSON.stringify(transaction));
|
|
103
|
+
}
|
|
104
|
+
function parseTransactionCookie(value) {
|
|
105
|
+
try {
|
|
106
|
+
const transaction = JSON.parse(base64UrlDecode(value));
|
|
107
|
+
if (!transaction.codeVerifier || !transaction.returnTo) return null;
|
|
108
|
+
return transaction;
|
|
109
|
+
} catch {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
function getTransactionCookieName(state) {
|
|
114
|
+
return `oidc_tx_${state}`;
|
|
115
|
+
}
|
|
116
|
+
function getCookieOptions(request) {
|
|
117
|
+
return {
|
|
118
|
+
domain: getCookieDomain(),
|
|
119
|
+
httpOnly: true,
|
|
120
|
+
path: "/",
|
|
121
|
+
sameSite: "lax",
|
|
122
|
+
secure: request.nextUrl.protocol === "https:"
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
function expireCookie(response, request, name) {
|
|
126
|
+
response.cookies.set(name, "", { ...getCookieOptions(request), expires: /* @__PURE__ */ new Date(0) });
|
|
127
|
+
}
|
|
128
|
+
function setTransactionCookie(response, request, state, transaction) {
|
|
129
|
+
response.cookies.set(
|
|
130
|
+
getTransactionCookieName(state),
|
|
131
|
+
encodeTransactionCookie(transaction),
|
|
132
|
+
{ ...getCookieOptions(request), expires: new Date(Date.now() + 10 * 60 * 1e3) }
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
function getRedirectUri(request) {
|
|
136
|
+
const override = getRedirectUriOverride();
|
|
137
|
+
if (override) return override;
|
|
138
|
+
return new URL(
|
|
139
|
+
`${getPublicAuthBasePath()}/oidc/callback`,
|
|
140
|
+
getPublicSiteUrlFromEnv(request) ?? request.nextUrl.origin
|
|
141
|
+
).toString();
|
|
142
|
+
}
|
|
143
|
+
function appendClientAuthentication(headers, params, clientId, clientSecret) {
|
|
144
|
+
if (clientSecret) {
|
|
145
|
+
const basic = Buffer.from(`${clientId}:${clientSecret}`).toString("base64");
|
|
146
|
+
headers.set("authorization", `Basic ${basic}`);
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
params.set("client_id", clientId);
|
|
150
|
+
}
|
|
151
|
+
async function exchangeAuthorizationCode(args) {
|
|
152
|
+
const params = new URLSearchParams({
|
|
153
|
+
code: args.code,
|
|
154
|
+
code_verifier: args.codeVerifier,
|
|
155
|
+
grant_type: "authorization_code",
|
|
156
|
+
redirect_uri: args.redirectUri
|
|
157
|
+
});
|
|
158
|
+
const reqHeaders = new Headers({ "content-type": "application/x-www-form-urlencoded" });
|
|
159
|
+
appendClientAuthentication(reqHeaders, params, args.clientId, args.clientSecret);
|
|
160
|
+
const response = await fetch(args.tokenEndpoint, {
|
|
161
|
+
body: params.toString(),
|
|
162
|
+
headers: reqHeaders,
|
|
163
|
+
method: "POST"
|
|
164
|
+
});
|
|
165
|
+
const json = await response.json();
|
|
166
|
+
if (!response.ok || !json.id_token) {
|
|
167
|
+
throw new Error(json.error_description ?? "OIDC code exchange failed.");
|
|
168
|
+
}
|
|
169
|
+
return json;
|
|
170
|
+
}
|
|
171
|
+
async function refreshIdToken(args) {
|
|
172
|
+
const params = new URLSearchParams({
|
|
173
|
+
grant_type: "refresh_token",
|
|
174
|
+
refresh_token: args.refreshToken
|
|
175
|
+
});
|
|
176
|
+
const reqHeaders = new Headers({ "content-type": "application/x-www-form-urlencoded" });
|
|
177
|
+
appendClientAuthentication(reqHeaders, params, args.clientId, args.clientSecret);
|
|
178
|
+
const response = await fetch(args.tokenEndpoint, {
|
|
179
|
+
body: params.toString(),
|
|
180
|
+
headers: reqHeaders,
|
|
181
|
+
method: "POST"
|
|
182
|
+
});
|
|
183
|
+
const json = await response.json();
|
|
184
|
+
if (!response.ok || !json.id_token) {
|
|
185
|
+
throw new Error(json.error_description ?? "OIDC token refresh failed.");
|
|
186
|
+
}
|
|
187
|
+
return json;
|
|
188
|
+
}
|
|
189
|
+
function sessionFromPayload(payload) {
|
|
190
|
+
return {
|
|
191
|
+
audience: payload.aud,
|
|
192
|
+
email: payload.email,
|
|
193
|
+
expiresAt: payload.exp ?? 0,
|
|
194
|
+
isAuthenticated: true,
|
|
195
|
+
issuer: payload.iss,
|
|
196
|
+
name: payload.name,
|
|
197
|
+
pictureUrl: payload.picture,
|
|
198
|
+
subject: payload.sub,
|
|
199
|
+
tokenIdentifier: payload.sub && payload.iss ? `${payload.sub}|${payload.iss}` : void 0
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
function applyTokenCookies(response, request, tokens) {
|
|
203
|
+
const options = getCookieOptions(request);
|
|
204
|
+
const payload = parseJwtPayload(tokens.id_token);
|
|
205
|
+
const expiresAt = payload.exp ?? (tokens.expires_in ? Math.floor(Date.now() / 1e3) + tokens.expires_in : void 0);
|
|
206
|
+
if (!expiresAt) throw new Error("The OIDC ID token did not include an expiry.");
|
|
207
|
+
response.cookies.set(oidcCookies.idToken, tokens.id_token, {
|
|
208
|
+
...options,
|
|
209
|
+
expires: new Date(expiresAt * 1e3)
|
|
210
|
+
});
|
|
211
|
+
response.cookies.set(oidcCookies.expiresAt, String(expiresAt), {
|
|
212
|
+
...options,
|
|
213
|
+
expires: new Date(expiresAt * 1e3)
|
|
214
|
+
});
|
|
215
|
+
response.cookies.set(
|
|
216
|
+
oidcCookies.session,
|
|
217
|
+
encodeSessionCookie(sessionFromPayload({ ...payload, exp: expiresAt })),
|
|
218
|
+
{ ...options, expires: new Date(expiresAt * 1e3) }
|
|
219
|
+
);
|
|
220
|
+
if (tokens.access_token) {
|
|
221
|
+
response.cookies.set(oidcCookies.accessToken, tokens.access_token, {
|
|
222
|
+
...options,
|
|
223
|
+
expires: new Date(expiresAt * 1e3)
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
if (tokens.refresh_token) {
|
|
227
|
+
response.cookies.set(oidcCookies.refreshToken, tokens.refresh_token, options);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
function clearTransientCookies(response, request) {
|
|
231
|
+
expireCookie(response, request, oidcCookies.codeVerifier);
|
|
232
|
+
expireCookie(response, request, oidcCookies.state);
|
|
233
|
+
expireCookie(response, request, oidcCookies.nonce);
|
|
234
|
+
}
|
|
235
|
+
async function buildAuthorizationRedirect(request) {
|
|
236
|
+
const discovery = await discoverOidc();
|
|
237
|
+
const clientId = getClientId();
|
|
238
|
+
const { challenge, verifier } = await createPkcePair();
|
|
239
|
+
const nonce = randomString();
|
|
240
|
+
const state = randomString();
|
|
241
|
+
const redirectUri = getRedirectUri(request);
|
|
242
|
+
const returnTo = request.nextUrl.searchParams.get("redirectTo") ?? getDefaultSignedOutRedirect();
|
|
243
|
+
const url = new URL(discovery.authorization_endpoint);
|
|
244
|
+
url.searchParams.set("client_id", clientId);
|
|
245
|
+
url.searchParams.set("code_challenge", challenge);
|
|
246
|
+
url.searchParams.set("code_challenge_method", "S256");
|
|
247
|
+
url.searchParams.set("nonce", nonce);
|
|
248
|
+
url.searchParams.set("redirect_uri", redirectUri);
|
|
249
|
+
url.searchParams.set("response_type", "code");
|
|
250
|
+
url.searchParams.set("scope", getScope());
|
|
251
|
+
url.searchParams.set("state", state);
|
|
252
|
+
const response = NextResponse.redirect(url);
|
|
253
|
+
const options = getCookieOptions(request);
|
|
254
|
+
setTransactionCookie(response, request, state, { codeVerifier: verifier, nonce, returnTo });
|
|
255
|
+
response.cookies.set(oidcCookies.codeVerifier, verifier, options);
|
|
256
|
+
response.cookies.set(oidcCookies.state, state, options);
|
|
257
|
+
response.cookies.set(oidcCookies.nonce, nonce, options);
|
|
258
|
+
response.cookies.set(oidcCookies.returnTo, returnTo, options);
|
|
259
|
+
return response;
|
|
260
|
+
}
|
|
261
|
+
async function handleAuthorizationCallback(request) {
|
|
262
|
+
const cookieStore = await cookies();
|
|
263
|
+
const code = request.nextUrl.searchParams.get("code");
|
|
264
|
+
const state = request.nextUrl.searchParams.get("state");
|
|
265
|
+
const transaction = state ? parseTransactionCookie(cookieStore.get(getTransactionCookieName(state))?.value ?? "") : null;
|
|
266
|
+
const savedState = cookieStore.get(oidcCookies.state)?.value;
|
|
267
|
+
const codeVerifier = transaction?.codeVerifier ?? cookieStore.get(oidcCookies.codeVerifier)?.value;
|
|
268
|
+
const savedNonce = transaction?.nonce ?? cookieStore.get(oidcCookies.nonce)?.value;
|
|
269
|
+
const returnTo = transaction?.returnTo ?? cookieStore.get(oidcCookies.returnTo)?.value ?? getDefaultSignedOutRedirect();
|
|
270
|
+
if (!code || !state || !codeVerifier || !transaction && (!savedState || state !== savedState)) {
|
|
271
|
+
return NextResponse.redirect(
|
|
272
|
+
resolvePublicRedirect(`${getPublicAuthBasePath()}?error=oidc_state`, request)
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
try {
|
|
276
|
+
const discovery = await discoverOidc();
|
|
277
|
+
const tokens = await exchangeAuthorizationCode({
|
|
278
|
+
clientId: getClientId(),
|
|
279
|
+
clientSecret: getOptionalClientSecret(),
|
|
280
|
+
code,
|
|
281
|
+
codeVerifier,
|
|
282
|
+
redirectUri: getRedirectUri(request),
|
|
283
|
+
tokenEndpoint: discovery.token_endpoint
|
|
284
|
+
});
|
|
285
|
+
const payload = parseJwtPayload(tokens.id_token);
|
|
286
|
+
if (savedNonce && payload.nonce && payload.nonce !== savedNonce) {
|
|
287
|
+
throw new Error("OIDC nonce mismatch.");
|
|
288
|
+
}
|
|
289
|
+
const response = NextResponse.redirect(resolvePublicRedirect(returnTo, request));
|
|
290
|
+
applyTokenCookies(response, request, tokens);
|
|
291
|
+
clearTransientCookies(response, request);
|
|
292
|
+
expireCookie(response, request, oidcCookies.returnTo);
|
|
293
|
+
expireCookie(response, request, getTransactionCookieName(state));
|
|
294
|
+
return response;
|
|
295
|
+
} catch {
|
|
296
|
+
return NextResponse.redirect(
|
|
297
|
+
resolvePublicRedirect(`${getPublicAuthBasePath()}?error=oidc_callback`, request)
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
async function getSessionFromCookies() {
|
|
302
|
+
const cookieStore = await cookies();
|
|
303
|
+
const idToken = cookieStore.get(oidcCookies.idToken)?.value;
|
|
304
|
+
if (idToken) {
|
|
305
|
+
try {
|
|
306
|
+
return sessionFromPayload(parseJwtPayload(idToken));
|
|
307
|
+
} catch {
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
const sessionCookie = cookieStore.get(oidcCookies.session)?.value;
|
|
311
|
+
const session = sessionCookie ? parseSessionCookie(sessionCookie) : null;
|
|
312
|
+
return session ?? { expiresAt: 0, isAuthenticated: false };
|
|
313
|
+
}
|
|
314
|
+
async function getServerIdToken() {
|
|
315
|
+
const cookieStore = await cookies();
|
|
316
|
+
return cookieStore.get(oidcCookies.idToken)?.value ?? null;
|
|
317
|
+
}
|
|
318
|
+
async function hasServerOidcSession() {
|
|
319
|
+
const cookieStore = await cookies();
|
|
320
|
+
return Boolean(
|
|
321
|
+
cookieStore.get(oidcCookies.session)?.value ?? cookieStore.get(oidcCookies.idToken)?.value ?? cookieStore.get(oidcCookies.accessToken)?.value
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
async function getServerAccessToken() {
|
|
325
|
+
const cookieStore = await cookies();
|
|
326
|
+
const accessToken = cookieStore.get(oidcCookies.accessToken)?.value;
|
|
327
|
+
if (accessToken) return accessToken;
|
|
328
|
+
const refreshToken = cookieStore.get(oidcCookies.refreshToken)?.value;
|
|
329
|
+
if (!refreshToken) return null;
|
|
330
|
+
try {
|
|
331
|
+
const discovery = await discoverOidc();
|
|
332
|
+
const tokens = await refreshIdToken({
|
|
333
|
+
clientId: getClientId(),
|
|
334
|
+
clientSecret: getOptionalClientSecret(),
|
|
335
|
+
refreshToken,
|
|
336
|
+
tokenEndpoint: discovery.token_endpoint
|
|
337
|
+
});
|
|
338
|
+
return tokens.access_token ?? null;
|
|
339
|
+
} catch {
|
|
340
|
+
return null;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
async function buildSessionResponse(_request) {
|
|
344
|
+
const cookieStore = await cookies();
|
|
345
|
+
const idToken = cookieStore.get(oidcCookies.idToken)?.value;
|
|
346
|
+
const expiresAt = Number(cookieStore.get(oidcCookies.expiresAt)?.value ?? "0");
|
|
347
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
348
|
+
if (!idToken || expiresAt <= now) {
|
|
349
|
+
return NextResponse.json({ expiresAt: 0, isAuthenticated: false }, {
|
|
350
|
+
status: 401
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
return NextResponse.json(sessionFromPayload({ ...parseJwtPayload(idToken), exp: expiresAt }));
|
|
354
|
+
}
|
|
355
|
+
async function buildTokenResponse(request) {
|
|
356
|
+
const cookieStore = await cookies();
|
|
357
|
+
const idToken = cookieStore.get(oidcCookies.idToken)?.value;
|
|
358
|
+
const expiresAt = Number(cookieStore.get(oidcCookies.expiresAt)?.value ?? "0");
|
|
359
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
360
|
+
if (!idToken || expiresAt <= now) {
|
|
361
|
+
const response = NextResponse.json({ token: null }, { status: 401 });
|
|
362
|
+
expireCookie(response, request, oidcCookies.idToken);
|
|
363
|
+
expireCookie(response, request, oidcCookies.refreshToken);
|
|
364
|
+
expireCookie(response, request, oidcCookies.expiresAt);
|
|
365
|
+
return response;
|
|
366
|
+
}
|
|
367
|
+
return NextResponse.json({ token: idToken });
|
|
368
|
+
}
|
|
369
|
+
async function buildLogoutResponse(request) {
|
|
370
|
+
const redirectTo = request.nextUrl.searchParams.get("redirectTo") ?? getDefaultSignedOutRedirect();
|
|
371
|
+
const postLogoutUrl = resolvePublicRedirect(redirectTo, request).toString();
|
|
372
|
+
const cookieStore = await cookies();
|
|
373
|
+
const idToken = cookieStore.get(oidcCookies.idToken)?.value;
|
|
374
|
+
const clearResponse = NextResponse.redirect(postLogoutUrl);
|
|
375
|
+
Object.values(oidcCookies).forEach((name) => expireCookie(clearResponse, request, name));
|
|
376
|
+
try {
|
|
377
|
+
const discovery = await discoverOidc();
|
|
378
|
+
if (discovery.end_session_endpoint) {
|
|
379
|
+
const endSessionUrl = new URL(discovery.end_session_endpoint);
|
|
380
|
+
endSessionUrl.searchParams.set("post_logout_redirect_uri", postLogoutUrl);
|
|
381
|
+
if (idToken) endSessionUrl.searchParams.set("id_token_hint", idToken);
|
|
382
|
+
return NextResponse.redirect(endSessionUrl.toString(), {
|
|
383
|
+
headers: clearResponse.headers
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
} catch {
|
|
387
|
+
}
|
|
388
|
+
return clearResponse;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
export {
|
|
392
|
+
oidcCookies,
|
|
393
|
+
buildAuthorizationRedirect,
|
|
394
|
+
handleAuthorizationCallback,
|
|
395
|
+
getSessionFromCookies,
|
|
396
|
+
getServerIdToken,
|
|
397
|
+
hasServerOidcSession,
|
|
398
|
+
getServerAccessToken,
|
|
399
|
+
buildSessionResponse,
|
|
400
|
+
buildTokenResponse,
|
|
401
|
+
buildLogoutResponse
|
|
402
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI authentication helpers — PKCE polling flow.
|
|
3
|
+
* Used by any CLI tool that authenticates via an orbseal-compatible API.
|
|
4
|
+
*
|
|
5
|
+
* Flow:
|
|
6
|
+
* 1. startCliAuth() → get state + URL to show user
|
|
7
|
+
* 2. pollCliAuth() → long-poll until user completes browser auth
|
|
8
|
+
*/
|
|
9
|
+
interface CliAuthStart {
|
|
10
|
+
state: string;
|
|
11
|
+
url: string;
|
|
12
|
+
expiresIn: number;
|
|
13
|
+
}
|
|
14
|
+
interface CliAuthResult {
|
|
15
|
+
token: string;
|
|
16
|
+
}
|
|
17
|
+
interface PollOptions {
|
|
18
|
+
/** ms between polls (default: 2000) */
|
|
19
|
+
intervalMs?: number;
|
|
20
|
+
/** ms before giving up (default: 300_000 = 5 min) */
|
|
21
|
+
timeoutMs?: number;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Call the API to initiate CLI login.
|
|
25
|
+
* Returns the state handle and the URL the user should open.
|
|
26
|
+
*/
|
|
27
|
+
declare function startCliAuth(apiBase: string): Promise<CliAuthStart>;
|
|
28
|
+
/**
|
|
29
|
+
* Poll the API until the user completes browser auth.
|
|
30
|
+
* Resolves with the token when done, rejects on timeout or error.
|
|
31
|
+
*/
|
|
32
|
+
declare function pollCliAuth(apiBase: string, state: string, opts?: PollOptions): Promise<CliAuthResult>;
|
|
33
|
+
|
|
34
|
+
export { type CliAuthResult, type CliAuthStart, type PollOptions, pollCliAuth, startCliAuth };
|