@lemondouble/lemon-auth 0.5.0 → 0.7.1
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 +180 -31
- package/dist/chunk-6W24RWSJ.js +16 -0
- package/dist/chunk-6W24RWSJ.js.map +1 -0
- package/dist/chunk-OX5MDVSR.js +120 -0
- package/dist/chunk-OX5MDVSR.js.map +1 -0
- package/dist/client/index.d.ts +2 -2
- package/dist/client/index.js +89 -11
- package/dist/client/index.js.map +1 -1
- package/dist/proxy/index.d.ts +15 -4
- package/dist/proxy/index.js +157 -30
- package/dist/proxy/index.js.map +1 -1
- package/dist/server/index.d.ts +14 -5
- package/dist/server/index.js +53 -11
- package/dist/server/index.js.map +1 -1
- package/dist/{types-CgrEUXwM.d.ts → types-B5uRY2Si.d.ts} +11 -1
- package/package.json +9 -7
- package/dist/chunk-3ARMB6QF.js +0 -23
- package/dist/chunk-3ARMB6QF.js.map +0 -1
package/dist/client/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/client/context.tsx","../../src/client/auto-token-refresh.tsx","../../src/client/url.ts"],"sourcesContent":["\"use client\";\n\nimport { createContext, useContext } from \"react\";\nimport type { LemonUser } from \"../types.js\";\n\ninterface AuthContextValue {\n user: LemonUser | null;\n isAuthenticated: boolean;\n}\n\nconst AuthContext = createContext<AuthContextValue>({\n user: null,\n isAuthenticated: false,\n});\n\nexport function AuthProvider({\n user,\n children,\n}: {\n user: LemonUser | null;\n children: React.ReactNode;\n}) {\n return (\n <AuthContext value={{ user, isAuthenticated: user !== null }}>\n {children}\n </AuthContext>\n );\n}\n\nexport function useAuth(): AuthContextValue {\n return useContext(AuthContext);\n}\n","\"use client\";\n\nimport { useEffect, useState } from \"react\";\nimport { useRouter } from \"next/navigation\";\nimport { useAuth } from \"./context.js\";\nimport { refreshToken } from \"./url.js\";\n\nexport function AutoTokenRefresh({\n children,\n fallback,\n}: {\n children?: React.ReactNode;\n fallback: React.ReactNode;\n}) {\n const { isAuthenticated } = useAuth();\n const router = useRouter();\n const [status, setStatus] = useState<\"loading\" | \"failed\">(\"loading\");\n\n useEffect(() => {\n if (isAuthenticated) return;\n\n let ignore = false;\n\n refreshToken().then((ok) => {\n if (ignore) return;\n if (ok) {\n router.refresh();\n } else {\n setStatus(\"failed\");\n }\n });\n\n return () => {\n ignore = true;\n };\n }, [isAuthenticated, router]);\n\n if (isAuthenticated) return null;\n if (status === \"loading\") return fallback ?? null;\n return children ?? null;\n}\n","import {\n LOGIN_URL,\n LOGOUT_URL,\n PROFILE_PAGE_URL,\n REFRESH_URL,\n} from \"../constants.js\";\n\nexport function loginUrl(redirectUrl: string): string {\n return `${LOGIN_URL}?redirect_url=${encodeURIComponent(redirectUrl)}`;\n}\n\nexport function profileUrl(redirectUrl?: string): string {\n if (redirectUrl) {\n return `${PROFILE_PAGE_URL}?redirect_url=${encodeURIComponent(redirectUrl)}`;\n }\n return PROFILE_PAGE_URL;\n}\n\nexport async function logout(): Promise<
|
|
1
|
+
{"version":3,"sources":["../../src/client/context.tsx","../../src/client/auto-token-refresh.tsx","../../src/client/url.ts"],"sourcesContent":["\"use client\";\n\nimport { createContext, useContext } from \"react\";\nimport type { LemonUser } from \"../types.js\";\n\ninterface AuthContextValue {\n user: LemonUser | null;\n isAuthenticated: boolean;\n}\n\nconst AuthContext = createContext<AuthContextValue>({\n user: null,\n isAuthenticated: false,\n});\n\nexport function AuthProvider({\n user,\n children,\n}: {\n user: LemonUser | null;\n children: React.ReactNode;\n}) {\n return (\n <AuthContext value={{ user, isAuthenticated: user !== null }}>\n {children}\n </AuthContext>\n );\n}\n\nexport function useAuth(): AuthContextValue {\n return useContext(AuthContext);\n}\n","\"use client\";\n\nimport { useEffect, useState } from \"react\";\nimport { useRouter } from \"next/navigation\";\nimport { useAuth } from \"./context.js\";\nimport { refreshToken } from \"./url.js\";\n\nexport function AutoTokenRefresh({\n children,\n fallback,\n}: {\n children?: React.ReactNode;\n fallback: React.ReactNode;\n}) {\n const { isAuthenticated } = useAuth();\n const router = useRouter();\n const [status, setStatus] = useState<\"loading\" | \"failed\">(\"loading\");\n\n useEffect(() => {\n if (isAuthenticated) return;\n\n let ignore = false;\n\n refreshToken().then((ok) => {\n if (ignore) return;\n if (ok) {\n router.refresh();\n } else {\n setStatus(\"failed\");\n }\n });\n\n return () => {\n ignore = true;\n };\n }, [isAuthenticated, router]);\n\n if (isAuthenticated) return null;\n if (status === \"loading\") return fallback ?? null;\n return children ?? null;\n}\n","import {\n ACCESS_TOKEN_COOKIE,\n DEVICE_ID_COOKIE,\n LOGIN_URL,\n LOGOUT_URL,\n PROFILE_PAGE_URL,\n REFRESH_URL,\n REFRESH_TOKEN_COOKIE,\n} from \"../constants.js\";\nimport { isMockAuthEnabled } from \"../mock.js\";\n\nconst LOGOUT_COOKIE_NAMES = [\n ACCESS_TOKEN_COOKIE,\n REFRESH_TOKEN_COOKIE,\n DEVICE_ID_COOKIE,\n];\n\nexport function loginUrl(redirectUrl: string): string {\n if (isMockAuthEnabled()) return redirectUrl;\n assertValidLoginRedirectUrl(redirectUrl);\n return `${LOGIN_URL}?redirect_url=${encodeURIComponent(redirectUrl)}`;\n}\n\nexport function profileUrl(redirectUrl?: string): string {\n if (isMockAuthEnabled()) return redirectUrl ?? \"/\";\n if (redirectUrl) {\n return `${PROFILE_PAGE_URL}?redirect_url=${encodeURIComponent(redirectUrl)}`;\n }\n return PROFILE_PAGE_URL;\n}\n\nexport async function logout(): Promise<boolean> {\n if (isMockAuthEnabled()) {\n expireAuthCookies();\n return true;\n }\n\n try {\n const res = await fetch(LOGOUT_URL, {\n method: \"POST\",\n credentials: \"include\",\n });\n return res.ok;\n } catch {\n return false;\n } finally {\n expireAuthCookies();\n }\n}\n\nexport async function refreshToken(): Promise<boolean> {\n if (isMockAuthEnabled()) return true;\n\n try {\n const res = await fetch(REFRESH_URL, {\n method: \"POST\",\n credentials: \"include\",\n });\n return res.ok;\n } catch {\n return false;\n }\n}\n\nfunction expireAuthCookies(): void {\n if (typeof document === \"undefined\") return;\n\n for (const name of LOGOUT_COOKIE_NAMES) {\n for (const domain of getCookieDomains()) {\n document.cookie = [\n `${name}=`,\n \"Max-Age=0\",\n \"Path=/\",\n \"SameSite=Lax\",\n domain ? `Domain=${domain}` : \"\",\n ]\n .filter(Boolean)\n .join(\"; \");\n }\n }\n}\n\nfunction getCookieDomains(): string[] {\n if (typeof window === \"undefined\") return [\"\"];\n\n const { hostname } = window.location;\n if (!hostname || hostname === \"localhost\" || isIpAddress(hostname)) {\n return [\"\"];\n }\n\n const labels = hostname.split(\".\");\n const domains = [\"\"];\n for (let i = 0; i < labels.length - 1; i += 1) {\n domains.push(`.${labels.slice(i).join(\".\")}`);\n }\n return domains;\n}\n\nfunction isIpAddress(hostname: string): boolean {\n return /^[\\d.]+$/.test(hostname) || hostname.includes(\":\");\n}\n\nfunction assertValidLoginRedirectUrl(redirectUrl: string): void {\n let url: URL;\n try {\n url = new URL(redirectUrl);\n } catch {\n throw new Error(\"loginUrl redirectUrl must be a valid absolute URL\");\n }\n\n if (url.protocol !== \"https:\") {\n throw new Error(\"loginUrl redirectUrl must use https\");\n }\n\n if (!isAllowedLoginRedirectHost(url.hostname)) {\n throw new Error(\n \"loginUrl redirectUrl host must be lemondouble.com or a subdomain of lemondouble.com\"\n );\n }\n}\n\nfunction isAllowedLoginRedirectHost(hostname: string): boolean {\n const normalized = hostname.toLowerCase();\n return (\n normalized === \"lemondouble.com\" ||\n normalized.endsWith(\".lemondouble.com\")\n );\n}\n"],"mappings":"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","names":[]}
|
package/dist/proxy/index.d.ts
CHANGED
|
@@ -1,14 +1,25 @@
|
|
|
1
1
|
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
-
import { A as AccessTokenClaims } from '../types-
|
|
3
|
-
export {
|
|
2
|
+
import { A as AccessTokenClaims } from '../types-B5uRY2Si.js';
|
|
3
|
+
export { a as LemonUser } from '../types-B5uRY2Si.js';
|
|
4
4
|
import 'jose';
|
|
5
5
|
|
|
6
|
+
declare const DEFAULT_AUTH_BYPASS_PATHS: string[];
|
|
7
|
+
declare const DEFAULT_API_PATHS: string[];
|
|
8
|
+
declare const PROXY_AUTH_ERROR: {
|
|
9
|
+
readonly UNAUTHORIZED: "UNAUTHORIZED";
|
|
10
|
+
readonly FORBIDDEN: "FORBIDDEN";
|
|
11
|
+
};
|
|
12
|
+
type ProxyAuthErrorCode = (typeof PROXY_AUTH_ERROR)[keyof typeof PROXY_AUTH_ERROR];
|
|
13
|
+
type LoginRedirectUrl = string | ((request: NextRequest) => string);
|
|
6
14
|
interface AuthProxyOptions {
|
|
7
15
|
publicPaths?: string[];
|
|
16
|
+
bypassPaths?: string[];
|
|
17
|
+
apiPaths?: string[];
|
|
8
18
|
clientId?: string;
|
|
9
|
-
loginRedirectUrl?:
|
|
19
|
+
loginRedirectUrl?: LoginRedirectUrl;
|
|
20
|
+
unapprovedRedirectUrl?: LoginRedirectUrl;
|
|
10
21
|
onAuthSuccess?: (claims: AccessTokenClaims, request: NextRequest, response: NextResponse) => Promise<NextResponse> | NextResponse;
|
|
11
22
|
}
|
|
12
23
|
declare function createAuthProxy(options?: AuthProxyOptions): (request: NextRequest) => Promise<NextResponse>;
|
|
13
24
|
|
|
14
|
-
export { AccessTokenClaims, type AuthProxyOptions, createAuthProxy };
|
|
25
|
+
export { AccessTokenClaims, type AuthProxyOptions, DEFAULT_API_PATHS, DEFAULT_AUTH_BYPASS_PATHS, type LoginRedirectUrl, PROXY_AUTH_ERROR, type ProxyAuthErrorCode, createAuthProxy };
|
package/dist/proxy/index.js
CHANGED
|
@@ -1,73 +1,173 @@
|
|
|
1
|
+
import {
|
|
2
|
+
isAccessTokenClaims,
|
|
3
|
+
isApprovedClient
|
|
4
|
+
} from "../chunk-6W24RWSJ.js";
|
|
1
5
|
import {
|
|
2
6
|
ACCESS_TOKEN_COOKIE,
|
|
3
7
|
AUTH_SERVER_URL,
|
|
8
|
+
DEVICE_ID_COOKIE,
|
|
4
9
|
JWKS_URL,
|
|
5
10
|
LOGIN_URL,
|
|
6
11
|
REFRESH_TOKEN_COOKIE,
|
|
7
|
-
REFRESH_URL
|
|
8
|
-
|
|
12
|
+
REFRESH_URL,
|
|
13
|
+
getMockAccessTokenClaims
|
|
14
|
+
} from "../chunk-OX5MDVSR.js";
|
|
9
15
|
|
|
10
16
|
// src/proxy/index.ts
|
|
11
17
|
import { NextResponse } from "next/server";
|
|
12
18
|
import { createRemoteJWKSet, jwtVerify } from "jose";
|
|
13
19
|
var REFRESH_BUFFER_SECONDS = 60;
|
|
20
|
+
var DEFAULT_AUTH_BYPASS_PATHS = [
|
|
21
|
+
"/sw.js",
|
|
22
|
+
"/service-worker.js",
|
|
23
|
+
"/manifest.webmanifest",
|
|
24
|
+
"/manifest.json",
|
|
25
|
+
"/favicon.ico",
|
|
26
|
+
"/robots.txt",
|
|
27
|
+
"/sitemap.xml",
|
|
28
|
+
"/browserconfig.xml",
|
|
29
|
+
"/apple-touch-icon.png",
|
|
30
|
+
"/apple-touch-icon-*",
|
|
31
|
+
"/icon-*",
|
|
32
|
+
"/icons/*"
|
|
33
|
+
];
|
|
34
|
+
var DEFAULT_API_PATHS = ["/api/*"];
|
|
35
|
+
var PROXY_AUTH_ERROR = {
|
|
36
|
+
UNAUTHORIZED: "UNAUTHORIZED",
|
|
37
|
+
FORBIDDEN: "FORBIDDEN"
|
|
38
|
+
};
|
|
14
39
|
var jwks = createRemoteJWKSet(new URL(JWKS_URL));
|
|
15
40
|
function createAuthProxy(options = {}) {
|
|
16
|
-
const {
|
|
41
|
+
const {
|
|
42
|
+
publicPaths = [],
|
|
43
|
+
bypassPaths = [],
|
|
44
|
+
apiPaths = DEFAULT_API_PATHS,
|
|
45
|
+
clientId,
|
|
46
|
+
loginRedirectUrl,
|
|
47
|
+
unapprovedRedirectUrl,
|
|
48
|
+
onAuthSuccess
|
|
49
|
+
} = options;
|
|
50
|
+
const effectiveBypassPaths = [
|
|
51
|
+
...DEFAULT_AUTH_BYPASS_PATHS,
|
|
52
|
+
...bypassPaths
|
|
53
|
+
];
|
|
17
54
|
return async function proxy(request) {
|
|
18
55
|
const { pathname } = request.nextUrl;
|
|
56
|
+
if (isPublicPath(pathname, effectiveBypassPaths)) {
|
|
57
|
+
return NextResponse.next();
|
|
58
|
+
}
|
|
19
59
|
if (isPublicPath(pathname, publicPaths)) {
|
|
20
60
|
return maybeRefreshAndContinue(request);
|
|
21
61
|
}
|
|
62
|
+
const isApiRequest = isPublicPath(pathname, apiPaths);
|
|
63
|
+
const mockClaims = getMockAccessTokenClaims();
|
|
64
|
+
if (mockClaims) {
|
|
65
|
+
return handleVerifiedClaims(
|
|
66
|
+
mockClaims,
|
|
67
|
+
request,
|
|
68
|
+
isApiRequest,
|
|
69
|
+
clientId,
|
|
70
|
+
unapprovedRedirectUrl,
|
|
71
|
+
onAuthSuccess,
|
|
72
|
+
NextResponse.next()
|
|
73
|
+
);
|
|
74
|
+
}
|
|
22
75
|
const accessToken = request.cookies.get(ACCESS_TOKEN_COOKIE)?.value;
|
|
23
76
|
const refreshToken = request.cookies.get(REFRESH_TOKEN_COOKIE)?.value;
|
|
77
|
+
const deviceId = request.cookies.get(DEVICE_ID_COOKIE)?.value;
|
|
24
78
|
if (accessToken && isTokenFresh(accessToken)) {
|
|
25
79
|
const claims = await verifyToken(accessToken);
|
|
26
80
|
if (claims) {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
81
|
+
return handleVerifiedClaims(
|
|
82
|
+
claims,
|
|
83
|
+
request,
|
|
84
|
+
isApiRequest,
|
|
85
|
+
clientId,
|
|
86
|
+
unapprovedRedirectUrl,
|
|
87
|
+
onAuthSuccess,
|
|
88
|
+
NextResponse.next()
|
|
89
|
+
);
|
|
32
90
|
}
|
|
33
91
|
}
|
|
34
92
|
if (refreshToken) {
|
|
35
|
-
const result = await tryRefresh(refreshToken);
|
|
93
|
+
const result = await tryRefresh(refreshToken, deviceId);
|
|
36
94
|
if (result) {
|
|
37
95
|
const claims = await verifyToken(result.newAccessToken);
|
|
38
96
|
if (claims) {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
97
|
+
const response = nextWithRefreshedCookies(
|
|
98
|
+
request,
|
|
99
|
+
result.setCookieHeaders
|
|
100
|
+
);
|
|
101
|
+
return handleVerifiedClaims(
|
|
102
|
+
claims,
|
|
103
|
+
request,
|
|
104
|
+
isApiRequest,
|
|
105
|
+
clientId,
|
|
106
|
+
unapprovedRedirectUrl,
|
|
107
|
+
onAuthSuccess,
|
|
108
|
+
response
|
|
109
|
+
);
|
|
47
110
|
}
|
|
48
111
|
}
|
|
49
112
|
}
|
|
50
|
-
return redirectToLogin(request, loginRedirectUrl);
|
|
113
|
+
return isApiRequest ? unauthorizedJson() : redirectToLogin(request, loginRedirectUrl);
|
|
51
114
|
};
|
|
52
115
|
}
|
|
116
|
+
async function handleVerifiedClaims(claims, request, isApiRequest, clientId, unapprovedRedirectUrl, onAuthSuccess, response) {
|
|
117
|
+
if (!isApprovedClient(claims.approved_clients, clientId)) {
|
|
118
|
+
return isApiRequest ? forbiddenJson() : redirectToUnapproved(request, unapprovedRedirectUrl);
|
|
119
|
+
}
|
|
120
|
+
return onAuthSuccess ? await onAuthSuccess(claims, request, response) : response;
|
|
121
|
+
}
|
|
122
|
+
function unauthorizedJson() {
|
|
123
|
+
return NextResponse.json(
|
|
124
|
+
{ code: PROXY_AUTH_ERROR.UNAUTHORIZED },
|
|
125
|
+
{ status: 401 }
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
function forbiddenJson() {
|
|
129
|
+
return NextResponse.json(
|
|
130
|
+
{ code: PROXY_AUTH_ERROR.FORBIDDEN },
|
|
131
|
+
{ status: 403 }
|
|
132
|
+
);
|
|
133
|
+
}
|
|
53
134
|
async function maybeRefreshAndContinue(request) {
|
|
54
135
|
const accessToken = request.cookies.get(ACCESS_TOKEN_COOKIE)?.value;
|
|
55
136
|
const refreshToken = request.cookies.get(REFRESH_TOKEN_COOKIE)?.value;
|
|
137
|
+
const deviceId = request.cookies.get(DEVICE_ID_COOKIE)?.value;
|
|
56
138
|
if (accessToken && isTokenFresh(accessToken)) {
|
|
57
139
|
return NextResponse.next();
|
|
58
140
|
}
|
|
59
141
|
if (refreshToken) {
|
|
60
|
-
const result = await tryRefresh(refreshToken);
|
|
142
|
+
const result = await tryRefresh(refreshToken, deviceId);
|
|
61
143
|
if (result) {
|
|
62
|
-
|
|
63
|
-
for (const header of result.setCookieHeaders) {
|
|
64
|
-
response.headers.append("Set-Cookie", header);
|
|
65
|
-
}
|
|
66
|
-
return response;
|
|
144
|
+
return nextWithRefreshedCookies(request, result.setCookieHeaders);
|
|
67
145
|
}
|
|
68
146
|
}
|
|
69
147
|
return NextResponse.next();
|
|
70
148
|
}
|
|
149
|
+
function nextWithRefreshedCookies(request, setCookieHeaders) {
|
|
150
|
+
const merged = /* @__PURE__ */ new Map();
|
|
151
|
+
for (const { name, value } of request.cookies.getAll()) {
|
|
152
|
+
merged.set(name, value);
|
|
153
|
+
}
|
|
154
|
+
for (const header of setCookieHeaders) {
|
|
155
|
+
const pair = header.split(";")[0] ?? "";
|
|
156
|
+
const separator = pair.indexOf("=");
|
|
157
|
+
if (separator <= 0) continue;
|
|
158
|
+
merged.set(pair.slice(0, separator).trim(), pair.slice(separator + 1).trim());
|
|
159
|
+
}
|
|
160
|
+
const requestHeaders = new Headers(request.headers);
|
|
161
|
+
requestHeaders.set(
|
|
162
|
+
"cookie",
|
|
163
|
+
Array.from(merged, ([name, value]) => `${name}=${value}`).join("; ")
|
|
164
|
+
);
|
|
165
|
+
const response = NextResponse.next({ request: { headers: requestHeaders } });
|
|
166
|
+
for (const header of setCookieHeaders) {
|
|
167
|
+
response.headers.append("Set-Cookie", header);
|
|
168
|
+
}
|
|
169
|
+
return response;
|
|
170
|
+
}
|
|
71
171
|
function isTokenFresh(token) {
|
|
72
172
|
try {
|
|
73
173
|
const parts = token.split(".");
|
|
@@ -85,16 +185,16 @@ async function verifyToken(token) {
|
|
|
85
185
|
const { payload } = await jwtVerify(token, jwks, {
|
|
86
186
|
issuer: AUTH_SERVER_URL
|
|
87
187
|
});
|
|
88
|
-
return payload;
|
|
188
|
+
return isAccessTokenClaims(payload) ? payload : null;
|
|
89
189
|
} catch {
|
|
90
190
|
return null;
|
|
91
191
|
}
|
|
92
192
|
}
|
|
93
|
-
async function tryRefresh(refreshToken) {
|
|
193
|
+
async function tryRefresh(refreshToken, deviceId) {
|
|
94
194
|
try {
|
|
95
195
|
const res = await fetch(REFRESH_URL, {
|
|
96
196
|
method: "POST",
|
|
97
|
-
headers: { Cookie:
|
|
197
|
+
headers: { Cookie: buildRefreshCookieHeader(refreshToken, deviceId) }
|
|
98
198
|
});
|
|
99
199
|
if (!res.ok) return null;
|
|
100
200
|
const setCookieHeaders = res.headers.getSetCookie();
|
|
@@ -105,9 +205,16 @@ async function tryRefresh(refreshToken) {
|
|
|
105
205
|
return null;
|
|
106
206
|
}
|
|
107
207
|
}
|
|
208
|
+
function buildRefreshCookieHeader(refreshToken, deviceId) {
|
|
209
|
+
const cookies = [`${REFRESH_TOKEN_COOKIE}=${refreshToken}`];
|
|
210
|
+
if (deviceId) {
|
|
211
|
+
cookies.push(`${DEVICE_ID_COOKIE}=${deviceId}`);
|
|
212
|
+
}
|
|
213
|
+
return cookies.join("; ");
|
|
214
|
+
}
|
|
108
215
|
function extractAccessToken(setCookieHeaders) {
|
|
109
216
|
for (const header of setCookieHeaders) {
|
|
110
|
-
const match = header.match(
|
|
217
|
+
const match = header.match(new RegExp(`^${ACCESS_TOKEN_COOKIE}=([^;]+)`));
|
|
111
218
|
if (match) return match[1];
|
|
112
219
|
}
|
|
113
220
|
return null;
|
|
@@ -121,13 +228,33 @@ function isPublicPath(pathname, publicPaths) {
|
|
|
121
228
|
});
|
|
122
229
|
}
|
|
123
230
|
function redirectToLogin(request, loginRedirectUrl) {
|
|
124
|
-
|
|
125
|
-
|
|
231
|
+
const redirectUrl = resolveLoginRedirectUrl(request, loginRedirectUrl);
|
|
232
|
+
if (redirectUrl) {
|
|
233
|
+
const url = `${LOGIN_URL}?redirect_url=${encodeURIComponent(redirectUrl)}`;
|
|
126
234
|
return NextResponse.redirect(new URL(url, request.url));
|
|
127
235
|
}
|
|
128
236
|
return NextResponse.redirect(new URL("/", request.url));
|
|
129
237
|
}
|
|
238
|
+
function redirectToUnapproved(request, unapprovedRedirectUrl) {
|
|
239
|
+
const redirectUrl = resolveLoginRedirectUrl(request, unapprovedRedirectUrl);
|
|
240
|
+
if (redirectUrl) {
|
|
241
|
+
return NextResponse.redirect(new URL(redirectUrl, request.url));
|
|
242
|
+
}
|
|
243
|
+
const errorUrl = new URL(`${AUTH_SERVER_URL}/error`);
|
|
244
|
+
errorUrl.searchParams.set("code", PROXY_AUTH_ERROR.FORBIDDEN);
|
|
245
|
+
errorUrl.searchParams.set("from", request.url);
|
|
246
|
+
return NextResponse.redirect(errorUrl);
|
|
247
|
+
}
|
|
248
|
+
function resolveLoginRedirectUrl(request, loginRedirectUrl) {
|
|
249
|
+
if (typeof loginRedirectUrl === "function") {
|
|
250
|
+
return loginRedirectUrl(request);
|
|
251
|
+
}
|
|
252
|
+
return loginRedirectUrl;
|
|
253
|
+
}
|
|
130
254
|
export {
|
|
255
|
+
DEFAULT_API_PATHS,
|
|
256
|
+
DEFAULT_AUTH_BYPASS_PATHS,
|
|
257
|
+
PROXY_AUTH_ERROR,
|
|
131
258
|
createAuthProxy
|
|
132
259
|
};
|
|
133
260
|
//# sourceMappingURL=index.js.map
|
package/dist/proxy/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/proxy/index.ts"],"sourcesContent":["import { NextRequest, NextResponse } from \"next/server\";\nimport {\n ACCESS_TOKEN_COOKIE,\n REFRESH_TOKEN_COOKIE,\n AUTH_SERVER_URL,\n REFRESH_URL,\n JWKS_URL,\n LOGIN_URL,\n} from \"../constants.js\";\nimport { createRemoteJWKSet, jwtVerify } from \"jose\";\nimport type { AccessTokenClaims } from \"../types.js\";\n\nconst REFRESH_BUFFER_SECONDS = 60;\n\nconst jwks = createRemoteJWKSet(new URL(JWKS_URL));\n\nexport interface AuthProxyOptions {\n publicPaths?: string[];\n clientId?: string;\n loginRedirectUrl?: string;\n onAuthSuccess?: (\n claims: AccessTokenClaims,\n request: NextRequest,\n response: NextResponse\n ) => Promise<NextResponse> | NextResponse;\n}\n\nexport function createAuthProxy(options: AuthProxyOptions = {}) {\n const { publicPaths = [], clientId, loginRedirectUrl, onAuthSuccess } =\n options;\n\n return async function proxy(request: NextRequest): Promise<NextResponse> {\n const { pathname } = request.nextUrl;\n\n if (isPublicPath(pathname, publicPaths)) {\n return maybeRefreshAndContinue(request);\n }\n\n const accessToken = request.cookies.get(ACCESS_TOKEN_COOKIE)?.value;\n const refreshToken = request.cookies.get(REFRESH_TOKEN_COOKIE)?.value;\n\n if (accessToken && isTokenFresh(accessToken)) {\n const claims = await verifyToken(accessToken);\n if (claims) {\n if (clientId && !claims.approved_clients.includes(clientId)) {\n return redirectToLogin(request, loginRedirectUrl);\n }\n const response = NextResponse.next();\n return onAuthSuccess\n ? await onAuthSuccess(claims, request, response)\n : response;\n }\n }\n\n if (refreshToken) {\n const result = await tryRefresh(refreshToken);\n if (result) {\n const claims = await verifyToken(result.newAccessToken);\n if (claims) {\n if (clientId && !claims.approved_clients.includes(clientId)) {\n return redirectToLogin(request, loginRedirectUrl);\n }\n const response = NextResponse.next();\n for (const header of result.setCookieHeaders) {\n response.headers.append(\"Set-Cookie\", header);\n }\n return onAuthSuccess\n ? await onAuthSuccess(claims, request, response)\n : response;\n }\n }\n }\n\n return redirectToLogin(request, loginRedirectUrl);\n };\n}\n\nasync function maybeRefreshAndContinue(\n request: NextRequest\n): Promise<NextResponse> {\n const accessToken = request.cookies.get(ACCESS_TOKEN_COOKIE)?.value;\n const refreshToken = request.cookies.get(REFRESH_TOKEN_COOKIE)?.value;\n\n if (accessToken && isTokenFresh(accessToken)) {\n return NextResponse.next();\n }\n\n if (refreshToken) {\n const result = await tryRefresh(refreshToken);\n if (result) {\n const response = NextResponse.next();\n for (const header of result.setCookieHeaders) {\n response.headers.append(\"Set-Cookie\", header);\n }\n return response;\n }\n }\n\n return NextResponse.next();\n}\n\nfunction isTokenFresh(token: string): boolean {\n try {\n const parts = token.split(\".\");\n if (parts.length !== 3) return false;\n const payload = JSON.parse(\n Buffer.from(parts[1], \"base64url\").toString()\n );\n return (\n typeof payload.exp === \"number\" &&\n payload.exp > Math.floor(Date.now() / 1000) + REFRESH_BUFFER_SECONDS\n );\n } catch {\n return false;\n }\n}\n\nasync function verifyToken(\n token: string\n): Promise<AccessTokenClaims | null> {\n try {\n const { payload } = await jwtVerify(token, jwks, {\n issuer: AUTH_SERVER_URL,\n });\n return payload as unknown as AccessTokenClaims;\n } catch {\n return null;\n }\n}\n\ninterface RefreshSuccess {\n newAccessToken: string;\n setCookieHeaders: string[];\n}\n\nasync function tryRefresh(\n refreshToken: string\n): Promise<RefreshSuccess | null> {\n try {\n const res = await fetch(REFRESH_URL, {\n method: \"POST\",\n headers: { Cookie: `lemon_refresh_token=${refreshToken}` },\n });\n\n if (!res.ok) return null;\n\n const setCookieHeaders = res.headers.getSetCookie();\n const newAccessToken = extractAccessToken(setCookieHeaders);\n if (!newAccessToken) return null;\n\n return { newAccessToken, setCookieHeaders };\n } catch {\n return null;\n }\n}\n\nfunction extractAccessToken(setCookieHeaders: string[]): string | null {\n for (const header of setCookieHeaders) {\n const match = header.match(/^lemon_access_token=([^;]+)/);\n if (match) return match[1];\n }\n return null;\n}\n\nfunction isPublicPath(pathname: string, publicPaths: string[]): boolean {\n return publicPaths.some((p) => {\n if (p.endsWith(\"*\")) {\n return pathname.startsWith(p.slice(0, -1));\n }\n return pathname === p;\n });\n}\n\nfunction redirectToLogin(\n request: NextRequest,\n loginRedirectUrl?: string\n): NextResponse {\n if (loginRedirectUrl) {\n const url = `${LOGIN_URL}?redirect_url=${encodeURIComponent(loginRedirectUrl)}`;\n return NextResponse.redirect(new URL(url, request.url));\n }\n return NextResponse.redirect(new URL(\"/\", request.url));\n}\n\nexport type { AccessTokenClaims } from \"../types.js\";\nexport type { LemonUser } from \"../types.js\";\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/proxy/index.ts"],"sourcesContent":["import { NextRequest, NextResponse } from \"next/server\";\nimport {\n ACCESS_TOKEN_COOKIE,\n REFRESH_TOKEN_COOKIE,\n DEVICE_ID_COOKIE,\n AUTH_SERVER_URL,\n REFRESH_URL,\n JWKS_URL,\n LOGIN_URL,\n} from \"../constants.js\";\nimport { createRemoteJWKSet, jwtVerify } from \"jose\";\nimport type { AccessTokenClaims } from \"../types.js\";\nimport { isAccessTokenClaims } from \"../claims.js\";\nimport { isApprovedClient } from \"../approval.js\";\nimport { getMockAccessTokenClaims } from \"../mock.js\";\n\nconst REFRESH_BUFFER_SECONDS = 60;\n\nexport const DEFAULT_AUTH_BYPASS_PATHS = [\n \"/sw.js\",\n \"/service-worker.js\",\n \"/manifest.webmanifest\",\n \"/manifest.json\",\n \"/favicon.ico\",\n \"/robots.txt\",\n \"/sitemap.xml\",\n \"/browserconfig.xml\",\n \"/apple-touch-icon.png\",\n \"/apple-touch-icon-*\",\n \"/icon-*\",\n \"/icons/*\",\n];\n\nexport const DEFAULT_API_PATHS = [\"/api/*\"];\n\nexport const PROXY_AUTH_ERROR = {\n UNAUTHORIZED: \"UNAUTHORIZED\",\n FORBIDDEN: \"FORBIDDEN\",\n} as const;\nexport type ProxyAuthErrorCode =\n (typeof PROXY_AUTH_ERROR)[keyof typeof PROXY_AUTH_ERROR];\n\nconst jwks = createRemoteJWKSet(new URL(JWKS_URL));\n\nexport type LoginRedirectUrl = string | ((request: NextRequest) => string);\n\nexport interface AuthProxyOptions {\n publicPaths?: string[];\n bypassPaths?: string[];\n apiPaths?: string[];\n clientId?: string;\n loginRedirectUrl?: LoginRedirectUrl;\n unapprovedRedirectUrl?: LoginRedirectUrl;\n onAuthSuccess?: (\n claims: AccessTokenClaims,\n request: NextRequest,\n response: NextResponse\n ) => Promise<NextResponse> | NextResponse;\n}\n\nexport function createAuthProxy(options: AuthProxyOptions = {}) {\n const {\n publicPaths = [],\n bypassPaths = [],\n apiPaths = DEFAULT_API_PATHS,\n clientId,\n loginRedirectUrl,\n unapprovedRedirectUrl,\n onAuthSuccess,\n } = options;\n const effectiveBypassPaths = [\n ...DEFAULT_AUTH_BYPASS_PATHS,\n ...bypassPaths,\n ];\n\n return async function proxy(request: NextRequest): Promise<NextResponse> {\n const { pathname } = request.nextUrl;\n\n if (isPublicPath(pathname, effectiveBypassPaths)) {\n return NextResponse.next();\n }\n\n if (isPublicPath(pathname, publicPaths)) {\n return maybeRefreshAndContinue(request);\n }\n\n const isApiRequest = isPublicPath(pathname, apiPaths);\n const mockClaims = getMockAccessTokenClaims();\n if (mockClaims) {\n return handleVerifiedClaims(\n mockClaims,\n request,\n isApiRequest,\n clientId,\n unapprovedRedirectUrl,\n onAuthSuccess,\n NextResponse.next()\n );\n }\n\n const accessToken = request.cookies.get(ACCESS_TOKEN_COOKIE)?.value;\n const refreshToken = request.cookies.get(REFRESH_TOKEN_COOKIE)?.value;\n const deviceId = request.cookies.get(DEVICE_ID_COOKIE)?.value;\n\n if (accessToken && isTokenFresh(accessToken)) {\n const claims = await verifyToken(accessToken);\n if (claims) {\n return handleVerifiedClaims(\n claims,\n request,\n isApiRequest,\n clientId,\n unapprovedRedirectUrl,\n onAuthSuccess,\n NextResponse.next()\n );\n }\n }\n\n if (refreshToken) {\n const result = await tryRefresh(refreshToken, deviceId);\n if (result) {\n const claims = await verifyToken(result.newAccessToken);\n if (claims) {\n const response = nextWithRefreshedCookies(\n request,\n result.setCookieHeaders\n );\n return handleVerifiedClaims(\n claims,\n request,\n isApiRequest,\n clientId,\n unapprovedRedirectUrl,\n onAuthSuccess,\n response\n );\n }\n }\n }\n\n return isApiRequest\n ? unauthorizedJson()\n : redirectToLogin(request, loginRedirectUrl);\n };\n}\n\nasync function handleVerifiedClaims(\n claims: AccessTokenClaims,\n request: NextRequest,\n isApiRequest: boolean,\n clientId: string | undefined,\n unapprovedRedirectUrl: LoginRedirectUrl | undefined,\n onAuthSuccess: AuthProxyOptions[\"onAuthSuccess\"],\n response: NextResponse\n): Promise<NextResponse> {\n if (!isApprovedClient(claims.approved_clients, clientId)) {\n return isApiRequest\n ? forbiddenJson()\n : redirectToUnapproved(request, unapprovedRedirectUrl);\n }\n\n return onAuthSuccess\n ? await onAuthSuccess(claims, request, response)\n : response;\n}\n\nfunction unauthorizedJson(): NextResponse {\n return NextResponse.json(\n { code: PROXY_AUTH_ERROR.UNAUTHORIZED },\n { status: 401 }\n );\n}\n\nfunction forbiddenJson(): NextResponse {\n return NextResponse.json(\n { code: PROXY_AUTH_ERROR.FORBIDDEN },\n { status: 403 }\n );\n}\n\nasync function maybeRefreshAndContinue(\n request: NextRequest\n): Promise<NextResponse> {\n const accessToken = request.cookies.get(ACCESS_TOKEN_COOKIE)?.value;\n const refreshToken = request.cookies.get(REFRESH_TOKEN_COOKIE)?.value;\n const deviceId = request.cookies.get(DEVICE_ID_COOKIE)?.value;\n\n if (accessToken && isTokenFresh(accessToken)) {\n return NextResponse.next();\n }\n\n if (refreshToken) {\n const result = await tryRefresh(refreshToken, deviceId);\n if (result) {\n return nextWithRefreshedCookies(request, result.setCookieHeaders);\n }\n }\n\n return NextResponse.next();\n}\n\n/**\n * 갱신된 토큰을 이 요청의 cookie 헤더에도 반영한 응답을 만든다.\n *\n * Set-Cookie 는 브라우저에만 적용된다. 그것만 붙이면 지금 이 요청을 처리하는\n * 서버 컴포넌트는 여전히 만료된 access token 을 읽고, 갱신에 성공했는데도\n * 그 요청은 로그아웃으로 판정된다. access token 이 만료된 뒤 첫 요청마다\n * 발생하며, PWA 콜드 스타트처럼 항상 만료 상태에서 시작하는 경우 매번 겪는다.\n *\n * Next.js 는 미들웨어가 넘긴 요청 헤더를 다운스트림에 그대로 전달하므로\n * cookie 헤더를 새 토큰으로 바꿔 끼워 같은 요청에서 인증이 보이게 한다.\n * (response.headers.append 는 NextResponse 의 쿠키 프록시를 우회하기 때문에\n * Next 의 x-middleware-set-cookie 병합 경로도 타지 않는다)\n */\nfunction nextWithRefreshedCookies(\n request: NextRequest,\n setCookieHeaders: string[]\n): NextResponse {\n const merged = new Map<string, string>();\n for (const { name, value } of request.cookies.getAll()) {\n merged.set(name, value);\n }\n\n // auth 서버는 도메인마다 같은 쿠키를 한 번씩 보내므로 이름 기준으로 덮어쓴다.\n for (const header of setCookieHeaders) {\n const pair = header.split(\";\")[0] ?? \"\";\n const separator = pair.indexOf(\"=\");\n if (separator <= 0) continue;\n merged.set(pair.slice(0, separator).trim(), pair.slice(separator + 1).trim());\n }\n\n const requestHeaders = new Headers(request.headers);\n requestHeaders.set(\n \"cookie\",\n Array.from(merged, ([name, value]) => `${name}=${value}`).join(\"; \")\n );\n\n const response = NextResponse.next({ request: { headers: requestHeaders } });\n for (const header of setCookieHeaders) {\n response.headers.append(\"Set-Cookie\", header);\n }\n return response;\n}\n\nfunction isTokenFresh(token: string): boolean {\n try {\n const parts = token.split(\".\");\n if (parts.length !== 3) return false;\n const payload = JSON.parse(\n Buffer.from(parts[1], \"base64url\").toString()\n );\n return (\n typeof payload.exp === \"number\" &&\n payload.exp > Math.floor(Date.now() / 1000) + REFRESH_BUFFER_SECONDS\n );\n } catch {\n return false;\n }\n}\n\nasync function verifyToken(\n token: string\n): Promise<AccessTokenClaims | null> {\n try {\n const { payload } = await jwtVerify(token, jwks, {\n issuer: AUTH_SERVER_URL,\n });\n return isAccessTokenClaims(payload) ? payload : null;\n } catch {\n return null;\n }\n}\n\ninterface RefreshSuccess {\n newAccessToken: string;\n setCookieHeaders: string[];\n}\n\nasync function tryRefresh(\n refreshToken: string,\n deviceId?: string\n): Promise<RefreshSuccess | null> {\n try {\n const res = await fetch(REFRESH_URL, {\n method: \"POST\",\n headers: { Cookie: buildRefreshCookieHeader(refreshToken, deviceId) },\n });\n\n if (!res.ok) return null;\n\n const setCookieHeaders = res.headers.getSetCookie();\n const newAccessToken = extractAccessToken(setCookieHeaders);\n if (!newAccessToken) return null;\n\n return { newAccessToken, setCookieHeaders };\n } catch {\n return null;\n }\n}\n\nfunction buildRefreshCookieHeader(\n refreshToken: string,\n deviceId?: string\n): string {\n const cookies = [`${REFRESH_TOKEN_COOKIE}=${refreshToken}`];\n if (deviceId) {\n cookies.push(`${DEVICE_ID_COOKIE}=${deviceId}`);\n }\n return cookies.join(\"; \");\n}\n\nfunction extractAccessToken(setCookieHeaders: string[]): string | null {\n for (const header of setCookieHeaders) {\n const match = header.match(new RegExp(`^${ACCESS_TOKEN_COOKIE}=([^;]+)`));\n if (match) return match[1];\n }\n return null;\n}\n\nfunction isPublicPath(pathname: string, publicPaths: string[]): boolean {\n return publicPaths.some((p) => {\n if (p.endsWith(\"*\")) {\n return pathname.startsWith(p.slice(0, -1));\n }\n return pathname === p;\n });\n}\n\nfunction redirectToLogin(\n request: NextRequest,\n loginRedirectUrl?: LoginRedirectUrl\n): NextResponse {\n const redirectUrl = resolveLoginRedirectUrl(request, loginRedirectUrl);\n if (redirectUrl) {\n const url = `${LOGIN_URL}?redirect_url=${encodeURIComponent(redirectUrl)}`;\n return NextResponse.redirect(new URL(url, request.url));\n }\n return NextResponse.redirect(new URL(\"/\", request.url));\n}\n\nfunction redirectToUnapproved(\n request: NextRequest,\n unapprovedRedirectUrl?: LoginRedirectUrl\n): NextResponse {\n const redirectUrl = resolveLoginRedirectUrl(request, unapprovedRedirectUrl);\n if (redirectUrl) {\n return NextResponse.redirect(new URL(redirectUrl, request.url));\n }\n const errorUrl = new URL(`${AUTH_SERVER_URL}/error`);\n errorUrl.searchParams.set(\"code\", PROXY_AUTH_ERROR.FORBIDDEN);\n errorUrl.searchParams.set(\"from\", request.url);\n return NextResponse.redirect(errorUrl);\n}\n\nfunction resolveLoginRedirectUrl(\n request: NextRequest,\n loginRedirectUrl?: LoginRedirectUrl\n): string | undefined {\n if (typeof loginRedirectUrl === \"function\") {\n return loginRedirectUrl(request);\n }\n return loginRedirectUrl;\n}\n\nexport type { AccessTokenClaims } from \"../types.js\";\nexport type { LemonUser } from \"../types.js\";\n"],"mappings":"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","names":[]}
|
package/dist/server/index.d.ts
CHANGED
|
@@ -1,19 +1,27 @@
|
|
|
1
|
-
import { A as AccessTokenClaims, L as LemonUser } from '../types-
|
|
2
|
-
export { U as UserProfile } from '../types-
|
|
1
|
+
import { A as AccessTokenClaims, L as LemonSession, a as LemonUser } from '../types-B5uRY2Si.js';
|
|
2
|
+
export { U as UserProfile } from '../types-B5uRY2Si.js';
|
|
3
3
|
import 'jose';
|
|
4
4
|
|
|
5
5
|
declare function verifyAccessToken(): Promise<AccessTokenClaims | null>;
|
|
6
6
|
declare function verifyAccessTokenString(token: string): Promise<AccessTokenClaims | null>;
|
|
7
7
|
|
|
8
8
|
declare const getUser: () => Promise<LemonUser | null>;
|
|
9
|
+
interface GetSessionOptions {
|
|
10
|
+
clientId?: string;
|
|
11
|
+
}
|
|
12
|
+
declare function getSession(options?: GetSessionOptions): Promise<LemonSession>;
|
|
9
13
|
declare function requireAuth(redirectTo?: string): Promise<LemonUser>;
|
|
10
|
-
|
|
14
|
+
interface RequireClientOptions {
|
|
15
|
+
loginRedirectTo?: string;
|
|
16
|
+
unapprovedRedirectTo?: string;
|
|
17
|
+
}
|
|
18
|
+
declare function requireClient(clientId: string, optionsOrRedirectTo?: string | RequireClientOptions): Promise<LemonUser>;
|
|
11
19
|
|
|
12
20
|
interface RefreshResult {
|
|
13
21
|
ok: boolean;
|
|
14
22
|
setCookieHeaders: string[];
|
|
15
23
|
}
|
|
16
|
-
declare function refreshTokenFromCookie(refreshTokenCookie: string): Promise<RefreshResult>;
|
|
24
|
+
declare function refreshTokenFromCookie(refreshTokenCookie: string, deviceIdCookie?: string): Promise<RefreshResult>;
|
|
17
25
|
|
|
18
26
|
declare const AUTH_SERVER_URL = "https://auth.lemondouble.com";
|
|
19
27
|
declare const JWKS_URL = "https://auth.lemondouble.com/.well-known/jwks.json";
|
|
@@ -24,5 +32,6 @@ declare const PROFILE_URL = "https://auth.lemondouble.com/api/user/me";
|
|
|
24
32
|
declare const PROFILE_PAGE_URL = "https://auth.lemondouble.com/profile";
|
|
25
33
|
declare const ACCESS_TOKEN_COOKIE = "lemon_access_token";
|
|
26
34
|
declare const REFRESH_TOKEN_COOKIE = "lemon_refresh_token";
|
|
35
|
+
declare const DEVICE_ID_COOKIE = "device_id";
|
|
27
36
|
|
|
28
|
-
export { ACCESS_TOKEN_COOKIE, AUTH_SERVER_URL, AccessTokenClaims, JWKS_URL, LOGIN_URL, LOGOUT_URL, LemonUser, PROFILE_PAGE_URL, PROFILE_URL, REFRESH_TOKEN_COOKIE, REFRESH_URL, type RefreshResult, getUser, refreshTokenFromCookie, requireAuth, requireClient, verifyAccessToken, verifyAccessTokenString };
|
|
37
|
+
export { ACCESS_TOKEN_COOKIE, AUTH_SERVER_URL, AccessTokenClaims, DEVICE_ID_COOKIE, type GetSessionOptions, JWKS_URL, LOGIN_URL, LOGOUT_URL, LemonSession, LemonUser, PROFILE_PAGE_URL, PROFILE_URL, REFRESH_TOKEN_COOKIE, REFRESH_URL, type RefreshResult, type RequireClientOptions, getSession, getUser, refreshTokenFromCookie, requireAuth, requireClient, verifyAccessToken, verifyAccessTokenString };
|
package/dist/server/index.js
CHANGED
|
@@ -1,20 +1,29 @@
|
|
|
1
|
+
import {
|
|
2
|
+
isAccessTokenClaims,
|
|
3
|
+
isApprovedClient
|
|
4
|
+
} from "../chunk-6W24RWSJ.js";
|
|
1
5
|
import {
|
|
2
6
|
ACCESS_TOKEN_COOKIE,
|
|
3
7
|
AUTH_SERVER_URL,
|
|
8
|
+
DEVICE_ID_COOKIE,
|
|
4
9
|
JWKS_URL,
|
|
5
10
|
LOGIN_URL,
|
|
6
11
|
LOGOUT_URL,
|
|
7
12
|
PROFILE_PAGE_URL,
|
|
8
13
|
PROFILE_URL,
|
|
9
14
|
REFRESH_TOKEN_COOKIE,
|
|
10
|
-
REFRESH_URL
|
|
11
|
-
|
|
15
|
+
REFRESH_URL,
|
|
16
|
+
getMockAccessTokenClaims,
|
|
17
|
+
isMockAuthEnabled
|
|
18
|
+
} from "../chunk-OX5MDVSR.js";
|
|
12
19
|
|
|
13
20
|
// src/server/verify.ts
|
|
14
21
|
import { createRemoteJWKSet, jwtVerify } from "jose";
|
|
15
22
|
import { cookies } from "next/headers";
|
|
16
23
|
var jwks = createRemoteJWKSet(new URL(JWKS_URL));
|
|
17
24
|
async function verifyAccessToken() {
|
|
25
|
+
const mockClaims = getMockAccessTokenClaims();
|
|
26
|
+
if (mockClaims) return mockClaims;
|
|
18
27
|
const cookieStore = await cookies();
|
|
19
28
|
const token = cookieStore.get(ACCESS_TOKEN_COOKIE)?.value;
|
|
20
29
|
if (!token) return null;
|
|
@@ -22,17 +31,19 @@ async function verifyAccessToken() {
|
|
|
22
31
|
const { payload } = await jwtVerify(token, jwks, {
|
|
23
32
|
issuer: AUTH_SERVER_URL
|
|
24
33
|
});
|
|
25
|
-
return payload;
|
|
34
|
+
return isAccessTokenClaims(payload) ? payload : null;
|
|
26
35
|
} catch {
|
|
27
36
|
return null;
|
|
28
37
|
}
|
|
29
38
|
}
|
|
30
39
|
async function verifyAccessTokenString(token) {
|
|
40
|
+
const mockClaims = getMockAccessTokenClaims();
|
|
41
|
+
if (mockClaims) return mockClaims;
|
|
31
42
|
try {
|
|
32
43
|
const { payload } = await jwtVerify(token, jwks, {
|
|
33
44
|
issuer: AUTH_SERVER_URL
|
|
34
45
|
});
|
|
35
|
-
return payload;
|
|
46
|
+
return isAccessTokenClaims(payload) ? payload : null;
|
|
36
47
|
} catch {
|
|
37
48
|
return null;
|
|
38
49
|
}
|
|
@@ -55,25 +66,47 @@ var getUser = cache(async () => {
|
|
|
55
66
|
if (!claims) return null;
|
|
56
67
|
return claimsToUser(claims);
|
|
57
68
|
});
|
|
69
|
+
var getSessionByClientId = cache(
|
|
70
|
+
async (clientId) => {
|
|
71
|
+
const user = await getUser();
|
|
72
|
+
if (!user) return { type: "none" };
|
|
73
|
+
if (!isApprovedClient(user.approvedClients, clientId)) {
|
|
74
|
+
return { type: "unapproved", user };
|
|
75
|
+
}
|
|
76
|
+
return { type: "authenticated", user };
|
|
77
|
+
}
|
|
78
|
+
);
|
|
79
|
+
async function getSession(options = {}) {
|
|
80
|
+
return getSessionByClientId(options.clientId);
|
|
81
|
+
}
|
|
58
82
|
async function requireAuth(redirectTo = "/") {
|
|
59
83
|
const user = await getUser();
|
|
60
84
|
if (user) return user;
|
|
61
85
|
redirect(redirectTo);
|
|
62
86
|
}
|
|
63
|
-
async function requireClient(clientId,
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
87
|
+
async function requireClient(clientId, optionsOrRedirectTo = "/") {
|
|
88
|
+
const options = typeof optionsOrRedirectTo === "string" ? { loginRedirectTo: optionsOrRedirectTo } : optionsOrRedirectTo;
|
|
89
|
+
const session = await getSession({ clientId });
|
|
90
|
+
if (session.type === "none") {
|
|
91
|
+
redirect(options.loginRedirectTo ?? "/");
|
|
67
92
|
}
|
|
68
|
-
|
|
93
|
+
if (session.type === "unapproved") {
|
|
94
|
+
redirect(
|
|
95
|
+
options.unapprovedRedirectTo ?? `${AUTH_SERVER_URL}/error?code=FORBIDDEN`
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
return session.user;
|
|
69
99
|
}
|
|
70
100
|
|
|
71
101
|
// src/server/refresh.ts
|
|
72
|
-
async function refreshTokenFromCookie(refreshTokenCookie) {
|
|
102
|
+
async function refreshTokenFromCookie(refreshTokenCookie, deviceIdCookie) {
|
|
103
|
+
if (isMockAuthEnabled()) {
|
|
104
|
+
return { ok: true, setCookieHeaders: [] };
|
|
105
|
+
}
|
|
73
106
|
const res = await fetch(REFRESH_URL, {
|
|
74
107
|
method: "POST",
|
|
75
108
|
headers: {
|
|
76
|
-
Cookie:
|
|
109
|
+
Cookie: buildRefreshCookieHeader(refreshTokenCookie, deviceIdCookie)
|
|
77
110
|
}
|
|
78
111
|
});
|
|
79
112
|
if (!res.ok) {
|
|
@@ -82,9 +115,17 @@ async function refreshTokenFromCookie(refreshTokenCookie) {
|
|
|
82
115
|
const setCookieHeaders = res.headers.getSetCookie();
|
|
83
116
|
return { ok: true, setCookieHeaders };
|
|
84
117
|
}
|
|
118
|
+
function buildRefreshCookieHeader(refreshTokenCookie, deviceIdCookie) {
|
|
119
|
+
const cookies2 = [`${REFRESH_TOKEN_COOKIE}=${refreshTokenCookie}`];
|
|
120
|
+
if (deviceIdCookie) {
|
|
121
|
+
cookies2.push(`${DEVICE_ID_COOKIE}=${deviceIdCookie}`);
|
|
122
|
+
}
|
|
123
|
+
return cookies2.join("; ");
|
|
124
|
+
}
|
|
85
125
|
export {
|
|
86
126
|
ACCESS_TOKEN_COOKIE,
|
|
87
127
|
AUTH_SERVER_URL,
|
|
128
|
+
DEVICE_ID_COOKIE,
|
|
88
129
|
JWKS_URL,
|
|
89
130
|
LOGIN_URL,
|
|
90
131
|
LOGOUT_URL,
|
|
@@ -92,6 +133,7 @@ export {
|
|
|
92
133
|
PROFILE_URL,
|
|
93
134
|
REFRESH_TOKEN_COOKIE,
|
|
94
135
|
REFRESH_URL,
|
|
136
|
+
getSession,
|
|
95
137
|
getUser,
|
|
96
138
|
refreshTokenFromCookie,
|
|
97
139
|
requireAuth,
|
package/dist/server/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/server/verify.ts","../../src/server/user.ts","../../src/server/refresh.ts"],"sourcesContent":["import { createRemoteJWKSet, jwtVerify } from \"jose\";\nimport { cookies } from \"next/headers\";\nimport type { AccessTokenClaims } from \"../types.js\";\nimport { JWKS_URL, AUTH_SERVER_URL, ACCESS_TOKEN_COOKIE } from \"../constants.js\";\n\nconst jwks = createRemoteJWKSet(new URL(JWKS_URL));\n\nexport async function verifyAccessToken(): Promise<AccessTokenClaims | null> {\n const cookieStore = await cookies();\n const token = cookieStore.get(ACCESS_TOKEN_COOKIE)?.value;\n if (!token) return null;\n\n try {\n const { payload } = await jwtVerify(token, jwks, {\n issuer: AUTH_SERVER_URL,\n });\n return payload
|
|
1
|
+
{"version":3,"sources":["../../src/server/verify.ts","../../src/server/user.ts","../../src/server/refresh.ts"],"sourcesContent":["import { createRemoteJWKSet, jwtVerify } from \"jose\";\nimport { cookies } from \"next/headers\";\nimport type { AccessTokenClaims } from \"../types.js\";\nimport { JWKS_URL, AUTH_SERVER_URL, ACCESS_TOKEN_COOKIE } from \"../constants.js\";\nimport { isAccessTokenClaims } from \"../claims.js\";\nimport { getMockAccessTokenClaims } from \"../mock.js\";\n\nconst jwks = createRemoteJWKSet(new URL(JWKS_URL));\n\nexport async function verifyAccessToken(): Promise<AccessTokenClaims | null> {\n const mockClaims = getMockAccessTokenClaims();\n if (mockClaims) return mockClaims;\n\n const cookieStore = await cookies();\n const token = cookieStore.get(ACCESS_TOKEN_COOKIE)?.value;\n if (!token) return null;\n\n try {\n const { payload } = await jwtVerify(token, jwks, {\n issuer: AUTH_SERVER_URL,\n });\n return isAccessTokenClaims(payload) ? payload : null;\n } catch {\n return null;\n }\n}\n\nexport async function verifyAccessTokenString(\n token: string\n): Promise<AccessTokenClaims | null> {\n const mockClaims = getMockAccessTokenClaims();\n if (mockClaims) return mockClaims;\n\n try {\n const { payload } = await jwtVerify(token, jwks, {\n issuer: AUTH_SERVER_URL,\n });\n return isAccessTokenClaims(payload) ? payload : null;\n } catch {\n return null;\n }\n}\n","import { cache } from \"react\";\nimport { redirect } from \"next/navigation\";\nimport type { LemonSession, LemonUser } from \"../types.js\";\nimport { AUTH_SERVER_URL } from \"../constants.js\";\nimport { isApprovedClient } from \"../approval.js\";\nimport { verifyAccessToken } from \"./verify.js\";\n\nfunction claimsToUser(\n claims: NonNullable<Awaited<ReturnType<typeof verifyAccessToken>>>\n): LemonUser {\n return {\n uid: claims.sub,\n nickname: claims.nickname,\n profileImageUrl: claims.profile_image_url,\n role: claims.role,\n approvedClients: claims.approved_clients,\n };\n}\n\nexport const getUser = cache(async (): Promise<LemonUser | null> => {\n const claims = await verifyAccessToken();\n if (!claims) return null;\n return claimsToUser(claims);\n});\n\nexport interface GetSessionOptions {\n clientId?: string;\n}\n\nconst getSessionByClientId = cache(\n async (clientId?: string): Promise<LemonSession> => {\n const user = await getUser();\n if (!user) return { type: \"none\" };\n if (!isApprovedClient(user.approvedClients, clientId)) {\n return { type: \"unapproved\", user };\n }\n return { type: \"authenticated\", user };\n }\n);\n\nexport async function getSession(\n options: GetSessionOptions = {}\n): Promise<LemonSession> {\n return getSessionByClientId(options.clientId);\n}\n\nexport async function requireAuth(redirectTo = \"/\"): Promise<LemonUser> {\n const user = await getUser();\n if (user) return user;\n redirect(redirectTo);\n}\n\nexport interface RequireClientOptions {\n loginRedirectTo?: string;\n unapprovedRedirectTo?: string;\n}\n\nexport async function requireClient(\n clientId: string,\n optionsOrRedirectTo: string | RequireClientOptions = \"/\"\n): Promise<LemonUser> {\n const options =\n typeof optionsOrRedirectTo === \"string\"\n ? { loginRedirectTo: optionsOrRedirectTo }\n : optionsOrRedirectTo;\n const session = await getSession({ clientId });\n if (session.type === \"none\") {\n redirect(options.loginRedirectTo ?? \"/\");\n }\n if (session.type === \"unapproved\") {\n redirect(\n options.unapprovedRedirectTo ??\n `${AUTH_SERVER_URL}/error?code=FORBIDDEN`\n );\n }\n return session.user;\n}\n","import {\n DEVICE_ID_COOKIE,\n REFRESH_TOKEN_COOKIE,\n REFRESH_URL,\n} from \"../constants.js\";\nimport { isMockAuthEnabled } from \"../mock.js\";\n\nexport interface RefreshResult {\n ok: boolean;\n setCookieHeaders: string[];\n}\n\nexport async function refreshTokenFromCookie(\n refreshTokenCookie: string,\n deviceIdCookie?: string\n): Promise<RefreshResult> {\n if (isMockAuthEnabled()) {\n return { ok: true, setCookieHeaders: [] };\n }\n\n const res = await fetch(REFRESH_URL, {\n method: \"POST\",\n headers: {\n Cookie: buildRefreshCookieHeader(refreshTokenCookie, deviceIdCookie),\n },\n });\n\n if (!res.ok) {\n return { ok: false, setCookieHeaders: [] };\n }\n\n const setCookieHeaders = res.headers.getSetCookie();\n return { ok: true, setCookieHeaders };\n}\n\nfunction buildRefreshCookieHeader(\n refreshTokenCookie: string,\n deviceIdCookie?: string\n): string {\n const cookies = [`${REFRESH_TOKEN_COOKIE}=${refreshTokenCookie}`];\n if (deviceIdCookie) {\n cookies.push(`${DEVICE_ID_COOKIE}=${deviceIdCookie}`);\n }\n return cookies.join(\"; \");\n}\n"],"mappings":"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","names":["cookies"]}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { JWTPayload } from 'jose';
|
|
2
2
|
|
|
3
3
|
interface AccessTokenClaims extends JWTPayload {
|
|
4
|
+
token_type: "access";
|
|
4
5
|
sub: string;
|
|
5
6
|
nickname: string;
|
|
6
7
|
profile_image_url: string;
|
|
@@ -14,6 +15,15 @@ interface LemonUser {
|
|
|
14
15
|
role: "user" | "admin";
|
|
15
16
|
approvedClients: string[];
|
|
16
17
|
}
|
|
18
|
+
type LemonSession = {
|
|
19
|
+
type: "none";
|
|
20
|
+
} | {
|
|
21
|
+
type: "unapproved";
|
|
22
|
+
user: LemonUser;
|
|
23
|
+
} | {
|
|
24
|
+
type: "authenticated";
|
|
25
|
+
user: LemonUser;
|
|
26
|
+
};
|
|
17
27
|
interface UserProfile {
|
|
18
28
|
uid: string;
|
|
19
29
|
nickname: string;
|
|
@@ -21,4 +31,4 @@ interface UserProfile {
|
|
|
21
31
|
role: "user" | "admin";
|
|
22
32
|
}
|
|
23
33
|
|
|
24
|
-
export type { AccessTokenClaims as A,
|
|
34
|
+
export type { AccessTokenClaims as A, LemonSession as L, UserProfile as U, LemonUser as a };
|