@baseworks/auth 0.2.7 → 1.0.2
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 +249 -84
- package/dist/chunk-3XJIAYEG.js +88 -0
- package/dist/{chunk-C4V5LCFA.js → chunk-GXDYEKX7.js} +3 -3
- package/dist/cli-auth.d.ts +7 -1
- package/dist/cli-auth.js +1 -1
- package/dist/cli.d.ts +15 -1
- package/dist/cli.js +57 -15
- package/dist/hono.d.ts +11 -5
- package/dist/hono.js +41 -14
- package/dist/identity.d.ts +95 -0
- package/dist/identity.js +16 -0
- package/dist/index.d.ts +0 -1
- package/dist/index.js +5 -25
- package/package.json +15 -19
- package/src/__tests__/identity.test.ts +238 -0
- package/src/cli-auth.ts +13 -4
- package/src/cli.ts +103 -16
- package/src/hono.ts +68 -16
- package/src/identity.ts +224 -0
- package/src/index.ts +3 -17
- package/dist/chunk-3NJASEF4.js +0 -69
- package/dist/chunk-JS5Y2DTW.js +0 -387
- package/dist/edge.d.ts +0 -17
- package/dist/edge.js +0 -145
- package/dist/session.d.ts +0 -42
- package/dist/session.js +0 -27
- package/dist/url-helpers.d.ts +0 -11
- package/dist/url-helpers.js +0 -22
- package/src/edge.ts +0 -174
- package/src/session.ts +0 -512
- package/src/url-helpers.ts +0 -66
package/src/edge.ts
DELETED
|
@@ -1,174 +0,0 @@
|
|
|
1
|
-
// Edge/server session: Pomerium JWT assertion headers + OIDC cookie fallback.
|
|
2
|
-
// Requires: next/headers (peer dep: next).
|
|
3
|
-
|
|
4
|
-
import { headers } from "next/headers";
|
|
5
|
-
import { redirect } from "next/navigation";
|
|
6
|
-
import { getSessionFromCookies, type OidcSession } from "./session";
|
|
7
|
-
import { buildPublicUrl, normalizeUrlLike } from "./url-helpers";
|
|
8
|
-
import { parseJwtPayload } from "./jwt.js";
|
|
9
|
-
|
|
10
|
-
export type EdgeAuthProvider = "anonymous" | "cookie" | "pomerium";
|
|
11
|
-
|
|
12
|
-
export type EdgeSession = OidcSession & {
|
|
13
|
-
groups?: string[];
|
|
14
|
-
provider: EdgeAuthProvider;
|
|
15
|
-
rawClaims?: Record<string, unknown>;
|
|
16
|
-
role?: string;
|
|
17
|
-
username?: string;
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
function firstHeader(headerStore: Headers, names: string[]) {
|
|
21
|
-
for (const name of names) {
|
|
22
|
-
const value = headerStore.get(name);
|
|
23
|
-
if (value) return value;
|
|
24
|
-
}
|
|
25
|
-
return undefined;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function stringClaim(claims: Record<string, unknown>, keys: string[]) {
|
|
29
|
-
for (const key of keys) {
|
|
30
|
-
const value = claims[key];
|
|
31
|
-
if (typeof value === "string" && value.trim()) return value;
|
|
32
|
-
if (Array.isArray(value)) {
|
|
33
|
-
const first = value.find((item) => typeof item === "string" && item.trim());
|
|
34
|
-
if (first) return first as string;
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
return undefined;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function parseHeaderClaim(value: string | undefined) {
|
|
41
|
-
if (!value) return undefined;
|
|
42
|
-
try {
|
|
43
|
-
const parsed = JSON.parse(value) as unknown;
|
|
44
|
-
if (typeof parsed === "string" && parsed.trim()) return parsed;
|
|
45
|
-
if (Array.isArray(parsed)) {
|
|
46
|
-
const first = parsed.find((item) => typeof item === "string" && item.trim());
|
|
47
|
-
if (first) return first as string;
|
|
48
|
-
}
|
|
49
|
-
} catch {
|
|
50
|
-
// Pomerium may forward plain header values depending on configuration.
|
|
51
|
-
}
|
|
52
|
-
return value.trim() || undefined;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function stringHeaderClaim(headerStore: Headers, names: string[]) {
|
|
56
|
-
for (const name of names) {
|
|
57
|
-
const value = parseHeaderClaim(headerStore.get(name) ?? undefined);
|
|
58
|
-
if (value) return value;
|
|
59
|
-
}
|
|
60
|
-
return undefined;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function stringArrayClaim(claims: Record<string, unknown>, keys: string[]) {
|
|
64
|
-
for (const key of keys) {
|
|
65
|
-
const value = claims[key];
|
|
66
|
-
if (typeof value === "string" && value.trim()) return [value];
|
|
67
|
-
if (Array.isArray(value)) {
|
|
68
|
-
const strings = value.filter((item): item is string => typeof item === "string" && item.trim().length > 0);
|
|
69
|
-
if (strings.length) return strings;
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
return undefined;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function stringArrayHeaderClaim(headerStore: Headers, names: string[]) {
|
|
76
|
-
for (const name of names) {
|
|
77
|
-
const raw = headerStore.get(name);
|
|
78
|
-
if (!raw) continue;
|
|
79
|
-
try {
|
|
80
|
-
const parsed = JSON.parse(raw) as unknown;
|
|
81
|
-
if (typeof parsed === "string" && parsed.trim()) return [parsed];
|
|
82
|
-
if (Array.isArray(parsed)) {
|
|
83
|
-
const strings = parsed.filter((item): item is string => typeof item === "string" && item.trim().length > 0);
|
|
84
|
-
if (strings.length) return strings;
|
|
85
|
-
}
|
|
86
|
-
} catch {
|
|
87
|
-
const values = raw.split(",").map((v) => v.trim()).filter(Boolean);
|
|
88
|
-
if (values.length) return values;
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
return undefined;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
function parsePomeriumSession(headerStore: Headers): EdgeSession | null {
|
|
95
|
-
const assertion = firstHeader(headerStore, ["x-pomerium-jwt-assertion", "x-pomerium-jwt"]);
|
|
96
|
-
const claims = assertion ? parseJwtPayload(assertion) : {};
|
|
97
|
-
|
|
98
|
-
const email =
|
|
99
|
-
stringHeaderClaim(headerStore, ["x-pomerium-claim-email", "x-forwarded-email"]) ??
|
|
100
|
-
stringClaim(claims ?? {}, ["email"]);
|
|
101
|
-
const subject =
|
|
102
|
-
stringHeaderClaim(headerStore, ["x-pomerium-claim-sub", "x-pomerium-claim-user", "x-user-id"]) ??
|
|
103
|
-
stringClaim(claims ?? {}, ["sub", "user", "id"]);
|
|
104
|
-
const name =
|
|
105
|
-
stringHeaderClaim(headerStore, ["x-pomerium-claim-name", "x-forwarded-user"]) ??
|
|
106
|
-
stringClaim(claims ?? {}, ["name", "preferred_username"]);
|
|
107
|
-
const pictureUrl =
|
|
108
|
-
stringHeaderClaim(headerStore, ["x-pomerium-claim-picture"]) ??
|
|
109
|
-
stringClaim(claims ?? {}, ["picture"]);
|
|
110
|
-
const username =
|
|
111
|
-
stringHeaderClaim(headerStore, ["x-pomerium-claim-username", "x-pomerium-claim-preferred-username"]) ??
|
|
112
|
-
stringClaim(claims ?? {}, ["username", "preferred_username", "user"]);
|
|
113
|
-
const groups =
|
|
114
|
-
stringArrayHeaderClaim(headerStore, ["x-pomerium-claim-groups", "x-pomerium-claim-roles"]) ??
|
|
115
|
-
stringArrayClaim(claims ?? {}, ["groups", "roles"]);
|
|
116
|
-
const role =
|
|
117
|
-
stringHeaderClaim(headerStore, ["x-pomerium-claim-role"]) ??
|
|
118
|
-
stringClaim(claims ?? {}, ["role"]);
|
|
119
|
-
|
|
120
|
-
if (!email && !subject && !name) return null;
|
|
121
|
-
|
|
122
|
-
return {
|
|
123
|
-
email,
|
|
124
|
-
expiresAt: Number.MAX_SAFE_INTEGER,
|
|
125
|
-
groups,
|
|
126
|
-
isAuthenticated: true,
|
|
127
|
-
name,
|
|
128
|
-
pictureUrl,
|
|
129
|
-
provider: "pomerium",
|
|
130
|
-
rawClaims: claims ?? undefined,
|
|
131
|
-
role,
|
|
132
|
-
subject: subject ?? email,
|
|
133
|
-
username,
|
|
134
|
-
};
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
export async function getEdgeSession(): Promise<EdgeSession> {
|
|
138
|
-
const headerStore = await headers();
|
|
139
|
-
const pomerium = parsePomeriumSession(headerStore);
|
|
140
|
-
if (pomerium) return pomerium;
|
|
141
|
-
|
|
142
|
-
const cookieSession = await getSessionFromCookies();
|
|
143
|
-
if (cookieSession.isAuthenticated) {
|
|
144
|
-
return { ...cookieSession, provider: "cookie" };
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
return { expiresAt: 0, isAuthenticated: false, provider: "anonymous" };
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
export function getPomeriumAuthenticateUrl(redirectTo: string, origin?: string) {
|
|
151
|
-
const appUrl = origin ?? process.env.APP_PUBLIC_URL ?? process.env.NEXT_PUBLIC_SITE_URL;
|
|
152
|
-
if (!appUrl) return null;
|
|
153
|
-
const url = new URL(
|
|
154
|
-
process.env.POMERIUM_LOGIN_PATH ?? "/auth/edge/login",
|
|
155
|
-
`${normalizeUrlLike(appUrl)}/`,
|
|
156
|
-
);
|
|
157
|
-
url.searchParams.set("pomerium_redirect_uri", buildPublicUrl(redirectTo, origin));
|
|
158
|
-
return url.toString();
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
export function handleEdgeLogin(request: Request) {
|
|
162
|
-
const url = new URL(request.url);
|
|
163
|
-
const redirectTo = url.searchParams.get("pomerium_redirect_uri") ?? "/";
|
|
164
|
-
redirect(redirectTo);
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
export function getPomeriumSignOutUrl(redirectTo: string, origin?: string) {
|
|
168
|
-
const authUrl =
|
|
169
|
-
process.env.POMERIUM_AUTHENTICATE_URL ?? process.env.POMERIUM_AUTH_URL ?? origin;
|
|
170
|
-
if (!authUrl) return null;
|
|
171
|
-
const url = new URL("/.pomerium/sign_out", `${normalizeUrlLike(authUrl)}/`);
|
|
172
|
-
url.searchParams.set("pomerium_redirect_uri", buildPublicUrl(redirectTo, origin));
|
|
173
|
-
return url.toString();
|
|
174
|
-
}
|
package/src/session.ts
DELETED
|
@@ -1,512 +0,0 @@
|
|
|
1
|
-
// Next.js OIDC session management — PKCE flow, cookie helpers, logout.
|
|
2
|
-
// Requires: next, next/headers, next/server (peer deps).
|
|
3
|
-
|
|
4
|
-
import { cookies } from "next/headers";
|
|
5
|
-
import type { NextRequest } from "next/server";
|
|
6
|
-
import { NextResponse } from "next/server";
|
|
7
|
-
import { base64urlEncode, base64urlDecodeString } from "@baseworks/core";
|
|
8
|
-
import { getAuthPublicUrl, normalizeUrlLike } from "./url-helpers";
|
|
9
|
-
import { generatePkce } from "./pkce.js";
|
|
10
|
-
import { parseJwtPayload } from "./jwt.js";
|
|
11
|
-
|
|
12
|
-
type DiscoveryDocument = {
|
|
13
|
-
authorization_endpoint: string;
|
|
14
|
-
end_session_endpoint?: string;
|
|
15
|
-
issuer: string;
|
|
16
|
-
jwks_uri: string;
|
|
17
|
-
token_endpoint: string;
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
type JwtPayload = {
|
|
21
|
-
aud?: string | string[];
|
|
22
|
-
email?: string;
|
|
23
|
-
exp?: number;
|
|
24
|
-
iss?: string;
|
|
25
|
-
name?: string;
|
|
26
|
-
nonce?: string;
|
|
27
|
-
picture?: string;
|
|
28
|
-
sub?: string;
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
type TokenResponse = {
|
|
32
|
-
access_token?: string;
|
|
33
|
-
error?: string;
|
|
34
|
-
error_description?: string;
|
|
35
|
-
expires_in?: number;
|
|
36
|
-
id_token?: string;
|
|
37
|
-
refresh_token?: string;
|
|
38
|
-
token_type?: string;
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
type OidcTransaction = {
|
|
42
|
-
codeVerifier: string;
|
|
43
|
-
nonce: string;
|
|
44
|
-
returnTo: string;
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
export type OidcSession = {
|
|
48
|
-
audience?: string | string[];
|
|
49
|
-
email?: string;
|
|
50
|
-
expiresAt: number;
|
|
51
|
-
isAuthenticated: boolean;
|
|
52
|
-
issuer?: string;
|
|
53
|
-
name?: string;
|
|
54
|
-
pictureUrl?: string;
|
|
55
|
-
subject?: string;
|
|
56
|
-
tokenIdentifier?: string;
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
export const oidcCookies = {
|
|
60
|
-
codeVerifier: "oidc_code_verifier",
|
|
61
|
-
state: "oidc_state",
|
|
62
|
-
nonce: "oidc_nonce",
|
|
63
|
-
returnTo: "oidc_return_to",
|
|
64
|
-
idToken: "oidc_id_token",
|
|
65
|
-
accessToken: "oidc_access_token",
|
|
66
|
-
refreshToken: "oidc_refresh_token",
|
|
67
|
-
expiresAt: "oidc_expires_at",
|
|
68
|
-
session: "oidc_session",
|
|
69
|
-
} as const;
|
|
70
|
-
|
|
71
|
-
function getIssuer() {
|
|
72
|
-
return (process.env.OIDC_ISSUER ?? "https://nesskey.com").replace(/\/+$/, "");
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function getClientId() {
|
|
76
|
-
const clientId = process.env.OIDC_CLIENT_ID;
|
|
77
|
-
if (!clientId) throw new Error("OIDC_CLIENT_ID is required.");
|
|
78
|
-
return clientId;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
function getOptionalClientSecret() {
|
|
82
|
-
return process.env.OIDC_CLIENT_SECRET;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
function getCookieDomain() {
|
|
86
|
-
const domain = process.env.OIDC_COOKIE_DOMAIN?.trim();
|
|
87
|
-
return domain || undefined;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
function getScope() {
|
|
91
|
-
return process.env.OIDC_SCOPE ?? "openid profile email offline_access";
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
function getPublicAuthBasePath() {
|
|
95
|
-
return normalizeUrlLike(process.env.OIDC_PUBLIC_BASE_PATH ?? getAuthPublicUrl() ?? "/auth");
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
function getRedirectUriOverride() {
|
|
99
|
-
return process.env.OIDC_REDIRECT_URI ?? null;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
function getDefaultSignedOutRedirect() {
|
|
103
|
-
return process.env.OIDC_DEFAULT_REDIRECT ?? "/";
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
function getPublicSiteUrlFromEnv(request?: NextRequest) {
|
|
107
|
-
const explicit =
|
|
108
|
-
process.env.APP_PUBLIC_URL ??
|
|
109
|
-
process.env.NEXT_PUBLIC_SITE_URL ??
|
|
110
|
-
process.env.OIDC_PUBLIC_SITE_URL;
|
|
111
|
-
if (explicit) return normalizeUrlLike(explicit);
|
|
112
|
-
if (request) return normalizeUrlLike(request.nextUrl.origin);
|
|
113
|
-
return null;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
function resolvePublicRedirect(target: string, request: NextRequest) {
|
|
117
|
-
if (/^https?:\/\//.test(target)) return new URL(target);
|
|
118
|
-
const publicSiteUrl = getPublicSiteUrlFromEnv(request);
|
|
119
|
-
if (publicSiteUrl) return new URL(target, publicSiteUrl);
|
|
120
|
-
return new URL(target, request.url);
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
function randomString() {
|
|
124
|
-
return base64urlEncode(crypto.getRandomValues(new Uint8Array(32)));
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
async function discoverOidc() {
|
|
128
|
-
const issuer = getIssuer();
|
|
129
|
-
const response = await fetch(`${issuer}/.well-known/openid-configuration`);
|
|
130
|
-
if (!response.ok) throw new Error(`Failed to load OIDC discovery document from ${issuer}.`);
|
|
131
|
-
return (await response.json()) as DiscoveryDocument;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
function encodeSessionCookie(session: OidcSession) {
|
|
136
|
-
return base64urlEncode(new TextEncoder().encode(JSON.stringify(session)));
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
function parseSessionCookie(value: string): OidcSession | null {
|
|
140
|
-
try {
|
|
141
|
-
const session = JSON.parse(base64urlDecodeString(value)) as OidcSession;
|
|
142
|
-
return session.isAuthenticated ? session : null;
|
|
143
|
-
} catch {
|
|
144
|
-
return null;
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
function encodeTransactionCookie(transaction: OidcTransaction) {
|
|
149
|
-
return base64urlEncode(new TextEncoder().encode(JSON.stringify(transaction)));
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
function parseTransactionCookie(value: string): OidcTransaction | null {
|
|
153
|
-
try {
|
|
154
|
-
const transaction = JSON.parse(base64urlDecodeString(value)) as OidcTransaction;
|
|
155
|
-
if (!transaction.codeVerifier || !transaction.returnTo) return null;
|
|
156
|
-
return transaction;
|
|
157
|
-
} catch {
|
|
158
|
-
return null;
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
function getTransactionCookieName(state: string) {
|
|
163
|
-
return `oidc_tx_${state}`;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
function getCookieOptions(request: NextRequest) {
|
|
167
|
-
return {
|
|
168
|
-
domain: getCookieDomain(),
|
|
169
|
-
httpOnly: true,
|
|
170
|
-
path: "/",
|
|
171
|
-
sameSite: "lax" as const,
|
|
172
|
-
secure: request.nextUrl.protocol === "https:",
|
|
173
|
-
};
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
function expireCookie(response: NextResponse, request: NextRequest, name: string) {
|
|
177
|
-
response.cookies.set(name, "", { ...getCookieOptions(request), expires: new Date(0) });
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
function setTransactionCookie(
|
|
181
|
-
response: NextResponse,
|
|
182
|
-
request: NextRequest,
|
|
183
|
-
state: string,
|
|
184
|
-
transaction: OidcTransaction,
|
|
185
|
-
) {
|
|
186
|
-
response.cookies.set(
|
|
187
|
-
getTransactionCookieName(state),
|
|
188
|
-
encodeTransactionCookie(transaction),
|
|
189
|
-
{ ...getCookieOptions(request), expires: new Date(Date.now() + 10 * 60 * 1_000) },
|
|
190
|
-
);
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
function getRedirectUri(request: NextRequest) {
|
|
194
|
-
const override = getRedirectUriOverride();
|
|
195
|
-
if (override) return override;
|
|
196
|
-
return new URL(
|
|
197
|
-
`${getPublicAuthBasePath()}/oidc/callback`,
|
|
198
|
-
getPublicSiteUrlFromEnv(request) ?? request.nextUrl.origin,
|
|
199
|
-
).toString();
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
function appendClientAuthentication(
|
|
203
|
-
headers: Headers,
|
|
204
|
-
params: URLSearchParams,
|
|
205
|
-
clientId: string,
|
|
206
|
-
clientSecret?: string,
|
|
207
|
-
) {
|
|
208
|
-
if (clientSecret) {
|
|
209
|
-
const basic = btoa(`${clientId}:${clientSecret}`);
|
|
210
|
-
headers.set("authorization", `Basic ${basic}`);
|
|
211
|
-
return;
|
|
212
|
-
}
|
|
213
|
-
params.set("client_id", clientId);
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
async function exchangeAuthorizationCode(args: {
|
|
217
|
-
clientId: string;
|
|
218
|
-
clientSecret?: string;
|
|
219
|
-
code: string;
|
|
220
|
-
codeVerifier: string;
|
|
221
|
-
redirectUri: string;
|
|
222
|
-
tokenEndpoint: string;
|
|
223
|
-
}) {
|
|
224
|
-
const params = new URLSearchParams({
|
|
225
|
-
code: args.code,
|
|
226
|
-
code_verifier: args.codeVerifier,
|
|
227
|
-
grant_type: "authorization_code",
|
|
228
|
-
redirect_uri: args.redirectUri,
|
|
229
|
-
});
|
|
230
|
-
const reqHeaders = new Headers({ "content-type": "application/x-www-form-urlencoded" });
|
|
231
|
-
appendClientAuthentication(reqHeaders, params, args.clientId, args.clientSecret);
|
|
232
|
-
const response = await fetch(args.tokenEndpoint, {
|
|
233
|
-
body: params.toString(),
|
|
234
|
-
headers: reqHeaders,
|
|
235
|
-
method: "POST",
|
|
236
|
-
});
|
|
237
|
-
const json = (await response.json()) as TokenResponse;
|
|
238
|
-
if (!response.ok || !json.id_token) {
|
|
239
|
-
throw new Error(json.error_description ?? "OIDC code exchange failed.");
|
|
240
|
-
}
|
|
241
|
-
return json;
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
async function refreshIdToken(args: {
|
|
245
|
-
clientId: string;
|
|
246
|
-
clientSecret?: string;
|
|
247
|
-
refreshToken: string;
|
|
248
|
-
tokenEndpoint: string;
|
|
249
|
-
}) {
|
|
250
|
-
const params = new URLSearchParams({
|
|
251
|
-
grant_type: "refresh_token",
|
|
252
|
-
refresh_token: args.refreshToken,
|
|
253
|
-
});
|
|
254
|
-
const reqHeaders = new Headers({ "content-type": "application/x-www-form-urlencoded" });
|
|
255
|
-
appendClientAuthentication(reqHeaders, params, args.clientId, args.clientSecret);
|
|
256
|
-
const response = await fetch(args.tokenEndpoint, {
|
|
257
|
-
body: params.toString(),
|
|
258
|
-
headers: reqHeaders,
|
|
259
|
-
method: "POST",
|
|
260
|
-
});
|
|
261
|
-
const json = (await response.json()) as TokenResponse;
|
|
262
|
-
if (!response.ok || !json.id_token) {
|
|
263
|
-
throw new Error(json.error_description ?? "OIDC token refresh failed.");
|
|
264
|
-
}
|
|
265
|
-
return json;
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
function sessionFromPayload(payload: JwtPayload): OidcSession {
|
|
269
|
-
return {
|
|
270
|
-
audience: payload.aud,
|
|
271
|
-
email: payload.email,
|
|
272
|
-
expiresAt: payload.exp ?? 0,
|
|
273
|
-
isAuthenticated: true,
|
|
274
|
-
issuer: payload.iss,
|
|
275
|
-
name: payload.name,
|
|
276
|
-
pictureUrl: payload.picture,
|
|
277
|
-
subject: payload.sub,
|
|
278
|
-
tokenIdentifier:
|
|
279
|
-
payload.sub && payload.iss ? `${payload.sub}|${payload.iss}` : undefined,
|
|
280
|
-
};
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
function applyTokenCookies(response: NextResponse, request: NextRequest, tokens: TokenResponse) {
|
|
284
|
-
const options = getCookieOptions(request);
|
|
285
|
-
const payload = (parseJwtPayload(tokens.id_token!) ?? {}) as JwtPayload;
|
|
286
|
-
const expiresAt =
|
|
287
|
-
payload.exp ??
|
|
288
|
-
(tokens.expires_in ? Math.floor(Date.now() / 1_000) + tokens.expires_in : undefined);
|
|
289
|
-
if (!expiresAt) throw new Error("The OIDC ID token did not include an expiry.");
|
|
290
|
-
|
|
291
|
-
response.cookies.set(oidcCookies.idToken, tokens.id_token!, {
|
|
292
|
-
...options,
|
|
293
|
-
expires: new Date(expiresAt * 1_000),
|
|
294
|
-
});
|
|
295
|
-
response.cookies.set(oidcCookies.expiresAt, String(expiresAt), {
|
|
296
|
-
...options,
|
|
297
|
-
expires: new Date(expiresAt * 1_000),
|
|
298
|
-
});
|
|
299
|
-
response.cookies.set(
|
|
300
|
-
oidcCookies.session,
|
|
301
|
-
encodeSessionCookie(sessionFromPayload({ ...payload, exp: expiresAt })),
|
|
302
|
-
{ ...options, expires: new Date(expiresAt * 1_000) },
|
|
303
|
-
);
|
|
304
|
-
if (tokens.access_token) {
|
|
305
|
-
response.cookies.set(oidcCookies.accessToken, tokens.access_token, {
|
|
306
|
-
...options,
|
|
307
|
-
expires: new Date(expiresAt * 1_000),
|
|
308
|
-
});
|
|
309
|
-
}
|
|
310
|
-
if (tokens.refresh_token) {
|
|
311
|
-
response.cookies.set(oidcCookies.refreshToken, tokens.refresh_token, options);
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
function clearTransientCookies(response: NextResponse, request: NextRequest) {
|
|
316
|
-
expireCookie(response, request, oidcCookies.codeVerifier);
|
|
317
|
-
expireCookie(response, request, oidcCookies.state);
|
|
318
|
-
expireCookie(response, request, oidcCookies.nonce);
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
export async function buildAuthorizationRedirect(request: NextRequest) {
|
|
322
|
-
const discovery = await discoverOidc();
|
|
323
|
-
const clientId = getClientId();
|
|
324
|
-
const { challenge, verifier } = await generatePkce();
|
|
325
|
-
const nonce = randomString();
|
|
326
|
-
const state = randomString();
|
|
327
|
-
const redirectUri = getRedirectUri(request);
|
|
328
|
-
const returnTo =
|
|
329
|
-
request.nextUrl.searchParams.get("redirectTo") ?? getDefaultSignedOutRedirect();
|
|
330
|
-
|
|
331
|
-
const url = new URL(discovery.authorization_endpoint);
|
|
332
|
-
url.searchParams.set("client_id", clientId);
|
|
333
|
-
url.searchParams.set("code_challenge", challenge);
|
|
334
|
-
url.searchParams.set("code_challenge_method", "S256");
|
|
335
|
-
url.searchParams.set("nonce", nonce);
|
|
336
|
-
url.searchParams.set("redirect_uri", redirectUri);
|
|
337
|
-
url.searchParams.set("response_type", "code");
|
|
338
|
-
url.searchParams.set("scope", getScope());
|
|
339
|
-
url.searchParams.set("state", state);
|
|
340
|
-
|
|
341
|
-
const response = NextResponse.redirect(url);
|
|
342
|
-
const options = getCookieOptions(request);
|
|
343
|
-
setTransactionCookie(response, request, state, { codeVerifier: verifier, nonce, returnTo });
|
|
344
|
-
response.cookies.set(oidcCookies.codeVerifier, verifier, options);
|
|
345
|
-
response.cookies.set(oidcCookies.state, state, options);
|
|
346
|
-
response.cookies.set(oidcCookies.nonce, nonce, options);
|
|
347
|
-
response.cookies.set(oidcCookies.returnTo, returnTo, options);
|
|
348
|
-
return response;
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
export async function handleAuthorizationCallback(request: NextRequest) {
|
|
352
|
-
const cookieStore = await cookies();
|
|
353
|
-
const code = request.nextUrl.searchParams.get("code");
|
|
354
|
-
const state = request.nextUrl.searchParams.get("state");
|
|
355
|
-
const transaction = state
|
|
356
|
-
? parseTransactionCookie(cookieStore.get(getTransactionCookieName(state))?.value ?? "")
|
|
357
|
-
: null;
|
|
358
|
-
const savedState = cookieStore.get(oidcCookies.state)?.value;
|
|
359
|
-
const codeVerifier =
|
|
360
|
-
transaction?.codeVerifier ?? cookieStore.get(oidcCookies.codeVerifier)?.value;
|
|
361
|
-
const savedNonce = transaction?.nonce ?? cookieStore.get(oidcCookies.nonce)?.value;
|
|
362
|
-
const returnTo =
|
|
363
|
-
transaction?.returnTo ??
|
|
364
|
-
cookieStore.get(oidcCookies.returnTo)?.value ??
|
|
365
|
-
getDefaultSignedOutRedirect();
|
|
366
|
-
|
|
367
|
-
if (
|
|
368
|
-
!code ||
|
|
369
|
-
!state ||
|
|
370
|
-
!codeVerifier ||
|
|
371
|
-
(!transaction && (!savedState || state !== savedState))
|
|
372
|
-
) {
|
|
373
|
-
return NextResponse.redirect(
|
|
374
|
-
resolvePublicRedirect(`${getPublicAuthBasePath()}?error=oidc_state`, request),
|
|
375
|
-
);
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
try {
|
|
379
|
-
const discovery = await discoverOidc();
|
|
380
|
-
const tokens = await exchangeAuthorizationCode({
|
|
381
|
-
clientId: getClientId(),
|
|
382
|
-
clientSecret: getOptionalClientSecret(),
|
|
383
|
-
code,
|
|
384
|
-
codeVerifier,
|
|
385
|
-
redirectUri: getRedirectUri(request),
|
|
386
|
-
tokenEndpoint: discovery.token_endpoint,
|
|
387
|
-
});
|
|
388
|
-
const payload = (parseJwtPayload(tokens.id_token!) ?? {}) as JwtPayload;
|
|
389
|
-
if (savedNonce && payload.nonce && payload.nonce !== savedNonce) {
|
|
390
|
-
throw new Error("OIDC nonce mismatch.");
|
|
391
|
-
}
|
|
392
|
-
const response = NextResponse.redirect(resolvePublicRedirect(returnTo, request));
|
|
393
|
-
applyTokenCookies(response, request, tokens);
|
|
394
|
-
clearTransientCookies(response, request);
|
|
395
|
-
expireCookie(response, request, oidcCookies.returnTo);
|
|
396
|
-
expireCookie(response, request, getTransactionCookieName(state));
|
|
397
|
-
return response;
|
|
398
|
-
} catch {
|
|
399
|
-
return NextResponse.redirect(
|
|
400
|
-
resolvePublicRedirect(`${getPublicAuthBasePath()}?error=oidc_callback`, request),
|
|
401
|
-
);
|
|
402
|
-
}
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
export async function getSessionFromCookies(): Promise<OidcSession> {
|
|
406
|
-
const cookieStore = await cookies();
|
|
407
|
-
const idToken = cookieStore.get(oidcCookies.idToken)?.value;
|
|
408
|
-
if (idToken) {
|
|
409
|
-
try {
|
|
410
|
-
return sessionFromPayload((parseJwtPayload(idToken) ?? {}) as JwtPayload);
|
|
411
|
-
} catch {
|
|
412
|
-
// fall through to compact session cookie
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
|
-
const sessionCookie = cookieStore.get(oidcCookies.session)?.value;
|
|
416
|
-
const session = sessionCookie ? parseSessionCookie(sessionCookie) : null;
|
|
417
|
-
return session ?? { expiresAt: 0, isAuthenticated: false };
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
export async function getServerIdToken() {
|
|
421
|
-
const cookieStore = await cookies();
|
|
422
|
-
return cookieStore.get(oidcCookies.idToken)?.value ?? null;
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
export async function hasServerOidcSession() {
|
|
426
|
-
const cookieStore = await cookies();
|
|
427
|
-
return Boolean(
|
|
428
|
-
cookieStore.get(oidcCookies.session)?.value ??
|
|
429
|
-
cookieStore.get(oidcCookies.idToken)?.value ??
|
|
430
|
-
cookieStore.get(oidcCookies.accessToken)?.value,
|
|
431
|
-
);
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
export async function getServerAccessToken() {
|
|
435
|
-
const cookieStore = await cookies();
|
|
436
|
-
const accessToken = cookieStore.get(oidcCookies.accessToken)?.value;
|
|
437
|
-
if (accessToken) return accessToken;
|
|
438
|
-
|
|
439
|
-
const refreshToken = cookieStore.get(oidcCookies.refreshToken)?.value;
|
|
440
|
-
if (!refreshToken) return null;
|
|
441
|
-
|
|
442
|
-
try {
|
|
443
|
-
const discovery = await discoverOidc();
|
|
444
|
-
const tokens = await refreshIdToken({
|
|
445
|
-
clientId: getClientId(),
|
|
446
|
-
clientSecret: getOptionalClientSecret(),
|
|
447
|
-
refreshToken,
|
|
448
|
-
tokenEndpoint: discovery.token_endpoint,
|
|
449
|
-
});
|
|
450
|
-
return tokens.access_token ?? null;
|
|
451
|
-
} catch {
|
|
452
|
-
return null;
|
|
453
|
-
}
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
export async function buildSessionResponse(_request?: NextRequest) {
|
|
457
|
-
const cookieStore = await cookies();
|
|
458
|
-
const idToken = cookieStore.get(oidcCookies.idToken)?.value;
|
|
459
|
-
const expiresAt = Number(cookieStore.get(oidcCookies.expiresAt)?.value ?? "0");
|
|
460
|
-
const now = Math.floor(Date.now() / 1_000);
|
|
461
|
-
|
|
462
|
-
if (!idToken || expiresAt <= now) {
|
|
463
|
-
return NextResponse.json({ expiresAt: 0, isAuthenticated: false } satisfies OidcSession, {
|
|
464
|
-
status: 401,
|
|
465
|
-
});
|
|
466
|
-
}
|
|
467
|
-
return NextResponse.json(sessionFromPayload({ ...(parseJwtPayload(idToken) ?? {}), exp: expiresAt } as JwtPayload));
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
export async function buildTokenResponse(request: NextRequest) {
|
|
471
|
-
const cookieStore = await cookies();
|
|
472
|
-
const idToken = cookieStore.get(oidcCookies.idToken)?.value;
|
|
473
|
-
const expiresAt = Number(cookieStore.get(oidcCookies.expiresAt)?.value ?? "0");
|
|
474
|
-
const now = Math.floor(Date.now() / 1_000);
|
|
475
|
-
|
|
476
|
-
if (!idToken || expiresAt <= now) {
|
|
477
|
-
const response = NextResponse.json({ token: null }, { status: 401 });
|
|
478
|
-
expireCookie(response, request, oidcCookies.idToken);
|
|
479
|
-
expireCookie(response, request, oidcCookies.refreshToken);
|
|
480
|
-
expireCookie(response, request, oidcCookies.expiresAt);
|
|
481
|
-
return response;
|
|
482
|
-
}
|
|
483
|
-
return NextResponse.json({ token: idToken });
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
export async function buildLogoutResponse(request: NextRequest) {
|
|
487
|
-
const redirectTo =
|
|
488
|
-
request.nextUrl.searchParams.get("redirectTo") ?? getDefaultSignedOutRedirect();
|
|
489
|
-
const postLogoutUrl = resolvePublicRedirect(redirectTo, request).toString();
|
|
490
|
-
|
|
491
|
-
const cookieStore = await cookies();
|
|
492
|
-
const idToken = cookieStore.get(oidcCookies.idToken)?.value;
|
|
493
|
-
|
|
494
|
-
const clearResponse = NextResponse.redirect(postLogoutUrl);
|
|
495
|
-
Object.values(oidcCookies).forEach((name) => expireCookie(clearResponse, request, name));
|
|
496
|
-
|
|
497
|
-
try {
|
|
498
|
-
const discovery = await discoverOidc();
|
|
499
|
-
if (discovery.end_session_endpoint) {
|
|
500
|
-
const endSessionUrl = new URL(discovery.end_session_endpoint);
|
|
501
|
-
endSessionUrl.searchParams.set("post_logout_redirect_uri", postLogoutUrl);
|
|
502
|
-
if (idToken) endSessionUrl.searchParams.set("id_token_hint", idToken);
|
|
503
|
-
return NextResponse.redirect(endSessionUrl.toString(), {
|
|
504
|
-
headers: clearResponse.headers,
|
|
505
|
-
});
|
|
506
|
-
}
|
|
507
|
-
} catch {
|
|
508
|
-
// Discovery failed — local-only logout.
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
return clearResponse;
|
|
512
|
-
}
|