@geekapps/auth-nextjs 0.3.1 → 0.3.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/dist/middleware.js +17 -3
- package/dist/middleware.js.map +1 -1
- package/package.json +1 -1
package/dist/middleware.js
CHANGED
|
@@ -74,6 +74,20 @@ function createAuthMiddleware(input = {}) {
|
|
|
74
74
|
const tokenPubCookie = `${prefix}_tp`;
|
|
75
75
|
const slotAt = (n) => `${prefix}_at${n}`;
|
|
76
76
|
const slotRt = (n) => `${prefix}_rt${n}`;
|
|
77
|
+
function redirectResponse(request, url) {
|
|
78
|
+
const isRsc = request.headers.has("rsc") || request.headers.has("next-router-prefetch") || request.nextUrl.searchParams.has("_rsc");
|
|
79
|
+
if (isRsc) {
|
|
80
|
+
return new NextResponse(null, {
|
|
81
|
+
status: 302,
|
|
82
|
+
headers: {
|
|
83
|
+
location: url.toString(),
|
|
84
|
+
"x-action-redirect": url.toString(),
|
|
85
|
+
vary: "RSC, Next-Router-State-Tree, Next-Router-Prefetch"
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
return NextResponse.redirect(url);
|
|
90
|
+
}
|
|
77
91
|
return async function authMiddleware(request) {
|
|
78
92
|
const countRaw = request.cookies.get(countCookie)?.value;
|
|
79
93
|
const count = countRaw ? Number.parseInt(countRaw, 10) : 0;
|
|
@@ -96,7 +110,7 @@ function createAuthMiddleware(input = {}) {
|
|
|
96
110
|
signInUrl.searchParams.set("redirectTo", redirectTo);
|
|
97
111
|
}
|
|
98
112
|
if (!accessToken && !refreshToken) {
|
|
99
|
-
return
|
|
113
|
+
return redirectResponse(request, signInUrl);
|
|
100
114
|
}
|
|
101
115
|
const exp = accessToken ? decodeJwtExp(accessToken) : null;
|
|
102
116
|
const nowSec = Math.floor(Date.now() / 1e3);
|
|
@@ -106,7 +120,7 @@ function createAuthMiddleware(input = {}) {
|
|
|
106
120
|
if (valid) return NextResponse.next();
|
|
107
121
|
}
|
|
108
122
|
if (!refreshToken) {
|
|
109
|
-
return
|
|
123
|
+
return redirectResponse(request, signInUrl);
|
|
110
124
|
}
|
|
111
125
|
const clientSecret = env("GEEKAPPS_AUTH_CLIENT_SECRET") ?? "";
|
|
112
126
|
const basicAuth = btoa(`${clientId ?? ""}:${clientSecret}`);
|
|
@@ -136,7 +150,7 @@ function createAuthMiddleware(input = {}) {
|
|
|
136
150
|
}
|
|
137
151
|
}
|
|
138
152
|
if (!newAccessToken) {
|
|
139
|
-
const response2 =
|
|
153
|
+
const response2 = redirectResponse(request, signInUrl);
|
|
140
154
|
const clearOpts = {
|
|
141
155
|
httpOnly: true,
|
|
142
156
|
secure: input.secureCookies ?? process.env.NODE_ENV === "production",
|
package/dist/middleware.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/middleware.ts"],"sourcesContent":["import { NextResponse, type NextRequest } from \"next/server\";\nimport { createRemoteJWKSet, jwtVerify } from \"jose\";\n\nexport type AuthMiddlewareConfig = {\n issuer?: string;\n basePath?: string;\n /**\n * @deprecated cookiePrefix is now auto-derived from GEEKAPPS_AUTH_CLIENT_ID.\n * This field is kept for backwards compatibility but is ignored.\n */\n cookiePrefix?: string;\n secureCookies?: boolean;\n /** Base URL of this app (e.g. \"https://meuapp.com\"). Overrides the GEEKAPPS_APP_URL env var. */\n appUrl?: string;\n};\n\ntype TokenLikeResponse = {\n access_token?: string;\n accessToken?: string;\n refresh_token?: string;\n refreshToken?: string;\n id_token?: string;\n idToken?: string;\n};\n\nfunction env(name: string) {\n const value = process.env[name];\n return value && value.trim().length > 0 ? value : undefined;\n}\n\nfunction normalizeBaseUrl(url: string) {\n return url.replace(/\\/$/, \"\");\n}\n\nfunction normalizeBasePath(path = \"/api/auth\") {\n if (!path.startsWith(\"/\")) return `/${path}`;\n return path.replace(/\\/$/, \"\");\n}\n\nfunction resolveIssuer(input: AuthMiddlewareConfig) {\n return normalizeBaseUrl(\n input.issuer ??\n env(\"GEEKAPPS_AUTH_ISSUER\") ??\n env(\"GEEKAPPS_AUTH_API_URL\") ??\n \"http://localhost:3020\",\n );\n}\n\nfunction hashClientId(clientId: string): string {\n let h = 5381;\n for (let i = 0; i < clientId.length; i++) {\n h = ((h << 5) + h) ^ (clientId.charCodeAt(i) & 0xff);\n h = h >>> 0;\n }\n return h.toString(16).padStart(8, \"0\");\n}\n\nfunction deriveCookiePrefix(clientId: string | undefined): string {\n if (!clientId) return \"gk\";\n return `gk_${hashClientId(clientId)}`;\n}\n\n\nconst MAX_REFRESH_ATTEMPTS = 3;\n\nfunction decodeJwtExp(token: string): number | null {\n try {\n const parts = token.split(\".\");\n if (parts.length !== 3) return null;\n const payload = parts[1]!;\n const padded = payload + \"=\".repeat((4 - (payload.length % 4)) % 4);\n const decoded = JSON.parse(atob(padded)) as { exp?: number };\n return typeof decoded.exp === \"number\" ? decoded.exp : null;\n } catch {\n return null;\n }\n}\n\nfunction resolveAppOrigin(request: NextRequest, appUrl?: string): string {\n const configured =\n appUrl ??\n env(\"GEEKAPPS_APP_URL\");\n if (configured) return configured.replace(/\\/$/, \"\");\n\n throw new Error(\n \"Cannot determine app origin for redirect. Set GEEKAPPS_APP_URL.\",\n );\n}\n\nconst _jwksCache = new Map<string, ReturnType<typeof createRemoteJWKSet>>();\n\nfunction getRemoteJWKS(issuer: string) {\n if (!_jwksCache.has(issuer)) {\n _jwksCache.set(issuer, createRemoteJWKSet(new URL(`${issuer}/.well-known/jwks.json`)));\n }\n return _jwksCache.get(issuer)!;\n}\n\nasync function verifyAccessToken(token: string, issuer: string): Promise<boolean> {\n try {\n await jwtVerify(token, getRemoteJWKS(issuer), { algorithms: [\"RS256\"], issuer });\n return true;\n } catch {\n return false;\n }\n}\n\nexport function createAuthMiddleware(input: AuthMiddlewareConfig = {}) {\n const issuer = resolveIssuer(input);\n const basePath = normalizeBasePath(input.basePath);\n const clientId = env(\"GEEKAPPS_AUTH_CLIENT_ID\");\n const prefix = deriveCookiePrefix(clientId);\n\n const activeCookie = `${prefix}_active`;\n const countCookie = `${prefix}_count`;\n const tokenPubCookie = `${prefix}_tp`; // legacy — cleared on next refresh\n const slotAt = (n: number) => `${prefix}_at${n}`;\n const slotRt = (n: number) => `${prefix}_rt${n}`;\n\n return async function authMiddleware(request: NextRequest): Promise<NextResponse> {\n const countRaw = request.cookies.get(countCookie)?.value;\n const count = countRaw ? Number.parseInt(countRaw, 10) : 0;\n\n const activeRaw = request.cookies.get(activeCookie)?.value;\n let activeIdx = activeRaw ? Number.parseInt(activeRaw, 10) : 1;\n if (Number.isNaN(activeIdx) || activeIdx < 1 || activeIdx > count) {\n activeIdx = count > 0 ? 1 : 0;\n }\n\n // Fallback: if count cookie is missing/zero but slot 1 tokens exist, use slot 1.\n // This handles the case where the count/active control cookies expired before the token cookies.\n if (activeIdx === 0) {\n const fallbackAt = request.cookies.get(slotAt(1))?.value;\n const fallbackRt = request.cookies.get(slotRt(1))?.value;\n if (fallbackAt || fallbackRt) activeIdx = 1;\n }\n\n const accessToken = activeIdx > 0 ? (request.cookies.get(slotAt(activeIdx))?.value ?? null) : null;\n const refreshToken = activeIdx > 0 ? (request.cookies.get(slotRt(activeIdx))?.value ?? null) : null;\n\n const origin = resolveAppOrigin(request, input.appUrl);\n const signInUrl = new URL(`${origin}${basePath}/sign-in`);\n const redirectTo = request.nextUrl.pathname + request.nextUrl.search;\n if (redirectTo && redirectTo !== \"/\") {\n signInUrl.searchParams.set(\"redirectTo\", redirectTo);\n }\n\n if (!accessToken && !refreshToken) {\n return NextResponse.redirect(signInUrl);\n }\n\n // Fast-path: check exp before hitting JWKS (avoids network on every request)\n const exp = accessToken ? decodeJwtExp(accessToken) : null;\n const nowSec = Math.floor(Date.now() / 1000);\n const expiredOrMissing = !exp || exp - nowSec < 60;\n\n if (!expiredOrMissing) {\n // Verify signature against app-api JWKS\n const valid = await verifyAccessToken(accessToken!, issuer);\n if (valid) return NextResponse.next();\n }\n\n if (!refreshToken) {\n return NextResponse.redirect(signInUrl);\n }\n\n const clientSecret = env(\"GEEKAPPS_AUTH_CLIENT_SECRET\") ?? \"\";\n const basicAuth = btoa(`${clientId ?? \"\"}:${clientSecret}`);\n\n let newAccessToken: string | null = null;\n let newRefreshToken: string | null = null;\n\n for (let attempt = 1; attempt <= MAX_REFRESH_ATTEMPTS; attempt++) {\n try {\n const refreshResponse = await fetch(`${issuer}/auth/token`, {\n method: \"POST\",\n headers: {\n \"content-type\": \"application/x-www-form-urlencoded\",\n authorization: `Basic ${basicAuth}`,\n },\n body: new URLSearchParams({\n grant_type: \"refresh_token\",\n refresh_token: refreshToken,\n }),\n cache: \"no-store\",\n });\n\n if (!refreshResponse.ok) break;\n\n const data = (await refreshResponse.json()) as TokenLikeResponse;\n newAccessToken = data.accessToken ?? data.access_token ?? null;\n newRefreshToken = data.refreshToken ?? data.refresh_token ?? null;\n if (newAccessToken) break;\n } catch {\n if (attempt === MAX_REFRESH_ATTEMPTS) break;\n }\n }\n\n if (!newAccessToken) {\n // Refresh failed — clear the active slot and redirect to sign-in\n const response = NextResponse.redirect(signInUrl);\n const clearOpts = {\n httpOnly: true,\n secure: input.secureCookies ?? process.env.NODE_ENV === \"production\",\n sameSite: \"lax\" as const,\n path: \"/\",\n maxAge: 0,\n };\n response.cookies.set(slotAt(activeIdx), \"\", clearOpts);\n response.cookies.set(slotRt(activeIdx), \"\", clearOpts);\n response.cookies.set(`${prefix}_m${activeIdx}`, \"\", clearOpts);\n response.cookies.set(tokenPubCookie, \"\", { ...clearOpts, httpOnly: false });\n response.cookies.set(activeCookie, \"0\", clearOpts);\n return response;\n }\n\n const cookieOptions = {\n httpOnly: true,\n secure: input.secureCookies ?? process.env.NODE_ENV === \"production\",\n sameSite: \"lax\" as const,\n path: \"/\",\n maxAge: 60 * 60 * 24 * 30,\n };\n\n // Forward new token via request header so server actions in the same\n // request can read it — response cookies are not visible within the same request.\n const requestHeaders = new Headers(request.headers);\n requestHeaders.set(\"x-gk-access-token\", newAccessToken);\n\n const response = NextResponse.next({ request: { headers: requestHeaders } });\n response.cookies.set(slotAt(activeIdx), newAccessToken, cookieOptions);\n if (newRefreshToken) response.cookies.set(slotRt(activeIdx), newRefreshToken, cookieOptions);\n // Clear legacy _tp cookie to reduce header size\n if (request.cookies.get(tokenPubCookie)?.value) {\n response.cookies.set(tokenPubCookie, \"\", { httpOnly: false, maxAge: 0, path: \"/\" });\n }\n\n return response;\n };\n}\n"],"mappings":"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","names":["response"]}
|
|
1
|
+
{"version":3,"sources":["../src/middleware.ts"],"sourcesContent":["import { NextResponse, type NextRequest } from \"next/server\";\nimport { createRemoteJWKSet, jwtVerify } from \"jose\";\n\nexport type AuthMiddlewareConfig = {\n issuer?: string;\n basePath?: string;\n /**\n * @deprecated cookiePrefix is now auto-derived from GEEKAPPS_AUTH_CLIENT_ID.\n * This field is kept for backwards compatibility but is ignored.\n */\n cookiePrefix?: string;\n secureCookies?: boolean;\n /** Base URL of this app (e.g. \"https://meuapp.com\"). Overrides the GEEKAPPS_APP_URL env var. */\n appUrl?: string;\n};\n\ntype TokenLikeResponse = {\n access_token?: string;\n accessToken?: string;\n refresh_token?: string;\n refreshToken?: string;\n id_token?: string;\n idToken?: string;\n};\n\nfunction env(name: string) {\n const value = process.env[name];\n return value && value.trim().length > 0 ? value : undefined;\n}\n\nfunction normalizeBaseUrl(url: string) {\n return url.replace(/\\/$/, \"\");\n}\n\nfunction normalizeBasePath(path = \"/api/auth\") {\n if (!path.startsWith(\"/\")) return `/${path}`;\n return path.replace(/\\/$/, \"\");\n}\n\nfunction resolveIssuer(input: AuthMiddlewareConfig) {\n return normalizeBaseUrl(\n input.issuer ??\n env(\"GEEKAPPS_AUTH_ISSUER\") ??\n env(\"GEEKAPPS_AUTH_API_URL\") ??\n \"http://localhost:3020\",\n );\n}\n\nfunction hashClientId(clientId: string): string {\n let h = 5381;\n for (let i = 0; i < clientId.length; i++) {\n h = ((h << 5) + h) ^ (clientId.charCodeAt(i) & 0xff);\n h = h >>> 0;\n }\n return h.toString(16).padStart(8, \"0\");\n}\n\nfunction deriveCookiePrefix(clientId: string | undefined): string {\n if (!clientId) return \"gk\";\n return `gk_${hashClientId(clientId)}`;\n}\n\n\nconst MAX_REFRESH_ATTEMPTS = 3;\n\nfunction decodeJwtExp(token: string): number | null {\n try {\n const parts = token.split(\".\");\n if (parts.length !== 3) return null;\n const payload = parts[1]!;\n const padded = payload + \"=\".repeat((4 - (payload.length % 4)) % 4);\n const decoded = JSON.parse(atob(padded)) as { exp?: number };\n return typeof decoded.exp === \"number\" ? decoded.exp : null;\n } catch {\n return null;\n }\n}\n\nfunction resolveAppOrigin(request: NextRequest, appUrl?: string): string {\n const configured =\n appUrl ??\n env(\"GEEKAPPS_APP_URL\");\n if (configured) return configured.replace(/\\/$/, \"\");\n\n throw new Error(\n \"Cannot determine app origin for redirect. Set GEEKAPPS_APP_URL.\",\n );\n}\n\nconst _jwksCache = new Map<string, ReturnType<typeof createRemoteJWKSet>>();\n\nfunction getRemoteJWKS(issuer: string) {\n if (!_jwksCache.has(issuer)) {\n _jwksCache.set(issuer, createRemoteJWKSet(new URL(`${issuer}/.well-known/jwks.json`)));\n }\n return _jwksCache.get(issuer)!;\n}\n\nasync function verifyAccessToken(token: string, issuer: string): Promise<boolean> {\n try {\n await jwtVerify(token, getRemoteJWKS(issuer), { algorithms: [\"RS256\"], issuer });\n return true;\n } catch {\n return false;\n }\n}\n\nexport function createAuthMiddleware(input: AuthMiddlewareConfig = {}) {\n const issuer = resolveIssuer(input);\n const basePath = normalizeBasePath(input.basePath);\n const clientId = env(\"GEEKAPPS_AUTH_CLIENT_ID\");\n const prefix = deriveCookiePrefix(clientId);\n\n const activeCookie = `${prefix}_active`;\n const countCookie = `${prefix}_count`;\n const tokenPubCookie = `${prefix}_tp`; // legacy — cleared on next refresh\n const slotAt = (n: number) => `${prefix}_at${n}`;\n const slotRt = (n: number) => `${prefix}_rt${n}`;\n\n // RSC/prefetch requests are cross-origin fetches — a 302 redirect triggers CORS.\n // Return a redirect response that Next.js understands instead.\n function redirectResponse(request: NextRequest, url: URL): NextResponse {\n const isRsc =\n request.headers.has(\"rsc\") ||\n request.headers.has(\"next-router-prefetch\") ||\n request.nextUrl.searchParams.has(\"_rsc\");\n if (isRsc) {\n return new NextResponse(null, {\n status: 302,\n headers: {\n location: url.toString(),\n \"x-action-redirect\": url.toString(),\n vary: \"RSC, Next-Router-State-Tree, Next-Router-Prefetch\",\n },\n }) as NextResponse;\n }\n return NextResponse.redirect(url);\n }\n\n return async function authMiddleware(request: NextRequest): Promise<NextResponse> {\n const countRaw = request.cookies.get(countCookie)?.value;\n const count = countRaw ? Number.parseInt(countRaw, 10) : 0;\n\n const activeRaw = request.cookies.get(activeCookie)?.value;\n let activeIdx = activeRaw ? Number.parseInt(activeRaw, 10) : 1;\n if (Number.isNaN(activeIdx) || activeIdx < 1 || activeIdx > count) {\n activeIdx = count > 0 ? 1 : 0;\n }\n\n // Fallback: if count cookie is missing/zero but slot 1 tokens exist, use slot 1.\n // This handles the case where the count/active control cookies expired before the token cookies.\n if (activeIdx === 0) {\n const fallbackAt = request.cookies.get(slotAt(1))?.value;\n const fallbackRt = request.cookies.get(slotRt(1))?.value;\n if (fallbackAt || fallbackRt) activeIdx = 1;\n }\n\n const accessToken = activeIdx > 0 ? (request.cookies.get(slotAt(activeIdx))?.value ?? null) : null;\n const refreshToken = activeIdx > 0 ? (request.cookies.get(slotRt(activeIdx))?.value ?? null) : null;\n\n const origin = resolveAppOrigin(request, input.appUrl);\n const signInUrl = new URL(`${origin}${basePath}/sign-in`);\n const redirectTo = request.nextUrl.pathname + request.nextUrl.search;\n if (redirectTo && redirectTo !== \"/\") {\n signInUrl.searchParams.set(\"redirectTo\", redirectTo);\n }\n\n if (!accessToken && !refreshToken) {\n return redirectResponse(request, signInUrl);\n }\n\n // Fast-path: check exp before hitting JWKS (avoids network on every request)\n const exp = accessToken ? decodeJwtExp(accessToken) : null;\n const nowSec = Math.floor(Date.now() / 1000);\n const expiredOrMissing = !exp || exp - nowSec < 60;\n\n if (!expiredOrMissing) {\n // Verify signature against app-api JWKS\n const valid = await verifyAccessToken(accessToken!, issuer);\n if (valid) return NextResponse.next();\n }\n\n if (!refreshToken) {\n return redirectResponse(request, signInUrl);\n }\n\n const clientSecret = env(\"GEEKAPPS_AUTH_CLIENT_SECRET\") ?? \"\";\n const basicAuth = btoa(`${clientId ?? \"\"}:${clientSecret}`);\n\n let newAccessToken: string | null = null;\n let newRefreshToken: string | null = null;\n\n for (let attempt = 1; attempt <= MAX_REFRESH_ATTEMPTS; attempt++) {\n try {\n const refreshResponse = await fetch(`${issuer}/auth/token`, {\n method: \"POST\",\n headers: {\n \"content-type\": \"application/x-www-form-urlencoded\",\n authorization: `Basic ${basicAuth}`,\n },\n body: new URLSearchParams({\n grant_type: \"refresh_token\",\n refresh_token: refreshToken,\n }),\n cache: \"no-store\",\n });\n\n if (!refreshResponse.ok) break;\n\n const data = (await refreshResponse.json()) as TokenLikeResponse;\n newAccessToken = data.accessToken ?? data.access_token ?? null;\n newRefreshToken = data.refreshToken ?? data.refresh_token ?? null;\n if (newAccessToken) break;\n } catch {\n if (attempt === MAX_REFRESH_ATTEMPTS) break;\n }\n }\n\n if (!newAccessToken) {\n // Refresh failed — clear the active slot and redirect to sign-in\n const response = redirectResponse(request, signInUrl);\n const clearOpts = {\n httpOnly: true,\n secure: input.secureCookies ?? process.env.NODE_ENV === \"production\",\n sameSite: \"lax\" as const,\n path: \"/\",\n maxAge: 0,\n };\n response.cookies.set(slotAt(activeIdx), \"\", clearOpts);\n response.cookies.set(slotRt(activeIdx), \"\", clearOpts);\n response.cookies.set(`${prefix}_m${activeIdx}`, \"\", clearOpts);\n response.cookies.set(tokenPubCookie, \"\", { ...clearOpts, httpOnly: false });\n response.cookies.set(activeCookie, \"0\", clearOpts);\n return response;\n }\n\n const cookieOptions = {\n httpOnly: true,\n secure: input.secureCookies ?? process.env.NODE_ENV === \"production\",\n sameSite: \"lax\" as const,\n path: \"/\",\n maxAge: 60 * 60 * 24 * 30,\n };\n\n // Forward new token via request header so server actions in the same\n // request can read it — response cookies are not visible within the same request.\n const requestHeaders = new Headers(request.headers);\n requestHeaders.set(\"x-gk-access-token\", newAccessToken);\n\n const response = NextResponse.next({ request: { headers: requestHeaders } });\n response.cookies.set(slotAt(activeIdx), newAccessToken, cookieOptions);\n if (newRefreshToken) response.cookies.set(slotRt(activeIdx), newRefreshToken, cookieOptions);\n // Clear legacy _tp cookie to reduce header size\n if (request.cookies.get(tokenPubCookie)?.value) {\n response.cookies.set(tokenPubCookie, \"\", { httpOnly: false, maxAge: 0, path: \"/\" });\n }\n\n return response;\n };\n}\n"],"mappings":"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","names":["response"]}
|