@ouim/logto-authkit 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +823 -0
- package/dist/bundler-config.cjs +1 -0
- package/dist/bundler-config.d.ts +81 -0
- package/dist/bundler-config.js +31 -0
- package/dist/callback.d.ts +3 -0
- package/dist/components/signin-button.d.ts +53 -0
- package/dist/components/ui/alert.d.ts +8 -0
- package/dist/components/ui/avatar.d.ts +6 -0
- package/dist/components/ui/badge.d.ts +9 -0
- package/dist/components/ui/button.d.ts +11 -0
- package/dist/components/ui/card.d.ts +8 -0
- package/dist/components/ui/dialog.d.ts +19 -0
- package/dist/components/ui/dropdown-menu.d.ts +17 -0
- package/dist/components/ui/loading-spinner.d.ts +2 -0
- package/dist/components/ui/skeleton.d.ts +2 -0
- package/dist/components/ui/tooltip.d.ts +7 -0
- package/dist/components/utils/utils.d.ts +2 -0
- package/dist/context.d.ts +57 -0
- package/dist/index.cjs +129 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +9583 -0
- package/dist/navigation.d.ts +10 -0
- package/dist/server/authorization.d.ts +53 -0
- package/dist/server/csrf.d.ts +247 -0
- package/dist/server/index.cjs +1 -0
- package/dist/server/index.d.ts +4 -0
- package/dist/server/index.js +431 -0
- package/dist/server/types.d.ts +62 -0
- package/dist/server/verify-auth.d.ts +296 -0
- package/dist/signin.d.ts +42 -0
- package/dist/types.d.ts +81 -0
- package/dist/useAuth.d.ts +55 -0
- package/dist/usePermission.d.ts +9 -0
- package/dist/user-center.d.ts +49 -0
- package/dist/utils.d.ts +169 -0
- package/package.json +111 -0
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { NavigationOptions } from './types.js';
|
|
3
|
+
type NavigateFunction = (url: string, options?: NavigationOptions) => void;
|
|
4
|
+
interface NavigationProviderProps {
|
|
5
|
+
children: React.ReactNode;
|
|
6
|
+
customNavigate?: NavigateFunction;
|
|
7
|
+
}
|
|
8
|
+
export declare const NavigationProvider: ({ children, customNavigate }: NavigationProviderProps) => import("react/jsx-runtime").JSX.Element;
|
|
9
|
+
export declare const useNavigation: () => NavigateFunction;
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { AuthContext, AuthPayload } from './types.js';
|
|
2
|
+
export type AuthorizationMode = 'all' | 'any';
|
|
3
|
+
export interface ScopeCheckOptions {
|
|
4
|
+
mode?: AuthorizationMode;
|
|
5
|
+
}
|
|
6
|
+
export interface RoleCheckOptions {
|
|
7
|
+
claimKeys?: string[];
|
|
8
|
+
}
|
|
9
|
+
type AuthSubject = AuthContext | AuthPayload | null | undefined;
|
|
10
|
+
/**
|
|
11
|
+
* Return `true` when the token payload contains the requested scopes.
|
|
12
|
+
*
|
|
13
|
+
* Accepts either a raw `AuthPayload` or a full `AuthContext`. Scope comparison is
|
|
14
|
+
* whitespace-delimited to match the standard OAuth `scope` claim shape used by Logto.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* hasScopes(auth.payload, ['read:profile', 'write:profile'])
|
|
18
|
+
* hasScopes(auth, ['read:profile', 'write:profile'], { mode: 'any' })
|
|
19
|
+
*/
|
|
20
|
+
export declare function hasScopes(subject: AuthSubject, scopes: string | string[], options?: ScopeCheckOptions): boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Assert that the token payload contains the requested scopes.
|
|
23
|
+
*
|
|
24
|
+
* Throws a descriptive error listing the missing scopes when the check fails.
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* requireScopes(auth, ['read:profile', 'write:profile'])
|
|
28
|
+
* requireScopes(payload, ['admin:read', 'admin:write'], { mode: 'any' })
|
|
29
|
+
*/
|
|
30
|
+
export declare function requireScopes(subject: AuthSubject, scopes: string | string[], options?: ScopeCheckOptions): void;
|
|
31
|
+
/**
|
|
32
|
+
* Return `true` when the token payload contains the requested role.
|
|
33
|
+
*
|
|
34
|
+
* By default the helper checks `roles` first and then `role`. Override
|
|
35
|
+
* `claimKeys` when your tenant emits roles under a custom claim name.
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* hasRole(auth, 'admin')
|
|
39
|
+
* hasRole(auth.payload, 'support', { claimKeys: ['https://example.com/roles'] })
|
|
40
|
+
*/
|
|
41
|
+
export declare function hasRole(subject: AuthSubject, role: string, options?: RoleCheckOptions): boolean;
|
|
42
|
+
/**
|
|
43
|
+
* Assert that the token payload contains the requested role.
|
|
44
|
+
*
|
|
45
|
+
* Throws a descriptive error when the role claim is missing or does not contain
|
|
46
|
+
* the required role.
|
|
47
|
+
*
|
|
48
|
+
* @example
|
|
49
|
+
* requireRole(auth, 'admin')
|
|
50
|
+
* requireRole(payload, 'tenant:owner', { claimKeys: ['https://example.com/roles'] })
|
|
51
|
+
*/
|
|
52
|
+
export declare function requireRole(subject: AuthSubject, role: string, options?: RoleCheckOptions): void;
|
|
53
|
+
export {};
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CSRF Protection Helpers for `@ouim/logto-authkit`
|
|
3
|
+
*
|
|
4
|
+
* Implements the **Double-Submit Cookie** pattern — the most portable stateless
|
|
5
|
+
* CSRF defence that requires no server-side session storage.
|
|
6
|
+
*
|
|
7
|
+
* ─────────────────────────────────────────────────────────────────────────────
|
|
8
|
+
* THREAT MODEL
|
|
9
|
+
* ─────────────────────────────────────────────────────────────────────────────
|
|
10
|
+
*
|
|
11
|
+
* WHAT IS CSRF?
|
|
12
|
+
* A Cross-Site Request Forgery attack tricks a victim's authenticated browser
|
|
13
|
+
* into making a state-changing request (POST/PUT/DELETE) to your API. Because
|
|
14
|
+
* browsers automatically include cookies with cross-origin requests, the server
|
|
15
|
+
* sees what appears to be a legitimate authenticated request even though the
|
|
16
|
+
* user never intended to make it.
|
|
17
|
+
*
|
|
18
|
+
* JWT cookies are NOT immune. If `logto_authtoken` is a `SameSite=Lax` cookie
|
|
19
|
+
* (the browser default when `SameSite` is omitted), cross-site top-level
|
|
20
|
+
* navigations (e.g. a form POST from an attacker's page) will include it.
|
|
21
|
+
* Even `SameSite=Strict` cookies are sent in some redirect flows and are not
|
|
22
|
+
* universally enforced across all browser versions.
|
|
23
|
+
*
|
|
24
|
+
* HOW THE DOUBLE-SUBMIT COOKIE PATTERN WORKS:
|
|
25
|
+
* 1. The server generates a random token and sets it in a **non-HttpOnly**
|
|
26
|
+
* cookie (the browser's same-origin policy keeps it readable only to your
|
|
27
|
+
* own JavaScript, not to scripts on other origins).
|
|
28
|
+
* 2. For every mutating request (POST, PUT, PATCH, DELETE), client-side JS
|
|
29
|
+
* reads the CSRF cookie value and sends it as a custom request header
|
|
30
|
+
* (e.g. `X-CSRF-Token`).
|
|
31
|
+
* 3. The server middleware compares the header value to the cookie value.
|
|
32
|
+
* A mismatch → 403 Forbidden.
|
|
33
|
+
*
|
|
34
|
+
* WHY AN ATTACKER CAN'T FORGE THIS:
|
|
35
|
+
* - Cross-origin JavaScript cannot read cookies from your domain
|
|
36
|
+
* (same-origin policy).
|
|
37
|
+
* - Cross-origin requests cannot set custom headers without a successful
|
|
38
|
+
* CORS preflight, which your server controls.
|
|
39
|
+
* - An attacker who can read your cookies has already achieved XSS on your
|
|
40
|
+
* domain — CSRF is no longer the threat at that point.
|
|
41
|
+
*
|
|
42
|
+
* LIMITATIONS / NOTES:
|
|
43
|
+
* - CSRF protection is complementary to, not a replacement for, a strict
|
|
44
|
+
* Content-Security-Policy and XSS hardening.
|
|
45
|
+
* - `SameSite=Strict` on the *auth* cookie provides strong CSRF protection
|
|
46
|
+
* on its own for modern browsers: the browser withholds the cookie on all
|
|
47
|
+
* cross-site requests, including cross-site form POSTs. The double-submit
|
|
48
|
+
* pattern here is defence-in-depth for older browsers and programmatic
|
|
49
|
+
* enforcement when you want to verify CSRF explicitly in middleware logic.
|
|
50
|
+
* - The CSRF cookie is intentionally **not** `HttpOnly` — the client JS
|
|
51
|
+
* must be able to read it. Do not store anything sensitive in it.
|
|
52
|
+
* - Safe HTTP methods (GET, HEAD, OPTIONS, TRACE) are exempt from CSRF
|
|
53
|
+
* validation. Ensure your API does not use GET for state changes.
|
|
54
|
+
*
|
|
55
|
+
* ─────────────────────────────────────────────────────────────────────────────
|
|
56
|
+
* QUICK-START — Express
|
|
57
|
+
* ─────────────────────────────────────────────────────────────────────────────
|
|
58
|
+
*
|
|
59
|
+
* ```ts
|
|
60
|
+
* import express from 'express';
|
|
61
|
+
* import { createExpressAuthMiddleware } from '@ouim/logto-authkit/server';
|
|
62
|
+
* import { createCsrfMiddleware, generateCsrfToken, buildCsrfCookieHeader } from '@ouim/logto-authkit/server';
|
|
63
|
+
*
|
|
64
|
+
* const app = express();
|
|
65
|
+
*
|
|
66
|
+
* // Mount CSRF middleware on all routes that mutate state.
|
|
67
|
+
* // The middleware sets the CSRF cookie on GET requests (if absent)
|
|
68
|
+
* // and validates it on POST/PUT/PATCH/DELETE.
|
|
69
|
+
* app.use(createCsrfMiddleware());
|
|
70
|
+
*
|
|
71
|
+
* // Auth middleware can be layered on top.
|
|
72
|
+
* app.use('/api', createExpressAuthMiddleware({ logtoUrl: process.env.LOGTO_ENDPOINT }));
|
|
73
|
+
* ```
|
|
74
|
+
*
|
|
75
|
+
* Client-side fetch helper:
|
|
76
|
+
* ```ts
|
|
77
|
+
* function getCsrfToken(): string {
|
|
78
|
+
* return document.cookie
|
|
79
|
+
* .split('; ')
|
|
80
|
+
* .find(c => c.startsWith('logto_csrf_token='))
|
|
81
|
+
* ?.split('=')[1] ?? '';
|
|
82
|
+
* }
|
|
83
|
+
*
|
|
84
|
+
* // HTTP headers are case-insensitive; use lowercase to match CSRF_HEADER_NAME
|
|
85
|
+
* await fetch('/api/data', {
|
|
86
|
+
* method: 'POST',
|
|
87
|
+
* headers: { 'x-csrf-token': getCsrfToken(), 'Content-Type': 'application/json' },
|
|
88
|
+
* body: JSON.stringify({ ... }),
|
|
89
|
+
* });
|
|
90
|
+
* ```
|
|
91
|
+
*
|
|
92
|
+
* ─────────────────────────────────────────────────────────────────────────────
|
|
93
|
+
* QUICK-START — Next.js
|
|
94
|
+
* ─────────────────────────────────────────────────────────────────────────────
|
|
95
|
+
*
|
|
96
|
+
* ```ts
|
|
97
|
+
* // app/api/data/route.ts
|
|
98
|
+
* import { NextRequest, NextResponse } from 'next/server';
|
|
99
|
+
* import { verifyCsrfToken, generateCsrfToken, buildCsrfCookieHeader } from '@ouim/logto-authkit/server';
|
|
100
|
+
*
|
|
101
|
+
* // GET — issue a fresh CSRF token
|
|
102
|
+
* export async function GET() {
|
|
103
|
+
* const token = generateCsrfToken();
|
|
104
|
+
* return new Response(JSON.stringify({ csrfToken: token }), {
|
|
105
|
+
* headers: { 'Set-Cookie': buildCsrfCookieHeader(token) },
|
|
106
|
+
* });
|
|
107
|
+
* }
|
|
108
|
+
*
|
|
109
|
+
* // POST — validate the CSRF token before processing
|
|
110
|
+
* export async function POST(request: NextRequest) {
|
|
111
|
+
* const csrf = verifyCsrfToken(request);
|
|
112
|
+
* if (!csrf.valid) {
|
|
113
|
+
* return NextResponse.json({ error: csrf.error }, { status: 403 });
|
|
114
|
+
* }
|
|
115
|
+
* // ... process the request
|
|
116
|
+
* }
|
|
117
|
+
* ```
|
|
118
|
+
*/
|
|
119
|
+
import type { ExpressRequest, ExpressResponse, ExpressNext, NextRequest } from './types.js';
|
|
120
|
+
/** Default name for the CSRF token cookie (non-HttpOnly, readable by JS). */
|
|
121
|
+
export declare const CSRF_COOKIE_NAME = "logto_csrf_token";
|
|
122
|
+
/** Default request header that clients must send the CSRF token in. */
|
|
123
|
+
export declare const CSRF_HEADER_NAME = "x-csrf-token";
|
|
124
|
+
/**
|
|
125
|
+
* Generate a cryptographically random CSRF token string.
|
|
126
|
+
*
|
|
127
|
+
* Uses `randomUUID` from `node:crypto` (available since Node.js 14.17,
|
|
128
|
+
* works in both CJS and ESM builds). Provides 122 bits of entropy —
|
|
129
|
+
* sufficient for CSRF tokens.
|
|
130
|
+
*
|
|
131
|
+
* @example
|
|
132
|
+
* import { generateCsrfToken } from '@ouim/logto-authkit/server';
|
|
133
|
+
*
|
|
134
|
+
* const csrfToken = generateCsrfToken();
|
|
135
|
+
* // Store it in a cookie or send it to the client in a bootstrap response
|
|
136
|
+
*/
|
|
137
|
+
export declare function generateCsrfToken(): string;
|
|
138
|
+
/**
|
|
139
|
+
* Build a `Set-Cookie` header string for the CSRF token cookie.
|
|
140
|
+
*
|
|
141
|
+
* The cookie is deliberately **not** `HttpOnly` — client-side JavaScript must
|
|
142
|
+
* be able to read this value and send it as a request header. An attacker on
|
|
143
|
+
* a different origin cannot read it because of the browser's same-origin policy.
|
|
144
|
+
*
|
|
145
|
+
* @param {string} token - The CSRF token value (from `generateCsrfToken()`).
|
|
146
|
+
* @param {object} [options]
|
|
147
|
+
* @param {string} [options.cookieName='logto_csrf_token'] - Cookie name.
|
|
148
|
+
* @param {number} [options.maxAge=86400] - Cookie lifetime in seconds (default: 1 day).
|
|
149
|
+
* @param {string} [options.domain] - Cookie domain (omit for current host).
|
|
150
|
+
* @param {string} [options.path='/'] - Cookie path.
|
|
151
|
+
* @param {'Strict'|'Lax'} [options.sameSite='Strict'] - SameSite policy.
|
|
152
|
+
* `'None'` is intentionally excluded: CSRF cookies sent to third parties
|
|
153
|
+
* defeat the purpose of the double-submit pattern.
|
|
154
|
+
*
|
|
155
|
+
* @returns {string} A complete `Set-Cookie` header value.
|
|
156
|
+
*
|
|
157
|
+
* @example
|
|
158
|
+
* import { generateCsrfToken, buildCsrfCookieHeader } from '@ouim/logto-authkit/server';
|
|
159
|
+
*
|
|
160
|
+
* const token = generateCsrfToken();
|
|
161
|
+
* const cookie = buildCsrfCookieHeader(token, { sameSite: 'Strict' });
|
|
162
|
+
* res.setHeader('Set-Cookie', cookie);
|
|
163
|
+
*/
|
|
164
|
+
export declare function buildCsrfCookieHeader(token: string, options?: {
|
|
165
|
+
cookieName?: string;
|
|
166
|
+
maxAge?: number;
|
|
167
|
+
domain?: string;
|
|
168
|
+
path?: string;
|
|
169
|
+
sameSite?: 'Strict' | 'Lax';
|
|
170
|
+
}): string;
|
|
171
|
+
/** Options for {@link createCsrfMiddleware}. */
|
|
172
|
+
export interface CsrfMiddlewareOptions {
|
|
173
|
+
/** Cookie name to read/write the CSRF token. Default: `'logto_csrf_token'`. */
|
|
174
|
+
cookieName?: string;
|
|
175
|
+
/** Request header clients must send the CSRF token in. Default: `'x-csrf-token'`. */
|
|
176
|
+
headerName?: string;
|
|
177
|
+
/** Cookie domain to set when issuing a new CSRF token. */
|
|
178
|
+
domain?: string;
|
|
179
|
+
/** Cookie path. Default: `'/'`. */
|
|
180
|
+
path?: string;
|
|
181
|
+
/** Cookie `SameSite` policy. Default: `'Strict'`. */
|
|
182
|
+
sameSite?: 'Strict' | 'Lax';
|
|
183
|
+
/** Cookie lifetime in seconds. Default: `86400` (1 day). */
|
|
184
|
+
maxAge?: number;
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Express middleware that implements CSRF double-submit cookie protection.
|
|
188
|
+
*
|
|
189
|
+
* **Behaviour:**
|
|
190
|
+
* - **Safe methods** (GET, HEAD, OPTIONS, TRACE): if no CSRF cookie is present,
|
|
191
|
+
* a new token is generated and set via `Set-Cookie`. The request is passed
|
|
192
|
+
* through unconditionally.
|
|
193
|
+
* - **Unsafe methods** (POST, PUT, PATCH, DELETE, …): the `X-CSRF-Token` header
|
|
194
|
+
* value is compared to the CSRF cookie value. A mismatch returns `403 Forbidden`.
|
|
195
|
+
*
|
|
196
|
+
* Mount this middleware **before** your auth middleware so CSRF errors are caught
|
|
197
|
+
* even on unauthenticated requests (defence-in-depth).
|
|
198
|
+
*
|
|
199
|
+
* @example
|
|
200
|
+
* import { createCsrfMiddleware } from '@ouim/logto-authkit/server';
|
|
201
|
+
*
|
|
202
|
+
* // Apply globally
|
|
203
|
+
* app.use(createCsrfMiddleware());
|
|
204
|
+
*
|
|
205
|
+
* // Apply only to API routes
|
|
206
|
+
* app.use('/api', createCsrfMiddleware({ sameSite: 'Lax' }));
|
|
207
|
+
*/
|
|
208
|
+
export declare function createCsrfMiddleware(options?: CsrfMiddlewareOptions): (req: ExpressRequest, res: ExpressResponse, next: ExpressNext) => void;
|
|
209
|
+
/** Result returned by {@link verifyCsrfToken}. */
|
|
210
|
+
export interface CsrfVerifyResult {
|
|
211
|
+
/** `true` if the CSRF token is present and matches. */
|
|
212
|
+
valid: boolean;
|
|
213
|
+
/** Human-readable reason for failure (only set when `valid` is `false`). */
|
|
214
|
+
error?: string;
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Verify the CSRF double-submit token for a Next.js request.
|
|
218
|
+
*
|
|
219
|
+
* Compares the value in the `X-CSRF-Token` request header against the
|
|
220
|
+
* `logto_csrf_token` cookie. Returns `{ valid: true }` on success or
|
|
221
|
+
* `{ valid: false, error: '...' }` on failure.
|
|
222
|
+
*
|
|
223
|
+
* Intended for use in Next.js Route Handlers and Middleware for mutating
|
|
224
|
+
* endpoints (POST, PUT, PATCH, DELETE).
|
|
225
|
+
*
|
|
226
|
+
* @param {NextRequest} request - The incoming Next.js request object.
|
|
227
|
+
* @param {object} [options]
|
|
228
|
+
* @param {string} [options.cookieName='logto_csrf_token'] - Cookie name to read.
|
|
229
|
+
* @param {string} [options.headerName='x-csrf-token'] - Header name to read.
|
|
230
|
+
*
|
|
231
|
+
* @returns {CsrfVerifyResult}
|
|
232
|
+
*
|
|
233
|
+
* @example
|
|
234
|
+
* import { verifyCsrfToken } from '@ouim/logto-authkit/server';
|
|
235
|
+
*
|
|
236
|
+
* export async function POST(request: NextRequest) {
|
|
237
|
+
* const csrf = verifyCsrfToken(request);
|
|
238
|
+
* if (!csrf.valid) {
|
|
239
|
+
* return NextResponse.json({ error: csrf.error }, { status: 403 });
|
|
240
|
+
* }
|
|
241
|
+
* // proceed with request handling
|
|
242
|
+
* }
|
|
243
|
+
*/
|
|
244
|
+
export declare function verifyCsrfToken(request: NextRequest, options?: {
|
|
245
|
+
cookieName?: string;
|
|
246
|
+
headerName?: string;
|
|
247
|
+
}): CsrfVerifyResult;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const $=require("jose"),D=require("cookie-parser"),O=require("node:crypto");function x(e){return typeof e=="object"&&e!==null&&"get"in e&&typeof e.get=="function"}function K(e){return typeof e=="object"&&e!==null}function L(e){return typeof e=="object"&&e!==null}const l=new Map,P=300*1e3;function J(e){return`${e.replace(/\/+$/,"")}/oidc`}function f(e,t="guest_logto_authtoken"){if(x(e)){const n=e.get(t);return(n==null?void 0:n.value)??void 0}else if(K(e))return e[t]??void 0}function m(e,t="logto_authtoken"){if(x(e)){const n=e.get(t);return(n==null?void 0:n.value)||null}else if(K(e))return e[t]||null;return null}function A(e){if(!L(e))return null;const t=typeof e.get=="function"?e.get("authorization"):e.authorization;return typeof t=="string"&&t.startsWith("Bearer ")?t.slice(7):null}function z(e){let t=e.replace(/-/g,"+").replace(/_/g,"/");const n=t.length%4;return n&&(t+="=".repeat(4-n)),typeof atob<"u"?atob(t):Buffer.from(t,"base64").toString()}async function b(e){const{logtoUrl:t,jwksCacheTtlMs:n=P,skipJwksCache:o=!1}=e,r=E(t),i=Date.now(),s=l.get(r);if(!o&&s&&s.expires>i)return s.keys;try{const a=await fetch(r);if(!a.ok)throw new Error(`Failed to fetch JWKS: ${a.status} ${a.statusText}`);const c=(await a.json()).keys||[];return!o&&n>0?l.set(r,{keys:c,expires:i+n}):l.delete(r),c}catch(a){throw new Error(`Failed to fetch JWKS from ${r}: ${a instanceof Error?a.message:"Unknown error"}`)}}function E(e){return`${J(e)}/jwks`}function B(e){l.delete(E(e))}function V(){l.clear()}function Y(e,t,n){if(!e||e.length===0)throw new Error("No keys found in JWKS");if(t){const r=e.find(i=>i.kid===t);if(r)return r;throw new Error(`Key with kid "${t}" not found in JWKS`)}if(n){const r=e.find(i=>i.alg===n);if(r)return r}const o=e.find(r=>r.kty==="RSA"&&(r.use==="sig"||!r.use));return o||e[0]}function q(e,t){const{logtoUrl:n,audience:o,requiredScope:r}=t,i=typeof e.exp=="number"?e.exp:void 0,s=typeof e.nbf=="number"?e.nbf:void 0,a=J(n);if(e.iss!==a)throw new Error(`Invalid issuer. Expected: ${a}, Got: ${e.iss}`);if(o){const c=e.aud,d=Array.isArray(o)?o:[o];if(!(Array.isArray(c)?c:c!==void 0?[c]:[]).some(y=>d.includes(y)))throw new Error(`Invalid audience. Expected one of: ${JSON.stringify(d)}, Got: ${Array.isArray(c)?JSON.stringify(c):c}`)}const u=Math.floor(Date.now()/1e3);if(i!==void 0&&i<u)throw new Error("Token has expired");if(s!==void 0&&s>u)throw new Error("Token is not yet valid");if(r&&(!e.scope||!e.scope.includes(r)))throw new Error(`Missing required scope: ${r}`)}function Q(e){if(typeof e!="object"||e===null)throw new Error("JWT payload is missing or not an object");const t=e;if(typeof t.sub!="string"||t.sub.trim()==="")throw new Error('JWT payload is missing required field "sub" (user ID)');if(typeof t.iss!="string"||t.iss.trim()==="")throw new Error('JWT payload is missing required field "iss" (issuer)');if(t.exp!==void 0&&typeof t.exp!="number")throw new Error('JWT payload field "exp" must be a number');if(t.nbf!==void 0&&typeof t.nbf!="number")throw new Error('JWT payload field "nbf" must be a number');if(t.aud!==void 0){const n=t.aud;if(!(typeof n=="string"||Array.isArray(n)&&n.every(r=>typeof r=="string")))throw new Error('JWT payload field "aud" must be a string or string[]')}if(t.scope!==void 0&&typeof t.scope!="string")throw new Error('JWT payload field "scope" must be a string')}function X(e){if(!(e instanceof Error))return!1;if(e.message.startsWith("No keys found in JWKS")||e.message.startsWith("Key with kid"))return!0;const t=e.code;return t==="ERR_JWKS_NO_MATCHING_KEY"||t==="ERR_JWS_SIGNATURE_VERIFICATION_FAILED"}async function _(e,t,n,o){const r=typeof t.kid=="string"?t.kid:void 0,i=typeof t.alg=="string"?t.alg:void 0,s=Y(n,r,i),a=await $.importJWK(s,i||"RS256"),{payload:u}=await $.jwtVerify(e,a);return Q(u),q(u,o),{userId:u.sub,isAuthenticated:!0,payload:u,isGuest:!1}}async function w(e,t){const{logtoUrl:n}=t,o=E(n);try{const[r]=e.split(".");if(!r)throw new Error("Invalid JWT format");const i=z(r),s=JSON.parse(i),a=await b(t);try{return await _(e,s,a,t)}catch(u){if(!X(u))throw u;console.warn("[verifyLogtoToken] Key/signature error — invalidating JWKS cache and retrying with fresh keys"),l.delete(o);const c=await b(t);return await _(e,s,c,t)}}catch(r){throw new Error(`Token verification failed: ${r instanceof Error?r.message:"Unknown error"}`)}}function Z(e){const t=D(),n=async(o,r,i)=>{try{let s=m(o.cookies,e.cookieName);if(s||(s=A(o.headers)),!s){if(e.allowGuest){const u=f(o.cookies);return o.auth={userId:null,isAuthenticated:!1,payload:null,isGuest:!0,guestId:u},i()}return r.status(401).json({error:"Authentication required",message:"No token found in cookies or Authorization header"})}const a=await w(s,e);return o.auth=a,i()}catch(s){if(e.allowGuest){const a=f(o.cookies);return o.auth={userId:null,isAuthenticated:!1,payload:null,isGuest:!0,guestId:a||void 0},i()}return r.status(401).json({error:"Authentication failed",message:s instanceof Error?s.message:"Unknown error"})}};return async(o,r,i)=>{o.cookies?await n(o,r,i):t(o,r,async s=>{if(s)return i(s);await n(o,r,i)})}}async function ee(e,t){try{let n=m(e.cookies,t.cookieName);return n||(n=A(e.headers)),n?{success:!0,auth:await w(n,t)}:t.allowGuest?{success:!0,auth:{userId:null,isAuthenticated:!1,payload:null,isGuest:!0,guestId:f(e.cookies)||void 0}}:{success:!1,error:"No token found in cookies or Authorization header"}}catch(n){if(t.allowGuest){const o=f(e.cookies),r=n instanceof Error?n.message:"Unknown error";return console.warn(`[verifyNextAuth] Token verification failed, falling back to guest: ${r}`),{success:!0,auth:{userId:null,isAuthenticated:!1,payload:null,isGuest:!0,guestId:o||void 0}}}return{success:!1,error:n instanceof Error?n.message:"Unknown error"}}}async function te(e,t){let n;if(typeof e=="string")n=e;else{const o=m(e.cookies,t.cookieName)||A(e.headers);if(!o){if(t.allowGuest)return{userId:null,isAuthenticated:!1,payload:null,isGuest:!0,guestId:f(e.cookies)||void 0};throw new Error("No token found in request")}n=o}try{return await w(n,t)}catch(o){if(t.allowGuest&&typeof e=="object")return{userId:null,isAuthenticated:!1,payload:null,isGuest:!0,guestId:f(e.cookies)||void 0};throw o}}function re(e,t={}){const{cookieName:n="logto_authtoken",maxAge:o=10080*60,domain:r,path:i="/",sameSite:s="Strict"}=t;let a=`${n}=${e}; Max-Age=${o}; Path=${i}; HttpOnly; Secure; SameSite=${s}`;return r&&(a+=`; Domain=${r}`),a}const k="logto_csrf_token",v="x-csrf-token",ne=new Set(["GET","HEAD","OPTIONS","TRACE"]);function N(){return O.randomUUID()}function M(e,t={}){const{cookieName:n=k,maxAge:o=1440*60,domain:r,path:i="/",sameSite:s="Strict"}=t;if(/[\r\n;]/.test(e))throw new Error("CSRF token contains invalid characters (newline or semicolon)");let a=`${n}=${e}; Max-Age=${o}; Path=${i}; Secure; SameSite=${s}`;return r&&(a+=`; Domain=${r}`),a}function oe(e={}){const{cookieName:t=k,headerName:n=v,domain:o,path:r="/",sameSite:i="Strict",maxAge:s=1440*60}=e;return(a,u,c)=>{var T,C,I;const d=(a.method??"GET").toUpperCase();if(ne.has(d)){if(!((T=a.cookies)!=null&&T[t])){const U=N(),H=M(U,{cookieName:t,maxAge:s,domain:o,path:r,sameSite:i});typeof u.setHeader=="function"&&u.setHeader("Set-Cookie",H)}return c()}const p=(C=a.cookies)==null?void 0:C[t],h=(I=a.headers)==null?void 0:I[n],y=Array.isArray(h)?h[0]:h;if(!p||!y||p!==y){u.status(403).json({error:"CSRF validation failed",message:`Include the '${n}' request header with the value from the '${t}' cookie.`});return}return c()}}function ie(e,t={}){var s,a;const{cookieName:n=k,headerName:o=v}=t,r=(a=(s=e.cookies)==null?void 0:s.get(n))==null?void 0:a.value,i=e.headers.get(o);return r?i?r!==i?{valid:!1,error:"CSRF token mismatch. The header value does not match the cookie value."}:{valid:!0}:{valid:!1,error:`Missing '${o}' request header. Read the '${n}' cookie value and send it in this header.`}:{valid:!1,error:`CSRF cookie '${n}' not found. Ensure GET requests are made first to receive the CSRF cookie.`}}const R=["roles","role"];function se(e){return typeof e=="object"&&e!==null&&"isAuthenticated"in e}function j(e){return e?se(e)?e.payload:e:null}function S(e){return(Array.isArray(e)?e:[e]).flatMap(t=>t.split(/\s+/)).map(t=>t.trim()).filter(Boolean)}function g(e){return(Array.isArray(e)?e:[e]).flatMap(t=>t.split(/[,\s]+/)).map(t=>t.trim()).filter(Boolean)}function W(e){const t=j(e);return t!=null&&t.scope?S(t.scope):[]}function G(e,t){const n=j(e);if(!n)return[];for(const o of t){const r=n[o];if(Array.isArray(r)&&r.every(i=>typeof i=="string")||typeof r=="string")return g(r)}return[]}function F(e,t,n={}){const o=S(t);if(o.length===0)return!0;const r=W(e),{mode:i="all"}=n;return i==="any"?o.some(s=>r.includes(s)):o.every(s=>r.includes(s))}function ae(e,t,n={}){const o=S(t);if(o.length===0)return;const r=W(e),{mode:i="all"}=n;if(F(e,o,{mode:i}))return;const s=o.filter(u=>!r.includes(u)),a=i==="any"?"at least one of":"all of";throw new Error(`Missing required scopes (${a}: ${o.join(", ")}). Token scopes: ${r.join(", ")||"(none)"}. Missing: ${s.join(", ")||o.join(", ")}`)}function ue(e,t,n={}){var s;const o=g(t);if(o.length===0)return!0;const r=(s=n.claimKeys)!=null&&s.length?n.claimKeys:R,i=G(e,r);return o.every(a=>i.includes(a))}function ce(e,t,n={}){var s;const o=g(t);if(o.length===0)return;const r=(s=n.claimKeys)!=null&&s.length?n.claimKeys:R,i=G(e,r);if(!o.every(a=>i.includes(a)))throw new Error(`Missing required role: ${o.join(", ")}. Checked claims: ${r.join(", ")}. Token roles: ${i.join(", ")||"(none)"}`)}exports.CSRF_COOKIE_NAME=k;exports.CSRF_HEADER_NAME=v;exports.buildAuthCookieHeader=re;exports.buildCsrfCookieHeader=M;exports.clearJwksCache=V;exports.createCsrfMiddleware=oe;exports.createExpressAuthMiddleware=Z;exports.generateCsrfToken=N;exports.hasRole=ue;exports.hasScopes=F;exports.invalidateJwksCache=B;exports.requireRole=ce;exports.requireScopes=ae;exports.verifyAuth=te;exports.verifyCsrfToken=ie;exports.verifyLogtoToken=w;exports.verifyNextAuth=ee;
|