@simple-login/sdk 1.2.0 → 1.4.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 +90 -14
- package/dist/index.cjs +23 -16
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +9 -9
- package/dist/index.d.ts +9 -9
- package/dist/index.js +23 -16
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -23,13 +23,14 @@ Set these environment variables and the SDK will auto-detect them:
|
|
|
23
23
|
```bash
|
|
24
24
|
SIMPLELOGIN_CLIENT_ID=your-client-id
|
|
25
25
|
SIMPLELOGIN_CLIENT_SECRET=your-client-secret
|
|
26
|
-
SIMPLELOGIN_REDIRECT_URI=https://your-app.com/callback
|
|
26
|
+
SIMPLELOGIN_REDIRECT_URI=https://your-app.com/auth/callback
|
|
27
|
+
SIMPLELOGIN_ORIGIN=https://your-app.com
|
|
27
28
|
```
|
|
28
29
|
|
|
29
30
|
```typescript
|
|
30
31
|
import { SimpleLogin } from '@simple-login/sdk'
|
|
31
32
|
|
|
32
|
-
const
|
|
33
|
+
const simpleLogin = new SimpleLogin()
|
|
33
34
|
```
|
|
34
35
|
|
|
35
36
|
### Explicit configuration
|
|
@@ -37,27 +38,102 @@ const client = new SimpleLogin()
|
|
|
37
38
|
You can also pass the config directly (this overrides environment variables):
|
|
38
39
|
|
|
39
40
|
```typescript
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
const client = new SimpleLogin({
|
|
41
|
+
const simpleLogin = new SimpleLogin({
|
|
43
42
|
clientId: 'your-client-id',
|
|
44
43
|
clientSecret: 'your-client-secret',
|
|
45
|
-
redirectUri: 'https://your-app.com/callback',
|
|
44
|
+
redirectUri: 'https://your-app.com/auth/callback',
|
|
45
|
+
origin: 'https://your-app.com',
|
|
46
46
|
})
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Quick Start (All-in-One Flow)
|
|
50
|
+
|
|
51
|
+
The SDK provides a batteries-included flow that handles cookies, PKCE, state verification, and token refresh automatically.
|
|
52
|
+
|
|
53
|
+
### 1. Login Route
|
|
54
|
+
|
|
55
|
+
```typescript
|
|
56
|
+
// GET /auth/login
|
|
57
|
+
export async function loader() {
|
|
58
|
+
return simpleLogin.redirectToAuth()
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### 2. Callback Route
|
|
63
|
+
|
|
64
|
+
```typescript
|
|
65
|
+
// GET /auth/callback
|
|
66
|
+
export async function loader({ request }) {
|
|
67
|
+
const { response, user } = await simpleLogin.handleCallback(request, '/dashboard')
|
|
68
|
+
|
|
69
|
+
// Store user in your database if needed
|
|
70
|
+
await db.users.upsert({
|
|
71
|
+
id: user.id,
|
|
72
|
+
email: user.email,
|
|
73
|
+
name: user.name,
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
return response
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### 3. Protected Routes
|
|
81
|
+
|
|
82
|
+
```typescript
|
|
83
|
+
// GET /dashboard
|
|
84
|
+
export async function loader({ request }) {
|
|
85
|
+
const auth = await simpleLogin.authenticate(request)
|
|
47
86
|
|
|
48
|
-
|
|
49
|
-
|
|
87
|
+
if (!auth) {
|
|
88
|
+
return simpleLogin.redirectToAuth()
|
|
89
|
+
}
|
|
50
90
|
|
|
51
|
-
//
|
|
52
|
-
const
|
|
91
|
+
// auth.claims contains decoded JWT claims (sub, application_id, etc.)
|
|
92
|
+
const user = await db.users.findById(auth.claims.sub)
|
|
53
93
|
|
|
54
|
-
//
|
|
55
|
-
|
|
94
|
+
// auth.headers contains Set-Cookie if tokens were refreshed
|
|
95
|
+
return json({ user }, { headers: auth.headers })
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### 4. Logout Route
|
|
56
100
|
|
|
57
|
-
|
|
58
|
-
|
|
101
|
+
```typescript
|
|
102
|
+
// POST /auth/logout
|
|
103
|
+
export async function action({ request }) {
|
|
104
|
+
return simpleLogin.logout(request)
|
|
105
|
+
}
|
|
59
106
|
```
|
|
60
107
|
|
|
108
|
+
## Security Features
|
|
109
|
+
|
|
110
|
+
### PKCE (Proof Key for Code Exchange)
|
|
111
|
+
|
|
112
|
+
PKCE is automatically enabled for all authorization requests. The SDK generates a cryptographically secure code verifier and challenge, stores the verifier in an HttpOnly cookie, and includes it in the token exchange.
|
|
113
|
+
|
|
114
|
+
### State Parameter
|
|
115
|
+
|
|
116
|
+
A cryptographically secure state parameter is automatically generated (or you can provide your own) and verified on callback to prevent CSRF attacks.
|
|
117
|
+
|
|
118
|
+
### CSRF Protection for Mutations
|
|
119
|
+
|
|
120
|
+
The `authenticate()` method automatically checks the `Origin` or `Referer` header for non-GET requests against your configured `origin`. Returns `null` if the origin doesn't match, preventing CSRF attacks.
|
|
121
|
+
|
|
122
|
+
**Important:** You must set `SIMPLELOGIN_ORIGIN` environment variable (or pass `origin` in config) for CSRF protection to work.
|
|
123
|
+
|
|
124
|
+
### Token Refresh
|
|
125
|
+
|
|
126
|
+
The `authenticate()` method automatically refreshes expired access tokens using the refresh token. When tokens are refreshed, the new cookies are included in `auth.headers` - just pass them to your response.
|
|
127
|
+
|
|
128
|
+
### Secure Cookie Defaults
|
|
129
|
+
|
|
130
|
+
All cookies are set with secure defaults:
|
|
131
|
+
|
|
132
|
+
- `HttpOnly` - Not accessible via JavaScript
|
|
133
|
+
- `Secure` - Only sent over HTTPS
|
|
134
|
+
- `SameSite=Lax` - CSRF protection
|
|
135
|
+
- `Path=/` - Available site-wide
|
|
136
|
+
|
|
61
137
|
## License
|
|
62
138
|
|
|
63
139
|
MIT
|
package/dist/index.cjs
CHANGED
|
@@ -98,11 +98,13 @@ var SimpleLogin = class {
|
|
|
98
98
|
clientId;
|
|
99
99
|
clientSecret;
|
|
100
100
|
redirectUri;
|
|
101
|
+
origin;
|
|
101
102
|
baseUrl;
|
|
102
103
|
constructor(config = {}) {
|
|
103
104
|
this.clientId = config.clientId ?? getEnv("SIMPLELOGIN_CLIENT_ID") ?? "";
|
|
104
105
|
this.clientSecret = config.clientSecret ?? getEnv("SIMPLELOGIN_CLIENT_SECRET") ?? "";
|
|
105
106
|
this.redirectUri = config.redirectUri ?? getEnv("SIMPLELOGIN_REDIRECT_URI") ?? "";
|
|
107
|
+
this.origin = config.origin ?? getEnv("SIMPLELOGIN_ORIGIN") ?? "";
|
|
106
108
|
this.baseUrl = BASE_URL;
|
|
107
109
|
if (!this.clientId) {
|
|
108
110
|
throw new Error(
|
|
@@ -119,6 +121,9 @@ var SimpleLogin = class {
|
|
|
119
121
|
"redirectUri is required. Pass it in config or set SIMPLELOGIN_REDIRECT_URI env var."
|
|
120
122
|
);
|
|
121
123
|
}
|
|
124
|
+
if (!this.origin) {
|
|
125
|
+
throw new Error("origin is required. Pass it in config or set SIMPLELOGIN_ORIGIN env var.");
|
|
126
|
+
}
|
|
122
127
|
}
|
|
123
128
|
/**
|
|
124
129
|
* Returns a Response that redirects to the authorization URL with state and PKCE cookies set.
|
|
@@ -286,7 +291,8 @@ var SimpleLogin = class {
|
|
|
286
291
|
if (!response.ok) {
|
|
287
292
|
throw new AuthorizationError("Failed to fetch public key");
|
|
288
293
|
}
|
|
289
|
-
const
|
|
294
|
+
const data = await response.json();
|
|
295
|
+
const pem = data["public_key"];
|
|
290
296
|
const key = await (0, import_jose.importSPKI)(pem, "RS256");
|
|
291
297
|
publicKeyCache.set(this.clientId, { key, fetchedAt: Date.now() });
|
|
292
298
|
return key;
|
|
@@ -294,15 +300,15 @@ var SimpleLogin = class {
|
|
|
294
300
|
/**
|
|
295
301
|
* Authenticate a request by verifying the access token from cookies.
|
|
296
302
|
* No network calls except when refreshing expired tokens.
|
|
303
|
+
* CSRF protection is automatic using the origin from config.
|
|
297
304
|
* @param request - The incoming Request object
|
|
298
|
-
* @param options - Optional settings for CSRF protection
|
|
299
305
|
* @returns AuthResult if authenticated, null if not authenticated
|
|
300
306
|
*/
|
|
301
|
-
async authenticate(request
|
|
307
|
+
async authenticate(request) {
|
|
302
308
|
const method = request.method.toUpperCase();
|
|
303
|
-
if (method !== "GET" && method !== "HEAD"
|
|
304
|
-
const
|
|
305
|
-
if (!
|
|
309
|
+
if (method !== "GET" && method !== "HEAD") {
|
|
310
|
+
const requestOrigin = request.headers.get("origin") || request.headers.get("referer");
|
|
311
|
+
if (!requestOrigin?.startsWith(this.origin)) {
|
|
306
312
|
return null;
|
|
307
313
|
}
|
|
308
314
|
}
|
|
@@ -316,7 +322,7 @@ var SimpleLogin = class {
|
|
|
316
322
|
try {
|
|
317
323
|
await (0, import_jose.jwtVerify)(accessToken, publicKey);
|
|
318
324
|
const claims = (0, import_jose.decodeJwt)(accessToken);
|
|
319
|
-
return { claims, accessToken };
|
|
325
|
+
return { claims, accessToken, headers: new Headers() };
|
|
320
326
|
} catch (error) {
|
|
321
327
|
const isExpired = error instanceof Error && "code" in error && error.code === "ERR_JWT_EXPIRED";
|
|
322
328
|
if (!isExpired || !refreshTokenValue) {
|
|
@@ -325,8 +331,11 @@ var SimpleLogin = class {
|
|
|
325
331
|
try {
|
|
326
332
|
const tokens = await this.refreshToken(refreshTokenValue);
|
|
327
333
|
const claims = (0, import_jose.decodeJwt)(tokens.access_token);
|
|
328
|
-
const
|
|
329
|
-
|
|
334
|
+
const headers = new Headers();
|
|
335
|
+
for (const cookie of createTokenCookies(tokens)) {
|
|
336
|
+
headers.append("Set-Cookie", cookie);
|
|
337
|
+
}
|
|
338
|
+
return { claims, accessToken: tokens.access_token, headers };
|
|
330
339
|
} catch {
|
|
331
340
|
return null;
|
|
332
341
|
}
|
|
@@ -347,21 +356,19 @@ var SimpleLogin = class {
|
|
|
347
356
|
});
|
|
348
357
|
}
|
|
349
358
|
/**
|
|
350
|
-
* Logout: revoke refresh token on IdP and
|
|
359
|
+
* Logout: revoke refresh token on IdP, clear cookies, and redirect to auth.
|
|
351
360
|
* @param request - The incoming Request object (to read refresh token from cookies)
|
|
352
|
-
* @param redirectTo - URL to redirect to after logout (default: '/')
|
|
353
361
|
*/
|
|
354
|
-
async logout(request
|
|
362
|
+
async logout(request) {
|
|
355
363
|
const cookieHeader = request.headers.get("cookie") ?? "";
|
|
356
364
|
const refreshToken = parseCookie(cookieHeader, "SIMPLELOGIN_REFRESH_TOKEN");
|
|
357
365
|
if (refreshToken) {
|
|
358
366
|
this.revokeToken(refreshToken).catch(() => {
|
|
359
367
|
});
|
|
360
368
|
}
|
|
361
|
-
const
|
|
362
|
-
const headers = new Headers();
|
|
363
|
-
|
|
364
|
-
for (const cookie of cookies) {
|
|
369
|
+
const authResponse = await this.redirectToAuth();
|
|
370
|
+
const headers = new Headers(authResponse.headers);
|
|
371
|
+
for (const cookie of clearTokenCookies()) {
|
|
365
372
|
headers.append("Set-Cookie", cookie);
|
|
366
373
|
}
|
|
367
374
|
return new Response(null, { status: 302, headers });
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/client.ts","../src/errors.ts"],"sourcesContent":["export { SimpleLogin } from './client'\nexport { AuthorizationError, SousaError, TokenError } from './errors'\nexport type {\n AccessTokenClaims,\n AuthResult,\n AuthorizationUrlOptions,\n AuthorizationUrlResult,\n CallbackResult,\n SimpleLoginConfig,\n TokenResponse,\n UserInfo,\n} from './types'\n","import { decodeJwt, importSPKI, jwtVerify } from 'jose'\nimport { AuthorizationError, TokenError } from './errors'\nimport type {\n AccessTokenClaims,\n AuthResult,\n AuthorizationUrlOptions,\n AuthorizationUrlResult,\n CallbackResult,\n SimpleLoginConfig,\n TokenResponse,\n UserInfo,\n} from './types'\n\ndeclare const __SIMPLELOGIN_BASE_URL__: string\n\nconst BASE_URL = __SIMPLELOGIN_BASE_URL__\n\nfunction generateState(): string {\n const array = new Uint8Array(32)\n crypto.getRandomValues(array)\n return Array.from(array, (b) => b.toString(16).padStart(2, '0')).join('')\n}\n\nfunction generateCodeVerifier(): string {\n const array = new Uint8Array(32)\n crypto.getRandomValues(array)\n // Base64url encode (RFC 7636)\n return btoa(String.fromCharCode(...array))\n .replace(/\\+/g, '-')\n .replace(/\\//g, '_')\n .replace(/=/g, '')\n}\n\nasync function generateCodeChallenge(verifier: string): Promise<string> {\n const encoder = new TextEncoder()\n const data = encoder.encode(verifier)\n const hash = await crypto.subtle.digest('SHA-256', data)\n // Base64url encode the hash\n return btoa(String.fromCharCode(...new Uint8Array(hash)))\n .replace(/\\+/g, '-')\n .replace(/\\//g, '_')\n .replace(/=/g, '')\n}\n\nfunction getEnv(key: string): string | undefined {\n if (typeof process !== 'undefined' && process.env) {\n return process.env[key]\n }\n return undefined\n}\n\nfunction parseCookie(cookieHeader: string, name: string): string | undefined {\n const match = cookieHeader.match(new RegExp(`(?:^|;\\\\s*)${name}=([^;]*)`))\n return match ? decodeURIComponent(match[1]) : undefined\n}\n\n// Public key cache: clientId -> { key, fetchedAt }\nconst publicKeyCache = new Map<string, { key: CryptoKey; fetchedAt: number }>()\nconst PUBLIC_KEY_CACHE_TTL = 60 * 60 * 1000 // 1 hour\n\nfunction createTokenCookies(tokens: TokenResponse): string[] {\n return [\n `SIMPLELOGIN_ACCESS_TOKEN=${tokens.access_token}; HttpOnly; Secure; SameSite=Lax; Path=/`,\n `SIMPLELOGIN_REFRESH_TOKEN=${tokens.refresh_token}; HttpOnly; Secure; SameSite=Lax; Path=/`,\n ]\n}\n\nfunction clearTokenCookies(): string[] {\n return [\n 'SIMPLELOGIN_ACCESS_TOKEN=; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=0',\n 'SIMPLELOGIN_REFRESH_TOKEN=; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=0',\n ]\n}\n\nexport class SimpleLogin {\n private clientId: string\n private clientSecret: string\n private redirectUri: string\n private baseUrl: string\n\n constructor(config: SimpleLoginConfig = {}) {\n this.clientId = config.clientId ?? getEnv('SIMPLELOGIN_CLIENT_ID') ?? ''\n this.clientSecret = config.clientSecret ?? getEnv('SIMPLELOGIN_CLIENT_SECRET') ?? ''\n this.redirectUri = config.redirectUri ?? getEnv('SIMPLELOGIN_REDIRECT_URI') ?? ''\n this.baseUrl = BASE_URL\n\n if (!this.clientId) {\n throw new Error(\n 'clientId is required. Pass it in config or set SIMPLELOGIN_CLIENT_ID env var.',\n )\n }\n if (!this.clientSecret) {\n throw new Error(\n 'clientSecret is required. Pass it in config or set SIMPLELOGIN_CLIENT_SECRET env var.',\n )\n }\n if (!this.redirectUri) {\n throw new Error(\n 'redirectUri is required. Pass it in config or set SIMPLELOGIN_REDIRECT_URI env var.',\n )\n }\n }\n\n /**\n * Returns a Response that redirects to the authorization URL with state and PKCE cookies set.\n * @param options - Optional scopes or custom state\n * @returns A 302 redirect Response ready to be returned from your route handler\n */\n async redirectToAuth(options: AuthorizationUrlOptions = {}): Promise<Response> {\n const { url, cookies } = await this.getAuthorizationUrl(options)\n const headers = new Headers()\n headers.set('Location', url)\n for (const cookie of cookies) {\n headers.append('Set-Cookie', cookie)\n }\n return new Response(null, { status: 302, headers })\n }\n\n /**\n * Generate the authorization URL to redirect users for sign-in\n * @returns The authorization URL, state, code verifier, and ready-to-use Set-Cookie header values\n */\n async getAuthorizationUrl(options: AuthorizationUrlOptions = {}): Promise<AuthorizationUrlResult> {\n const state = options.state ?? generateState()\n const codeVerifier = generateCodeVerifier()\n const codeChallenge = await generateCodeChallenge(codeVerifier)\n\n const params = new URLSearchParams({\n client_id: this.clientId,\n redirect_uri: this.redirectUri,\n response_type: 'code',\n state,\n code_challenge: codeChallenge,\n code_challenge_method: 'S256',\n })\n\n if (options.scopes?.length) {\n params.set('scope', options.scopes.join(' '))\n }\n\n const cookies = [\n `SIMPLELOGIN_STATE=${state}; HttpOnly; Secure; SameSite=Lax; Max-Age=600; Path=/`,\n `SIMPLELOGIN_CODE_VERIFIER=${codeVerifier}; HttpOnly; Secure; SameSite=Lax; Max-Age=600; Path=/`,\n ]\n\n return {\n url: `${this.baseUrl}/v1/auth/authorize?${params.toString()}`,\n state,\n codeVerifier,\n cookies,\n }\n }\n\n /**\n * Handle the OAuth callback: verify state, exchange code, fetch user info, and return a redirect Response.\n * @param request - The incoming Request object from the callback\n * @param redirectTo - URL to redirect to after successful authentication (default: '/')\n * @returns CallbackResult with redirect Response (cookies set) and user info to store\n * @throws AuthorizationError if state is missing or doesn't match\n */\n async handleCallback(request: Request, redirectTo: string = '/'): Promise<CallbackResult> {\n const url = new URL(request.url)\n const code = url.searchParams.get('code')\n const state = url.searchParams.get('state')\n\n if (!code) {\n throw new AuthorizationError('Missing authorization code in callback')\n }\n\n if (!state) {\n throw new AuthorizationError('Missing state parameter in callback')\n }\n\n const cookieHeader = request.headers.get('cookie') ?? ''\n const expectedState = parseCookie(cookieHeader, 'SIMPLELOGIN_STATE')\n const codeVerifier = parseCookie(cookieHeader, 'SIMPLELOGIN_CODE_VERIFIER')\n\n if (!expectedState) {\n throw new AuthorizationError('Missing SIMPLELOGIN_STATE cookie')\n }\n\n if (state !== expectedState) {\n throw new AuthorizationError('Invalid state parameter')\n }\n\n if (!codeVerifier) {\n throw new AuthorizationError('Missing SIMPLELOGIN_CODE_VERIFIER cookie')\n }\n\n // Exchange code for tokens (with PKCE code_verifier)\n const tokens = await this.exchangeCode(code, codeVerifier)\n\n // Fetch user info (only time we do this)\n const user = await this.getUserInfo(tokens.access_token)\n\n // Build redirect response with token cookies\n const cookies = createTokenCookies(tokens)\n const headers = new Headers()\n headers.set('Location', redirectTo)\n for (const cookie of cookies) {\n headers.append('Set-Cookie', cookie)\n }\n // Clear the state and code verifier cookies\n headers.append(\n 'Set-Cookie',\n 'SIMPLELOGIN_STATE=; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=0',\n )\n headers.append(\n 'Set-Cookie',\n 'SIMPLELOGIN_CODE_VERIFIER=; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=0',\n )\n\n const response = new Response(null, { status: 302, headers })\n\n return { response, user }\n }\n\n /**\n * Exchange an authorization code for access and refresh tokens\n * @param code - The authorization code from the callback\n * @param codeVerifier - The PKCE code verifier\n */\n async exchangeCode(code: string, codeVerifier: string): Promise<TokenResponse> {\n const response = await fetch(`${this.baseUrl}/v1/auth/token`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/x-www-form-urlencoded',\n },\n body: new URLSearchParams({\n grant_type: 'authorization_code',\n client_id: this.clientId,\n client_secret: this.clientSecret,\n redirect_uri: this.redirectUri,\n code,\n code_verifier: codeVerifier,\n }),\n })\n\n if (!response.ok) {\n const error = await response.json().catch(() => ({}))\n throw new TokenError(error.error_description || 'Failed to exchange authorization code')\n }\n\n return response.json()\n }\n\n /**\n * Refresh an access token using a refresh token\n */\n async refreshToken(refreshToken: string): Promise<TokenResponse> {\n const response = await fetch(`${this.baseUrl}/v1/auth/token`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/x-www-form-urlencoded',\n },\n body: new URLSearchParams({\n grant_type: 'refresh_token',\n client_id: this.clientId,\n client_secret: this.clientSecret,\n refresh_token: refreshToken,\n }),\n })\n\n if (!response.ok) {\n const error = await response.json().catch(() => ({}))\n throw new TokenError(error.error_description || 'Failed to refresh token')\n }\n\n return response.json()\n }\n\n /**\n * Get user information using an access token\n */\n async getUserInfo(accessToken: string): Promise<UserInfo> {\n const response = await fetch(`${this.baseUrl}/v1/auth/userinfo`, {\n headers: {\n Authorization: `Bearer ${accessToken}`,\n },\n })\n\n if (!response.ok) {\n throw new AuthorizationError('Failed to fetch user info')\n }\n\n return response.json()\n }\n\n /**\n * Fetch and cache the public key for JWT verification\n */\n private async getPublicKey(): Promise<CryptoKey> {\n const cached = publicKeyCache.get(this.clientId)\n if (cached && Date.now() - cached.fetchedAt < PUBLIC_KEY_CACHE_TTL) {\n return cached.key\n }\n\n const response = await fetch(`${this.baseUrl}/api/applications/${this.clientId}/public-key`)\n if (!response.ok) {\n throw new AuthorizationError('Failed to fetch public key')\n }\n\n const pem = await response.text()\n const key = await importSPKI(pem, 'RS256')\n\n publicKeyCache.set(this.clientId, { key, fetchedAt: Date.now() })\n return key\n }\n\n /**\n * Authenticate a request by verifying the access token from cookies.\n * No network calls except when refreshing expired tokens.\n * @param request - The incoming Request object\n * @param options - Optional settings for CSRF protection\n * @returns AuthResult if authenticated, null if not authenticated\n */\n async authenticate(\n request: Request,\n options?: { allowedOrigin?: string },\n ): Promise<AuthResult | null> {\n // CSRF protection for state-changing requests\n const method = request.method.toUpperCase()\n if (method !== 'GET' && method !== 'HEAD' && options?.allowedOrigin) {\n const origin = request.headers.get('origin') || request.headers.get('referer')\n if (!origin?.startsWith(options.allowedOrigin)) {\n return null\n }\n }\n\n const cookieHeader = request.headers.get('cookie') ?? ''\n const accessToken = parseCookie(cookieHeader, 'SIMPLELOGIN_ACCESS_TOKEN')\n const refreshTokenValue = parseCookie(cookieHeader, 'SIMPLELOGIN_REFRESH_TOKEN')\n\n if (!accessToken) {\n return null\n }\n\n const publicKey = await this.getPublicKey()\n\n try {\n // Verify the token (local, no network call)\n await jwtVerify(accessToken, publicKey)\n\n // Decode claims from JWT (no network call)\n const claims = decodeJwt(accessToken) as AccessTokenClaims\n\n return { claims, accessToken }\n } catch (error) {\n // Check if token is expired\n const isExpired =\n error instanceof Error &&\n 'code' in error &&\n (error as { code: string }).code === 'ERR_JWT_EXPIRED'\n\n if (!isExpired || !refreshTokenValue) {\n return null\n }\n\n // Token expired, try to refresh\n try {\n const tokens = await this.refreshToken(refreshTokenValue)\n const claims = decodeJwt(tokens.access_token) as AccessTokenClaims\n const cookies = createTokenCookies(tokens)\n\n return { claims, accessToken: tokens.access_token, cookies }\n } catch {\n return null\n }\n }\n }\n\n /**\n * Revoke a refresh token on the IdP\n */\n private async revokeToken(refreshToken: string): Promise<void> {\n await fetch(`${this.baseUrl}/v1/auth/revoke`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/x-www-form-urlencoded' },\n body: new URLSearchParams({\n token: refreshToken,\n client_id: this.clientId,\n client_secret: this.clientSecret,\n }),\n })\n }\n\n /**\n * Logout: revoke refresh token on IdP and clear cookies.\n * @param request - The incoming Request object (to read refresh token from cookies)\n * @param redirectTo - URL to redirect to after logout (default: '/')\n */\n async logout(request: Request, redirectTo: string = '/'): Promise<Response> {\n const cookieHeader = request.headers.get('cookie') ?? ''\n const refreshToken = parseCookie(cookieHeader, 'SIMPLELOGIN_REFRESH_TOKEN')\n\n // Revoke token on IdP (fire and forget - don't block logout on failure)\n if (refreshToken) {\n this.revokeToken(refreshToken).catch(() => {})\n }\n\n // Clear cookies regardless\n const cookies = clearTokenCookies()\n const headers = new Headers()\n headers.set('Location', redirectTo)\n for (const cookie of cookies) {\n headers.append('Set-Cookie', cookie)\n }\n return new Response(null, { status: 302, headers })\n }\n}\n","export class SousaError extends Error {\n constructor(\n message: string,\n public code: string,\n public statusCode?: number\n ) {\n super(message);\n this.name = \"SousaError\";\n }\n}\n\nexport class AuthorizationError extends SousaError {\n constructor(message: string) {\n super(message, \"AUTHORIZATION_ERROR\", 401);\n this.name = \"AuthorizationError\";\n }\n}\n\nexport class TokenError extends SousaError {\n constructor(message: string) {\n super(message, \"TOKEN_ERROR\", 400);\n this.name = \"TokenError\";\n }\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/client.ts","../src/errors.ts"],"sourcesContent":["export { SimpleLogin } from './client'\nexport { AuthorizationError, SousaError, TokenError } from './errors'\nexport type {\n AccessTokenClaims,\n AuthResult,\n AuthorizationUrlOptions,\n AuthorizationUrlResult,\n CallbackResult,\n SimpleLoginConfig,\n TokenResponse,\n UserInfo,\n} from './types'\n","import { decodeJwt, importSPKI, jwtVerify } from 'jose'\nimport { AuthorizationError, TokenError } from './errors'\nimport type {\n AccessTokenClaims,\n AuthResult,\n AuthorizationUrlOptions,\n AuthorizationUrlResult,\n CallbackResult,\n SimpleLoginConfig,\n TokenResponse,\n UserInfo,\n} from './types'\n\ndeclare const __SIMPLELOGIN_BASE_URL__: string\n\nconst BASE_URL = __SIMPLELOGIN_BASE_URL__\n\nfunction generateState(): string {\n const array = new Uint8Array(32)\n crypto.getRandomValues(array)\n return Array.from(array, (b) => b.toString(16).padStart(2, '0')).join('')\n}\n\nfunction generateCodeVerifier(): string {\n const array = new Uint8Array(32)\n crypto.getRandomValues(array)\n // Base64url encode (RFC 7636)\n return btoa(String.fromCharCode(...array))\n .replace(/\\+/g, '-')\n .replace(/\\//g, '_')\n .replace(/=/g, '')\n}\n\nasync function generateCodeChallenge(verifier: string): Promise<string> {\n const encoder = new TextEncoder()\n const data = encoder.encode(verifier)\n const hash = await crypto.subtle.digest('SHA-256', data)\n // Base64url encode the hash\n return btoa(String.fromCharCode(...new Uint8Array(hash)))\n .replace(/\\+/g, '-')\n .replace(/\\//g, '_')\n .replace(/=/g, '')\n}\n\nfunction getEnv(key: string): string | undefined {\n if (typeof process !== 'undefined' && process.env) {\n return process.env[key]\n }\n return undefined\n}\n\nfunction parseCookie(cookieHeader: string, name: string): string | undefined {\n const match = cookieHeader.match(new RegExp(`(?:^|;\\\\s*)${name}=([^;]*)`))\n return match ? decodeURIComponent(match[1]) : undefined\n}\n\n// Public key cache: clientId -> { key, fetchedAt }\nconst publicKeyCache = new Map<string, { key: CryptoKey; fetchedAt: number }>()\nconst PUBLIC_KEY_CACHE_TTL = 60 * 60 * 1000 // 1 hour\n\nfunction createTokenCookies(tokens: TokenResponse): string[] {\n return [\n `SIMPLELOGIN_ACCESS_TOKEN=${tokens.access_token}; HttpOnly; Secure; SameSite=Lax; Path=/`,\n `SIMPLELOGIN_REFRESH_TOKEN=${tokens.refresh_token}; HttpOnly; Secure; SameSite=Lax; Path=/`,\n ]\n}\n\nfunction clearTokenCookies(): string[] {\n return [\n 'SIMPLELOGIN_ACCESS_TOKEN=; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=0',\n 'SIMPLELOGIN_REFRESH_TOKEN=; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=0',\n ]\n}\n\nexport class SimpleLogin {\n private clientId: string\n private clientSecret: string\n private redirectUri: string\n private origin: string\n private baseUrl: string\n\n constructor(config: SimpleLoginConfig = {}) {\n this.clientId = config.clientId ?? getEnv('SIMPLELOGIN_CLIENT_ID') ?? ''\n this.clientSecret = config.clientSecret ?? getEnv('SIMPLELOGIN_CLIENT_SECRET') ?? ''\n this.redirectUri = config.redirectUri ?? getEnv('SIMPLELOGIN_REDIRECT_URI') ?? ''\n this.origin = config.origin ?? getEnv('SIMPLELOGIN_ORIGIN') ?? ''\n this.baseUrl = BASE_URL\n\n if (!this.clientId) {\n throw new Error(\n 'clientId is required. Pass it in config or set SIMPLELOGIN_CLIENT_ID env var.',\n )\n }\n if (!this.clientSecret) {\n throw new Error(\n 'clientSecret is required. Pass it in config or set SIMPLELOGIN_CLIENT_SECRET env var.',\n )\n }\n if (!this.redirectUri) {\n throw new Error(\n 'redirectUri is required. Pass it in config or set SIMPLELOGIN_REDIRECT_URI env var.',\n )\n }\n if (!this.origin) {\n throw new Error('origin is required. Pass it in config or set SIMPLELOGIN_ORIGIN env var.')\n }\n }\n\n /**\n * Returns a Response that redirects to the authorization URL with state and PKCE cookies set.\n * @param options - Optional scopes or custom state\n * @returns A 302 redirect Response ready to be returned from your route handler\n */\n async redirectToAuth(options: AuthorizationUrlOptions = {}): Promise<Response> {\n const { url, cookies } = await this.getAuthorizationUrl(options)\n const headers = new Headers()\n headers.set('Location', url)\n for (const cookie of cookies) {\n headers.append('Set-Cookie', cookie)\n }\n return new Response(null, { status: 302, headers })\n }\n\n /**\n * Generate the authorization URL to redirect users for sign-in\n * @returns The authorization URL, state, code verifier, and ready-to-use Set-Cookie header values\n */\n async getAuthorizationUrl(\n options: AuthorizationUrlOptions = {},\n ): Promise<AuthorizationUrlResult> {\n const state = options.state ?? generateState()\n const codeVerifier = generateCodeVerifier()\n const codeChallenge = await generateCodeChallenge(codeVerifier)\n\n const params = new URLSearchParams({\n client_id: this.clientId,\n redirect_uri: this.redirectUri,\n response_type: 'code',\n state,\n code_challenge: codeChallenge,\n code_challenge_method: 'S256',\n })\n\n if (options.scopes?.length) {\n params.set('scope', options.scopes.join(' '))\n }\n\n const cookies = [\n `SIMPLELOGIN_STATE=${state}; HttpOnly; Secure; SameSite=Lax; Max-Age=600; Path=/`,\n `SIMPLELOGIN_CODE_VERIFIER=${codeVerifier}; HttpOnly; Secure; SameSite=Lax; Max-Age=600; Path=/`,\n ]\n\n return {\n url: `${this.baseUrl}/v1/auth/authorize?${params.toString()}`,\n state,\n codeVerifier,\n cookies,\n }\n }\n\n /**\n * Handle the OAuth callback: verify state, exchange code, fetch user info, and return a redirect Response.\n * @param request - The incoming Request object from the callback\n * @param redirectTo - URL to redirect to after successful authentication (default: '/')\n * @returns CallbackResult with redirect Response (cookies set) and user info to store\n * @throws AuthorizationError if state is missing or doesn't match\n */\n async handleCallback(request: Request, redirectTo: string = '/'): Promise<CallbackResult> {\n const url = new URL(request.url)\n const code = url.searchParams.get('code')\n const state = url.searchParams.get('state')\n\n if (!code) {\n throw new AuthorizationError('Missing authorization code in callback')\n }\n\n if (!state) {\n throw new AuthorizationError('Missing state parameter in callback')\n }\n\n const cookieHeader = request.headers.get('cookie') ?? ''\n const expectedState = parseCookie(cookieHeader, 'SIMPLELOGIN_STATE')\n const codeVerifier = parseCookie(cookieHeader, 'SIMPLELOGIN_CODE_VERIFIER')\n\n if (!expectedState) {\n throw new AuthorizationError('Missing SIMPLELOGIN_STATE cookie')\n }\n\n if (state !== expectedState) {\n throw new AuthorizationError('Invalid state parameter')\n }\n\n if (!codeVerifier) {\n throw new AuthorizationError('Missing SIMPLELOGIN_CODE_VERIFIER cookie')\n }\n\n // Exchange code for tokens (with PKCE code_verifier)\n const tokens = await this.exchangeCode(code, codeVerifier)\n\n // Fetch user info (only time we do this)\n const user = await this.getUserInfo(tokens.access_token)\n\n // Build redirect response with token cookies\n const cookies = createTokenCookies(tokens)\n const headers = new Headers()\n headers.set('Location', redirectTo)\n for (const cookie of cookies) {\n headers.append('Set-Cookie', cookie)\n }\n // Clear the state and code verifier cookies\n headers.append(\n 'Set-Cookie',\n 'SIMPLELOGIN_STATE=; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=0',\n )\n headers.append(\n 'Set-Cookie',\n 'SIMPLELOGIN_CODE_VERIFIER=; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=0',\n )\n\n const response = new Response(null, { status: 302, headers })\n\n return { response, user }\n }\n\n /**\n * Exchange an authorization code for access and refresh tokens\n * @param code - The authorization code from the callback\n * @param codeVerifier - The PKCE code verifier\n */\n async exchangeCode(code: string, codeVerifier: string): Promise<TokenResponse> {\n const response = await fetch(`${this.baseUrl}/v1/auth/token`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/x-www-form-urlencoded',\n },\n body: new URLSearchParams({\n grant_type: 'authorization_code',\n client_id: this.clientId,\n client_secret: this.clientSecret,\n redirect_uri: this.redirectUri,\n code,\n code_verifier: codeVerifier,\n }),\n })\n\n if (!response.ok) {\n const error = await response.json().catch(() => ({}))\n throw new TokenError(error.error_description || 'Failed to exchange authorization code')\n }\n\n return response.json()\n }\n\n /**\n * Refresh an access token using a refresh token\n */\n async refreshToken(refreshToken: string): Promise<TokenResponse> {\n const response = await fetch(`${this.baseUrl}/v1/auth/token`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/x-www-form-urlencoded',\n },\n body: new URLSearchParams({\n grant_type: 'refresh_token',\n client_id: this.clientId,\n client_secret: this.clientSecret,\n refresh_token: refreshToken,\n }),\n })\n\n if (!response.ok) {\n const error = await response.json().catch(() => ({}))\n throw new TokenError(error.error_description || 'Failed to refresh token')\n }\n\n return response.json()\n }\n\n /**\n * Get user information using an access token\n */\n async getUserInfo(accessToken: string): Promise<UserInfo> {\n const response = await fetch(`${this.baseUrl}/v1/auth/userinfo`, {\n headers: {\n Authorization: `Bearer ${accessToken}`,\n },\n })\n\n if (!response.ok) {\n throw new AuthorizationError('Failed to fetch user info')\n }\n\n return response.json()\n }\n\n /**\n * Fetch and cache the public key for JWT verification\n */\n private async getPublicKey(): Promise<CryptoKey> {\n const cached = publicKeyCache.get(this.clientId)\n if (cached && Date.now() - cached.fetchedAt < PUBLIC_KEY_CACHE_TTL) {\n return cached.key\n }\n\n const response = await fetch(`${this.baseUrl}/api/applications/${this.clientId}/public-key`)\n if (!response.ok) {\n throw new AuthorizationError('Failed to fetch public key')\n }\n\n const data = await response.json()\n const pem = data['public_key']\n const key = await importSPKI(pem, 'RS256')\n\n publicKeyCache.set(this.clientId, { key, fetchedAt: Date.now() })\n return key\n }\n\n /**\n * Authenticate a request by verifying the access token from cookies.\n * No network calls except when refreshing expired tokens.\n * CSRF protection is automatic using the origin from config.\n * @param request - The incoming Request object\n * @returns AuthResult if authenticated, null if not authenticated\n */\n async authenticate(request: Request): Promise<AuthResult | null> {\n // CSRF protection for state-changing requests\n const method = request.method.toUpperCase()\n if (method !== 'GET' && method !== 'HEAD') {\n const requestOrigin = request.headers.get('origin') || request.headers.get('referer')\n if (!requestOrigin?.startsWith(this.origin)) {\n return null\n }\n }\n\n const cookieHeader = request.headers.get('cookie') ?? ''\n const accessToken = parseCookie(cookieHeader, 'SIMPLELOGIN_ACCESS_TOKEN')\n const refreshTokenValue = parseCookie(cookieHeader, 'SIMPLELOGIN_REFRESH_TOKEN')\n\n if (!accessToken) {\n return null\n }\n\n const publicKey = await this.getPublicKey()\n\n try {\n // Verify the token (local, no network call)\n await jwtVerify(accessToken, publicKey)\n\n // Decode claims from JWT (no network call)\n const claims = decodeJwt(accessToken) as AccessTokenClaims\n\n return { claims, accessToken, headers: new Headers() }\n } catch (error) {\n // Check if token is expired\n const isExpired =\n error instanceof Error &&\n 'code' in error &&\n (error as { code: string }).code === 'ERR_JWT_EXPIRED'\n\n if (!isExpired || !refreshTokenValue) {\n return null\n }\n\n // Token expired, try to refresh\n try {\n const tokens = await this.refreshToken(refreshTokenValue)\n const claims = decodeJwt(tokens.access_token) as AccessTokenClaims\n\n // Build headers with new cookies\n const headers = new Headers()\n for (const cookie of createTokenCookies(tokens)) {\n headers.append('Set-Cookie', cookie)\n }\n\n return { claims, accessToken: tokens.access_token, headers }\n } catch {\n return null\n }\n }\n }\n\n /**\n * Revoke a refresh token on the IdP\n */\n private async revokeToken(refreshToken: string): Promise<void> {\n await fetch(`${this.baseUrl}/v1/auth/revoke`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/x-www-form-urlencoded' },\n body: new URLSearchParams({\n token: refreshToken,\n client_id: this.clientId,\n client_secret: this.clientSecret,\n }),\n })\n }\n\n /**\n * Logout: revoke refresh token on IdP, clear cookies, and redirect to auth.\n * @param request - The incoming Request object (to read refresh token from cookies)\n */\n async logout(request: Request): Promise<Response> {\n const cookieHeader = request.headers.get('cookie') ?? ''\n const refreshToken = parseCookie(cookieHeader, 'SIMPLELOGIN_REFRESH_TOKEN')\n\n // Revoke token on IdP (fire and forget - don't block logout on failure)\n if (refreshToken) {\n this.revokeToken(refreshToken).catch(() => {})\n }\n\n // Get auth redirect response\n const authResponse = await this.redirectToAuth()\n\n // Merge clear cookies with auth cookies\n const headers = new Headers(authResponse.headers)\n for (const cookie of clearTokenCookies()) {\n headers.append('Set-Cookie', cookie)\n }\n\n return new Response(null, { status: 302, headers })\n }\n}\n","export class SousaError extends Error {\n constructor(\n message: string,\n public code: string,\n public statusCode?: number\n ) {\n super(message);\n this.name = \"SousaError\";\n }\n}\n\nexport class AuthorizationError extends SousaError {\n constructor(message: string) {\n super(message, \"AUTHORIZATION_ERROR\", 401);\n this.name = \"AuthorizationError\";\n }\n}\n\nexport class TokenError extends SousaError {\n constructor(message: string) {\n super(message, \"TOKEN_ERROR\", 400);\n this.name = \"TokenError\";\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACAA,kBAAiD;;;ACA1C,IAAM,aAAN,cAAyB,MAAM;AAAA,EACpC,YACE,SACO,MACA,YACP;AACA,UAAM,OAAO;AAHN;AACA;AAGP,SAAK,OAAO;AAAA,EACd;AACF;AAEO,IAAM,qBAAN,cAAiC,WAAW;AAAA,EACjD,YAAY,SAAiB;AAC3B,UAAM,SAAS,uBAAuB,GAAG;AACzC,SAAK,OAAO;AAAA,EACd;AACF;AAEO,IAAM,aAAN,cAAyB,WAAW;AAAA,EACzC,YAAY,SAAiB;AAC3B,UAAM,SAAS,eAAe,GAAG;AACjC,SAAK,OAAO;AAAA,EACd;AACF;;;ADRA,IAAM,WAAW;AAEjB,SAAS,gBAAwB;AAC/B,QAAM,QAAQ,IAAI,WAAW,EAAE;AAC/B,SAAO,gBAAgB,KAAK;AAC5B,SAAO,MAAM,KAAK,OAAO,CAAC,MAAM,EAAE,SAAS,EAAE,EAAE,SAAS,GAAG,GAAG,CAAC,EAAE,KAAK,EAAE;AAC1E;AAEA,SAAS,uBAA+B;AACtC,QAAM,QAAQ,IAAI,WAAW,EAAE;AAC/B,SAAO,gBAAgB,KAAK;AAE5B,SAAO,KAAK,OAAO,aAAa,GAAG,KAAK,CAAC,EACtC,QAAQ,OAAO,GAAG,EAClB,QAAQ,OAAO,GAAG,EAClB,QAAQ,MAAM,EAAE;AACrB;AAEA,eAAe,sBAAsB,UAAmC;AACtE,QAAM,UAAU,IAAI,YAAY;AAChC,QAAM,OAAO,QAAQ,OAAO,QAAQ;AACpC,QAAM,OAAO,MAAM,OAAO,OAAO,OAAO,WAAW,IAAI;AAEvD,SAAO,KAAK,OAAO,aAAa,GAAG,IAAI,WAAW,IAAI,CAAC,CAAC,EACrD,QAAQ,OAAO,GAAG,EAClB,QAAQ,OAAO,GAAG,EAClB,QAAQ,MAAM,EAAE;AACrB;AAEA,SAAS,OAAO,KAAiC;AAC/C,MAAI,OAAO,YAAY,eAAe,QAAQ,KAAK;AACjD,WAAO,QAAQ,IAAI,GAAG;AAAA,EACxB;AACA,SAAO;AACT;AAEA,SAAS,YAAY,cAAsB,MAAkC;AAC3E,QAAM,QAAQ,aAAa,MAAM,IAAI,OAAO,cAAc,IAAI,UAAU,CAAC;AACzE,SAAO,QAAQ,mBAAmB,MAAM,CAAC,CAAC,IAAI;AAChD;AAGA,IAAM,iBAAiB,oBAAI,IAAmD;AAC9E,IAAM,uBAAuB,KAAK,KAAK;AAEvC,SAAS,mBAAmB,QAAiC;AAC3D,SAAO;AAAA,IACL,4BAA4B,OAAO,YAAY;AAAA,IAC/C,6BAA6B,OAAO,aAAa;AAAA,EACnD;AACF;AAEA,SAAS,oBAA8B;AACrC,SAAO;AAAA,IACL;AAAA,IACA;AAAA,EACF;AACF;AAEO,IAAM,cAAN,MAAkB;AAAA,EACf;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EAER,YAAY,SAA4B,CAAC,GAAG;AAC1C,SAAK,WAAW,OAAO,YAAY,OAAO,uBAAuB,KAAK;AACtE,SAAK,eAAe,OAAO,gBAAgB,OAAO,2BAA2B,KAAK;AAClF,SAAK,cAAc,OAAO,eAAe,OAAO,0BAA0B,KAAK;AAC/E,SAAK,SAAS,OAAO,UAAU,OAAO,oBAAoB,KAAK;AAC/D,SAAK,UAAU;AAEf,QAAI,CAAC,KAAK,UAAU;AAClB,YAAM,IAAI;AAAA,QACR;AAAA,MACF;AAAA,IACF;AACA,QAAI,CAAC,KAAK,cAAc;AACtB,YAAM,IAAI;AAAA,QACR;AAAA,MACF;AAAA,IACF;AACA,QAAI,CAAC,KAAK,aAAa;AACrB,YAAM,IAAI;AAAA,QACR;AAAA,MACF;AAAA,IACF;AACA,QAAI,CAAC,KAAK,QAAQ;AAChB,YAAM,IAAI,MAAM,0EAA0E;AAAA,IAC5F;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,MAAM,eAAe,UAAmC,CAAC,GAAsB;AAC7E,UAAM,EAAE,KAAK,QAAQ,IAAI,MAAM,KAAK,oBAAoB,OAAO;AAC/D,UAAM,UAAU,IAAI,QAAQ;AAC5B,YAAQ,IAAI,YAAY,GAAG;AAC3B,eAAW,UAAU,SAAS;AAC5B,cAAQ,OAAO,cAAc,MAAM;AAAA,IACrC;AACA,WAAO,IAAI,SAAS,MAAM,EAAE,QAAQ,KAAK,QAAQ,CAAC;AAAA,EACpD;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,oBACJ,UAAmC,CAAC,GACH;AACjC,UAAM,QAAQ,QAAQ,SAAS,cAAc;AAC7C,UAAM,eAAe,qBAAqB;AAC1C,UAAM,gBAAgB,MAAM,sBAAsB,YAAY;AAE9D,UAAM,SAAS,IAAI,gBAAgB;AAAA,MACjC,WAAW,KAAK;AAAA,MAChB,cAAc,KAAK;AAAA,MACnB,eAAe;AAAA,MACf;AAAA,MACA,gBAAgB;AAAA,MAChB,uBAAuB;AAAA,IACzB,CAAC;AAED,QAAI,QAAQ,QAAQ,QAAQ;AAC1B,aAAO,IAAI,SAAS,QAAQ,OAAO,KAAK,GAAG,CAAC;AAAA,IAC9C;AAEA,UAAM,UAAU;AAAA,MACd,qBAAqB,KAAK;AAAA,MAC1B,6BAA6B,YAAY;AAAA,IAC3C;AAEA,WAAO;AAAA,MACL,KAAK,GAAG,KAAK,OAAO,sBAAsB,OAAO,SAAS,CAAC;AAAA,MAC3D;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,MAAM,eAAe,SAAkB,aAAqB,KAA8B;AACxF,UAAM,MAAM,IAAI,IAAI,QAAQ,GAAG;AAC/B,UAAM,OAAO,IAAI,aAAa,IAAI,MAAM;AACxC,UAAM,QAAQ,IAAI,aAAa,IAAI,OAAO;AAE1C,QAAI,CAAC,MAAM;AACT,YAAM,IAAI,mBAAmB,wCAAwC;AAAA,IACvE;AAEA,QAAI,CAAC,OAAO;AACV,YAAM,IAAI,mBAAmB,qCAAqC;AAAA,IACpE;AAEA,UAAM,eAAe,QAAQ,QAAQ,IAAI,QAAQ,KAAK;AACtD,UAAM,gBAAgB,YAAY,cAAc,mBAAmB;AACnE,UAAM,eAAe,YAAY,cAAc,2BAA2B;AAE1E,QAAI,CAAC,eAAe;AAClB,YAAM,IAAI,mBAAmB,kCAAkC;AAAA,IACjE;AAEA,QAAI,UAAU,eAAe;AAC3B,YAAM,IAAI,mBAAmB,yBAAyB;AAAA,IACxD;AAEA,QAAI,CAAC,cAAc;AACjB,YAAM,IAAI,mBAAmB,0CAA0C;AAAA,IACzE;AAGA,UAAM,SAAS,MAAM,KAAK,aAAa,MAAM,YAAY;AAGzD,UAAM,OAAO,MAAM,KAAK,YAAY,OAAO,YAAY;AAGvD,UAAM,UAAU,mBAAmB,MAAM;AACzC,UAAM,UAAU,IAAI,QAAQ;AAC5B,YAAQ,IAAI,YAAY,UAAU;AAClC,eAAW,UAAU,SAAS;AAC5B,cAAQ,OAAO,cAAc,MAAM;AAAA,IACrC;AAEA,YAAQ;AAAA,MACN;AAAA,MACA;AAAA,IACF;AACA,YAAQ;AAAA,MACN;AAAA,MACA;AAAA,IACF;AAEA,UAAM,WAAW,IAAI,SAAS,MAAM,EAAE,QAAQ,KAAK,QAAQ,CAAC;AAE5D,WAAO,EAAE,UAAU,KAAK;AAAA,EAC1B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,MAAM,aAAa,MAAc,cAA8C;AAC7E,UAAM,WAAW,MAAM,MAAM,GAAG,KAAK,OAAO,kBAAkB;AAAA,MAC5D,QAAQ;AAAA,MACR,SAAS;AAAA,QACP,gBAAgB;AAAA,MAClB;AAAA,MACA,MAAM,IAAI,gBAAgB;AAAA,QACxB,YAAY;AAAA,QACZ,WAAW,KAAK;AAAA,QAChB,eAAe,KAAK;AAAA,QACpB,cAAc,KAAK;AAAA,QACnB;AAAA,QACA,eAAe;AAAA,MACjB,CAAC;AAAA,IACH,CAAC;AAED,QAAI,CAAC,SAAS,IAAI;AAChB,YAAM,QAAQ,MAAM,SAAS,KAAK,EAAE,MAAM,OAAO,CAAC,EAAE;AACpD,YAAM,IAAI,WAAW,MAAM,qBAAqB,uCAAuC;AAAA,IACzF;AAEA,WAAO,SAAS,KAAK;AAAA,EACvB;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,aAAa,cAA8C;AAC/D,UAAM,WAAW,MAAM,MAAM,GAAG,KAAK,OAAO,kBAAkB;AAAA,MAC5D,QAAQ;AAAA,MACR,SAAS;AAAA,QACP,gBAAgB;AAAA,MAClB;AAAA,MACA,MAAM,IAAI,gBAAgB;AAAA,QACxB,YAAY;AAAA,QACZ,WAAW,KAAK;AAAA,QAChB,eAAe,KAAK;AAAA,QACpB,eAAe;AAAA,MACjB,CAAC;AAAA,IACH,CAAC;AAED,QAAI,CAAC,SAAS,IAAI;AAChB,YAAM,QAAQ,MAAM,SAAS,KAAK,EAAE,MAAM,OAAO,CAAC,EAAE;AACpD,YAAM,IAAI,WAAW,MAAM,qBAAqB,yBAAyB;AAAA,IAC3E;AAEA,WAAO,SAAS,KAAK;AAAA,EACvB;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,YAAY,aAAwC;AACxD,UAAM,WAAW,MAAM,MAAM,GAAG,KAAK,OAAO,qBAAqB;AAAA,MAC/D,SAAS;AAAA,QACP,eAAe,UAAU,WAAW;AAAA,MACtC;AAAA,IACF,CAAC;AAED,QAAI,CAAC,SAAS,IAAI;AAChB,YAAM,IAAI,mBAAmB,2BAA2B;AAAA,IAC1D;AAEA,WAAO,SAAS,KAAK;AAAA,EACvB;AAAA;AAAA;AAAA;AAAA,EAKA,MAAc,eAAmC;AAC/C,UAAM,SAAS,eAAe,IAAI,KAAK,QAAQ;AAC/C,QAAI,UAAU,KAAK,IAAI,IAAI,OAAO,YAAY,sBAAsB;AAClE,aAAO,OAAO;AAAA,IAChB;AAEA,UAAM,WAAW,MAAM,MAAM,GAAG,KAAK,OAAO,qBAAqB,KAAK,QAAQ,aAAa;AAC3F,QAAI,CAAC,SAAS,IAAI;AAChB,YAAM,IAAI,mBAAmB,4BAA4B;AAAA,IAC3D;AAEA,UAAM,OAAO,MAAM,SAAS,KAAK;AACjC,UAAM,MAAM,KAAK,YAAY;AAC7B,UAAM,MAAM,UAAM,wBAAW,KAAK,OAAO;AAEzC,mBAAe,IAAI,KAAK,UAAU,EAAE,KAAK,WAAW,KAAK,IAAI,EAAE,CAAC;AAChE,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,MAAM,aAAa,SAA8C;AAE/D,UAAM,SAAS,QAAQ,OAAO,YAAY;AAC1C,QAAI,WAAW,SAAS,WAAW,QAAQ;AACzC,YAAM,gBAAgB,QAAQ,QAAQ,IAAI,QAAQ,KAAK,QAAQ,QAAQ,IAAI,SAAS;AACpF,UAAI,CAAC,eAAe,WAAW,KAAK,MAAM,GAAG;AAC3C,eAAO;AAAA,MACT;AAAA,IACF;AAEA,UAAM,eAAe,QAAQ,QAAQ,IAAI,QAAQ,KAAK;AACtD,UAAM,cAAc,YAAY,cAAc,0BAA0B;AACxE,UAAM,oBAAoB,YAAY,cAAc,2BAA2B;AAE/E,QAAI,CAAC,aAAa;AAChB,aAAO;AAAA,IACT;AAEA,UAAM,YAAY,MAAM,KAAK,aAAa;AAE1C,QAAI;AAEF,gBAAM,uBAAU,aAAa,SAAS;AAGtC,YAAM,aAAS,uBAAU,WAAW;AAEpC,aAAO,EAAE,QAAQ,aAAa,SAAS,IAAI,QAAQ,EAAE;AAAA,IACvD,SAAS,OAAO;AAEd,YAAM,YACJ,iBAAiB,SACjB,UAAU,SACT,MAA2B,SAAS;AAEvC,UAAI,CAAC,aAAa,CAAC,mBAAmB;AACpC,eAAO;AAAA,MACT;AAGA,UAAI;AACF,cAAM,SAAS,MAAM,KAAK,aAAa,iBAAiB;AACxD,cAAM,aAAS,uBAAU,OAAO,YAAY;AAG5C,cAAM,UAAU,IAAI,QAAQ;AAC5B,mBAAW,UAAU,mBAAmB,MAAM,GAAG;AAC/C,kBAAQ,OAAO,cAAc,MAAM;AAAA,QACrC;AAEA,eAAO,EAAE,QAAQ,aAAa,OAAO,cAAc,QAAQ;AAAA,MAC7D,QAAQ;AACN,eAAO;AAAA,MACT;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,MAAc,YAAY,cAAqC;AAC7D,UAAM,MAAM,GAAG,KAAK,OAAO,mBAAmB;AAAA,MAC5C,QAAQ;AAAA,MACR,SAAS,EAAE,gBAAgB,oCAAoC;AAAA,MAC/D,MAAM,IAAI,gBAAgB;AAAA,QACxB,OAAO;AAAA,QACP,WAAW,KAAK;AAAA,QAChB,eAAe,KAAK;AAAA,MACtB,CAAC;AAAA,IACH,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,OAAO,SAAqC;AAChD,UAAM,eAAe,QAAQ,QAAQ,IAAI,QAAQ,KAAK;AACtD,UAAM,eAAe,YAAY,cAAc,2BAA2B;AAG1E,QAAI,cAAc;AAChB,WAAK,YAAY,YAAY,EAAE,MAAM,MAAM;AAAA,MAAC,CAAC;AAAA,IAC/C;AAGA,UAAM,eAAe,MAAM,KAAK,eAAe;AAG/C,UAAM,UAAU,IAAI,QAAQ,aAAa,OAAO;AAChD,eAAW,UAAU,kBAAkB,GAAG;AACxC,cAAQ,OAAO,cAAc,MAAM;AAAA,IACrC;AAEA,WAAO,IAAI,SAAS,MAAM,EAAE,QAAQ,KAAK,QAAQ,CAAC;AAAA,EACpD;AACF;","names":[]}
|
package/dist/index.d.cts
CHANGED
|
@@ -2,6 +2,8 @@ interface SimpleLoginConfig {
|
|
|
2
2
|
clientId?: string;
|
|
3
3
|
clientSecret?: string;
|
|
4
4
|
redirectUri?: string;
|
|
5
|
+
/** Origin for CSRF protection (e.g., 'https://myapp.com'). Required for authenticate() to work. */
|
|
6
|
+
origin?: string;
|
|
5
7
|
}
|
|
6
8
|
interface AuthorizationUrlOptions {
|
|
7
9
|
state?: string;
|
|
@@ -34,8 +36,8 @@ interface AuthResult {
|
|
|
34
36
|
/** Decoded claims from the access token JWT (no network call) */
|
|
35
37
|
claims: AccessTokenClaims;
|
|
36
38
|
accessToken: string;
|
|
37
|
-
/** Set-Cookie
|
|
38
|
-
|
|
39
|
+
/** Headers to include in your response (contains Set-Cookie if tokens were refreshed) */
|
|
40
|
+
headers: Headers;
|
|
39
41
|
}
|
|
40
42
|
interface CallbackResult {
|
|
41
43
|
/** Redirect Response with token cookies set */
|
|
@@ -69,6 +71,7 @@ declare class SimpleLogin {
|
|
|
69
71
|
private clientId;
|
|
70
72
|
private clientSecret;
|
|
71
73
|
private redirectUri;
|
|
74
|
+
private origin;
|
|
72
75
|
private baseUrl;
|
|
73
76
|
constructor(config?: SimpleLoginConfig);
|
|
74
77
|
/**
|
|
@@ -111,23 +114,20 @@ declare class SimpleLogin {
|
|
|
111
114
|
/**
|
|
112
115
|
* Authenticate a request by verifying the access token from cookies.
|
|
113
116
|
* No network calls except when refreshing expired tokens.
|
|
117
|
+
* CSRF protection is automatic using the origin from config.
|
|
114
118
|
* @param request - The incoming Request object
|
|
115
|
-
* @param options - Optional settings for CSRF protection
|
|
116
119
|
* @returns AuthResult if authenticated, null if not authenticated
|
|
117
120
|
*/
|
|
118
|
-
authenticate(request: Request
|
|
119
|
-
allowedOrigin?: string;
|
|
120
|
-
}): Promise<AuthResult | null>;
|
|
121
|
+
authenticate(request: Request): Promise<AuthResult | null>;
|
|
121
122
|
/**
|
|
122
123
|
* Revoke a refresh token on the IdP
|
|
123
124
|
*/
|
|
124
125
|
private revokeToken;
|
|
125
126
|
/**
|
|
126
|
-
* Logout: revoke refresh token on IdP and
|
|
127
|
+
* Logout: revoke refresh token on IdP, clear cookies, and redirect to auth.
|
|
127
128
|
* @param request - The incoming Request object (to read refresh token from cookies)
|
|
128
|
-
* @param redirectTo - URL to redirect to after logout (default: '/')
|
|
129
129
|
*/
|
|
130
|
-
logout(request: Request
|
|
130
|
+
logout(request: Request): Promise<Response>;
|
|
131
131
|
}
|
|
132
132
|
|
|
133
133
|
declare class SousaError extends Error {
|
package/dist/index.d.ts
CHANGED
|
@@ -2,6 +2,8 @@ interface SimpleLoginConfig {
|
|
|
2
2
|
clientId?: string;
|
|
3
3
|
clientSecret?: string;
|
|
4
4
|
redirectUri?: string;
|
|
5
|
+
/** Origin for CSRF protection (e.g., 'https://myapp.com'). Required for authenticate() to work. */
|
|
6
|
+
origin?: string;
|
|
5
7
|
}
|
|
6
8
|
interface AuthorizationUrlOptions {
|
|
7
9
|
state?: string;
|
|
@@ -34,8 +36,8 @@ interface AuthResult {
|
|
|
34
36
|
/** Decoded claims from the access token JWT (no network call) */
|
|
35
37
|
claims: AccessTokenClaims;
|
|
36
38
|
accessToken: string;
|
|
37
|
-
/** Set-Cookie
|
|
38
|
-
|
|
39
|
+
/** Headers to include in your response (contains Set-Cookie if tokens were refreshed) */
|
|
40
|
+
headers: Headers;
|
|
39
41
|
}
|
|
40
42
|
interface CallbackResult {
|
|
41
43
|
/** Redirect Response with token cookies set */
|
|
@@ -69,6 +71,7 @@ declare class SimpleLogin {
|
|
|
69
71
|
private clientId;
|
|
70
72
|
private clientSecret;
|
|
71
73
|
private redirectUri;
|
|
74
|
+
private origin;
|
|
72
75
|
private baseUrl;
|
|
73
76
|
constructor(config?: SimpleLoginConfig);
|
|
74
77
|
/**
|
|
@@ -111,23 +114,20 @@ declare class SimpleLogin {
|
|
|
111
114
|
/**
|
|
112
115
|
* Authenticate a request by verifying the access token from cookies.
|
|
113
116
|
* No network calls except when refreshing expired tokens.
|
|
117
|
+
* CSRF protection is automatic using the origin from config.
|
|
114
118
|
* @param request - The incoming Request object
|
|
115
|
-
* @param options - Optional settings for CSRF protection
|
|
116
119
|
* @returns AuthResult if authenticated, null if not authenticated
|
|
117
120
|
*/
|
|
118
|
-
authenticate(request: Request
|
|
119
|
-
allowedOrigin?: string;
|
|
120
|
-
}): Promise<AuthResult | null>;
|
|
121
|
+
authenticate(request: Request): Promise<AuthResult | null>;
|
|
121
122
|
/**
|
|
122
123
|
* Revoke a refresh token on the IdP
|
|
123
124
|
*/
|
|
124
125
|
private revokeToken;
|
|
125
126
|
/**
|
|
126
|
-
* Logout: revoke refresh token on IdP and
|
|
127
|
+
* Logout: revoke refresh token on IdP, clear cookies, and redirect to auth.
|
|
127
128
|
* @param request - The incoming Request object (to read refresh token from cookies)
|
|
128
|
-
* @param redirectTo - URL to redirect to after logout (default: '/')
|
|
129
129
|
*/
|
|
130
|
-
logout(request: Request
|
|
130
|
+
logout(request: Request): Promise<Response>;
|
|
131
131
|
}
|
|
132
132
|
|
|
133
133
|
declare class SousaError extends Error {
|
package/dist/index.js
CHANGED
|
@@ -69,11 +69,13 @@ var SimpleLogin = class {
|
|
|
69
69
|
clientId;
|
|
70
70
|
clientSecret;
|
|
71
71
|
redirectUri;
|
|
72
|
+
origin;
|
|
72
73
|
baseUrl;
|
|
73
74
|
constructor(config = {}) {
|
|
74
75
|
this.clientId = config.clientId ?? getEnv("SIMPLELOGIN_CLIENT_ID") ?? "";
|
|
75
76
|
this.clientSecret = config.clientSecret ?? getEnv("SIMPLELOGIN_CLIENT_SECRET") ?? "";
|
|
76
77
|
this.redirectUri = config.redirectUri ?? getEnv("SIMPLELOGIN_REDIRECT_URI") ?? "";
|
|
78
|
+
this.origin = config.origin ?? getEnv("SIMPLELOGIN_ORIGIN") ?? "";
|
|
77
79
|
this.baseUrl = BASE_URL;
|
|
78
80
|
if (!this.clientId) {
|
|
79
81
|
throw new Error(
|
|
@@ -90,6 +92,9 @@ var SimpleLogin = class {
|
|
|
90
92
|
"redirectUri is required. Pass it in config or set SIMPLELOGIN_REDIRECT_URI env var."
|
|
91
93
|
);
|
|
92
94
|
}
|
|
95
|
+
if (!this.origin) {
|
|
96
|
+
throw new Error("origin is required. Pass it in config or set SIMPLELOGIN_ORIGIN env var.");
|
|
97
|
+
}
|
|
93
98
|
}
|
|
94
99
|
/**
|
|
95
100
|
* Returns a Response that redirects to the authorization URL with state and PKCE cookies set.
|
|
@@ -257,7 +262,8 @@ var SimpleLogin = class {
|
|
|
257
262
|
if (!response.ok) {
|
|
258
263
|
throw new AuthorizationError("Failed to fetch public key");
|
|
259
264
|
}
|
|
260
|
-
const
|
|
265
|
+
const data = await response.json();
|
|
266
|
+
const pem = data["public_key"];
|
|
261
267
|
const key = await importSPKI(pem, "RS256");
|
|
262
268
|
publicKeyCache.set(this.clientId, { key, fetchedAt: Date.now() });
|
|
263
269
|
return key;
|
|
@@ -265,15 +271,15 @@ var SimpleLogin = class {
|
|
|
265
271
|
/**
|
|
266
272
|
* Authenticate a request by verifying the access token from cookies.
|
|
267
273
|
* No network calls except when refreshing expired tokens.
|
|
274
|
+
* CSRF protection is automatic using the origin from config.
|
|
268
275
|
* @param request - The incoming Request object
|
|
269
|
-
* @param options - Optional settings for CSRF protection
|
|
270
276
|
* @returns AuthResult if authenticated, null if not authenticated
|
|
271
277
|
*/
|
|
272
|
-
async authenticate(request
|
|
278
|
+
async authenticate(request) {
|
|
273
279
|
const method = request.method.toUpperCase();
|
|
274
|
-
if (method !== "GET" && method !== "HEAD"
|
|
275
|
-
const
|
|
276
|
-
if (!
|
|
280
|
+
if (method !== "GET" && method !== "HEAD") {
|
|
281
|
+
const requestOrigin = request.headers.get("origin") || request.headers.get("referer");
|
|
282
|
+
if (!requestOrigin?.startsWith(this.origin)) {
|
|
277
283
|
return null;
|
|
278
284
|
}
|
|
279
285
|
}
|
|
@@ -287,7 +293,7 @@ var SimpleLogin = class {
|
|
|
287
293
|
try {
|
|
288
294
|
await jwtVerify(accessToken, publicKey);
|
|
289
295
|
const claims = decodeJwt(accessToken);
|
|
290
|
-
return { claims, accessToken };
|
|
296
|
+
return { claims, accessToken, headers: new Headers() };
|
|
291
297
|
} catch (error) {
|
|
292
298
|
const isExpired = error instanceof Error && "code" in error && error.code === "ERR_JWT_EXPIRED";
|
|
293
299
|
if (!isExpired || !refreshTokenValue) {
|
|
@@ -296,8 +302,11 @@ var SimpleLogin = class {
|
|
|
296
302
|
try {
|
|
297
303
|
const tokens = await this.refreshToken(refreshTokenValue);
|
|
298
304
|
const claims = decodeJwt(tokens.access_token);
|
|
299
|
-
const
|
|
300
|
-
|
|
305
|
+
const headers = new Headers();
|
|
306
|
+
for (const cookie of createTokenCookies(tokens)) {
|
|
307
|
+
headers.append("Set-Cookie", cookie);
|
|
308
|
+
}
|
|
309
|
+
return { claims, accessToken: tokens.access_token, headers };
|
|
301
310
|
} catch {
|
|
302
311
|
return null;
|
|
303
312
|
}
|
|
@@ -318,21 +327,19 @@ var SimpleLogin = class {
|
|
|
318
327
|
});
|
|
319
328
|
}
|
|
320
329
|
/**
|
|
321
|
-
* Logout: revoke refresh token on IdP and
|
|
330
|
+
* Logout: revoke refresh token on IdP, clear cookies, and redirect to auth.
|
|
322
331
|
* @param request - The incoming Request object (to read refresh token from cookies)
|
|
323
|
-
* @param redirectTo - URL to redirect to after logout (default: '/')
|
|
324
332
|
*/
|
|
325
|
-
async logout(request
|
|
333
|
+
async logout(request) {
|
|
326
334
|
const cookieHeader = request.headers.get("cookie") ?? "";
|
|
327
335
|
const refreshToken = parseCookie(cookieHeader, "SIMPLELOGIN_REFRESH_TOKEN");
|
|
328
336
|
if (refreshToken) {
|
|
329
337
|
this.revokeToken(refreshToken).catch(() => {
|
|
330
338
|
});
|
|
331
339
|
}
|
|
332
|
-
const
|
|
333
|
-
const headers = new Headers();
|
|
334
|
-
|
|
335
|
-
for (const cookie of cookies) {
|
|
340
|
+
const authResponse = await this.redirectToAuth();
|
|
341
|
+
const headers = new Headers(authResponse.headers);
|
|
342
|
+
for (const cookie of clearTokenCookies()) {
|
|
336
343
|
headers.append("Set-Cookie", cookie);
|
|
337
344
|
}
|
|
338
345
|
return new Response(null, { status: 302, headers });
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/client.ts","../src/errors.ts"],"sourcesContent":["import { decodeJwt, importSPKI, jwtVerify } from 'jose'\nimport { AuthorizationError, TokenError } from './errors'\nimport type {\n AccessTokenClaims,\n AuthResult,\n AuthorizationUrlOptions,\n AuthorizationUrlResult,\n CallbackResult,\n SimpleLoginConfig,\n TokenResponse,\n UserInfo,\n} from './types'\n\ndeclare const __SIMPLELOGIN_BASE_URL__: string\n\nconst BASE_URL = __SIMPLELOGIN_BASE_URL__\n\nfunction generateState(): string {\n const array = new Uint8Array(32)\n crypto.getRandomValues(array)\n return Array.from(array, (b) => b.toString(16).padStart(2, '0')).join('')\n}\n\nfunction generateCodeVerifier(): string {\n const array = new Uint8Array(32)\n crypto.getRandomValues(array)\n // Base64url encode (RFC 7636)\n return btoa(String.fromCharCode(...array))\n .replace(/\\+/g, '-')\n .replace(/\\//g, '_')\n .replace(/=/g, '')\n}\n\nasync function generateCodeChallenge(verifier: string): Promise<string> {\n const encoder = new TextEncoder()\n const data = encoder.encode(verifier)\n const hash = await crypto.subtle.digest('SHA-256', data)\n // Base64url encode the hash\n return btoa(String.fromCharCode(...new Uint8Array(hash)))\n .replace(/\\+/g, '-')\n .replace(/\\//g, '_')\n .replace(/=/g, '')\n}\n\nfunction getEnv(key: string): string | undefined {\n if (typeof process !== 'undefined' && process.env) {\n return process.env[key]\n }\n return undefined\n}\n\nfunction parseCookie(cookieHeader: string, name: string): string | undefined {\n const match = cookieHeader.match(new RegExp(`(?:^|;\\\\s*)${name}=([^;]*)`))\n return match ? decodeURIComponent(match[1]) : undefined\n}\n\n// Public key cache: clientId -> { key, fetchedAt }\nconst publicKeyCache = new Map<string, { key: CryptoKey; fetchedAt: number }>()\nconst PUBLIC_KEY_CACHE_TTL = 60 * 60 * 1000 // 1 hour\n\nfunction createTokenCookies(tokens: TokenResponse): string[] {\n return [\n `SIMPLELOGIN_ACCESS_TOKEN=${tokens.access_token}; HttpOnly; Secure; SameSite=Lax; Path=/`,\n `SIMPLELOGIN_REFRESH_TOKEN=${tokens.refresh_token}; HttpOnly; Secure; SameSite=Lax; Path=/`,\n ]\n}\n\nfunction clearTokenCookies(): string[] {\n return [\n 'SIMPLELOGIN_ACCESS_TOKEN=; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=0',\n 'SIMPLELOGIN_REFRESH_TOKEN=; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=0',\n ]\n}\n\nexport class SimpleLogin {\n private clientId: string\n private clientSecret: string\n private redirectUri: string\n private baseUrl: string\n\n constructor(config: SimpleLoginConfig = {}) {\n this.clientId = config.clientId ?? getEnv('SIMPLELOGIN_CLIENT_ID') ?? ''\n this.clientSecret = config.clientSecret ?? getEnv('SIMPLELOGIN_CLIENT_SECRET') ?? ''\n this.redirectUri = config.redirectUri ?? getEnv('SIMPLELOGIN_REDIRECT_URI') ?? ''\n this.baseUrl = BASE_URL\n\n if (!this.clientId) {\n throw new Error(\n 'clientId is required. Pass it in config or set SIMPLELOGIN_CLIENT_ID env var.',\n )\n }\n if (!this.clientSecret) {\n throw new Error(\n 'clientSecret is required. Pass it in config or set SIMPLELOGIN_CLIENT_SECRET env var.',\n )\n }\n if (!this.redirectUri) {\n throw new Error(\n 'redirectUri is required. Pass it in config or set SIMPLELOGIN_REDIRECT_URI env var.',\n )\n }\n }\n\n /**\n * Returns a Response that redirects to the authorization URL with state and PKCE cookies set.\n * @param options - Optional scopes or custom state\n * @returns A 302 redirect Response ready to be returned from your route handler\n */\n async redirectToAuth(options: AuthorizationUrlOptions = {}): Promise<Response> {\n const { url, cookies } = await this.getAuthorizationUrl(options)\n const headers = new Headers()\n headers.set('Location', url)\n for (const cookie of cookies) {\n headers.append('Set-Cookie', cookie)\n }\n return new Response(null, { status: 302, headers })\n }\n\n /**\n * Generate the authorization URL to redirect users for sign-in\n * @returns The authorization URL, state, code verifier, and ready-to-use Set-Cookie header values\n */\n async getAuthorizationUrl(options: AuthorizationUrlOptions = {}): Promise<AuthorizationUrlResult> {\n const state = options.state ?? generateState()\n const codeVerifier = generateCodeVerifier()\n const codeChallenge = await generateCodeChallenge(codeVerifier)\n\n const params = new URLSearchParams({\n client_id: this.clientId,\n redirect_uri: this.redirectUri,\n response_type: 'code',\n state,\n code_challenge: codeChallenge,\n code_challenge_method: 'S256',\n })\n\n if (options.scopes?.length) {\n params.set('scope', options.scopes.join(' '))\n }\n\n const cookies = [\n `SIMPLELOGIN_STATE=${state}; HttpOnly; Secure; SameSite=Lax; Max-Age=600; Path=/`,\n `SIMPLELOGIN_CODE_VERIFIER=${codeVerifier}; HttpOnly; Secure; SameSite=Lax; Max-Age=600; Path=/`,\n ]\n\n return {\n url: `${this.baseUrl}/v1/auth/authorize?${params.toString()}`,\n state,\n codeVerifier,\n cookies,\n }\n }\n\n /**\n * Handle the OAuth callback: verify state, exchange code, fetch user info, and return a redirect Response.\n * @param request - The incoming Request object from the callback\n * @param redirectTo - URL to redirect to after successful authentication (default: '/')\n * @returns CallbackResult with redirect Response (cookies set) and user info to store\n * @throws AuthorizationError if state is missing or doesn't match\n */\n async handleCallback(request: Request, redirectTo: string = '/'): Promise<CallbackResult> {\n const url = new URL(request.url)\n const code = url.searchParams.get('code')\n const state = url.searchParams.get('state')\n\n if (!code) {\n throw new AuthorizationError('Missing authorization code in callback')\n }\n\n if (!state) {\n throw new AuthorizationError('Missing state parameter in callback')\n }\n\n const cookieHeader = request.headers.get('cookie') ?? ''\n const expectedState = parseCookie(cookieHeader, 'SIMPLELOGIN_STATE')\n const codeVerifier = parseCookie(cookieHeader, 'SIMPLELOGIN_CODE_VERIFIER')\n\n if (!expectedState) {\n throw new AuthorizationError('Missing SIMPLELOGIN_STATE cookie')\n }\n\n if (state !== expectedState) {\n throw new AuthorizationError('Invalid state parameter')\n }\n\n if (!codeVerifier) {\n throw new AuthorizationError('Missing SIMPLELOGIN_CODE_VERIFIER cookie')\n }\n\n // Exchange code for tokens (with PKCE code_verifier)\n const tokens = await this.exchangeCode(code, codeVerifier)\n\n // Fetch user info (only time we do this)\n const user = await this.getUserInfo(tokens.access_token)\n\n // Build redirect response with token cookies\n const cookies = createTokenCookies(tokens)\n const headers = new Headers()\n headers.set('Location', redirectTo)\n for (const cookie of cookies) {\n headers.append('Set-Cookie', cookie)\n }\n // Clear the state and code verifier cookies\n headers.append(\n 'Set-Cookie',\n 'SIMPLELOGIN_STATE=; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=0',\n )\n headers.append(\n 'Set-Cookie',\n 'SIMPLELOGIN_CODE_VERIFIER=; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=0',\n )\n\n const response = new Response(null, { status: 302, headers })\n\n return { response, user }\n }\n\n /**\n * Exchange an authorization code for access and refresh tokens\n * @param code - The authorization code from the callback\n * @param codeVerifier - The PKCE code verifier\n */\n async exchangeCode(code: string, codeVerifier: string): Promise<TokenResponse> {\n const response = await fetch(`${this.baseUrl}/v1/auth/token`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/x-www-form-urlencoded',\n },\n body: new URLSearchParams({\n grant_type: 'authorization_code',\n client_id: this.clientId,\n client_secret: this.clientSecret,\n redirect_uri: this.redirectUri,\n code,\n code_verifier: codeVerifier,\n }),\n })\n\n if (!response.ok) {\n const error = await response.json().catch(() => ({}))\n throw new TokenError(error.error_description || 'Failed to exchange authorization code')\n }\n\n return response.json()\n }\n\n /**\n * Refresh an access token using a refresh token\n */\n async refreshToken(refreshToken: string): Promise<TokenResponse> {\n const response = await fetch(`${this.baseUrl}/v1/auth/token`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/x-www-form-urlencoded',\n },\n body: new URLSearchParams({\n grant_type: 'refresh_token',\n client_id: this.clientId,\n client_secret: this.clientSecret,\n refresh_token: refreshToken,\n }),\n })\n\n if (!response.ok) {\n const error = await response.json().catch(() => ({}))\n throw new TokenError(error.error_description || 'Failed to refresh token')\n }\n\n return response.json()\n }\n\n /**\n * Get user information using an access token\n */\n async getUserInfo(accessToken: string): Promise<UserInfo> {\n const response = await fetch(`${this.baseUrl}/v1/auth/userinfo`, {\n headers: {\n Authorization: `Bearer ${accessToken}`,\n },\n })\n\n if (!response.ok) {\n throw new AuthorizationError('Failed to fetch user info')\n }\n\n return response.json()\n }\n\n /**\n * Fetch and cache the public key for JWT verification\n */\n private async getPublicKey(): Promise<CryptoKey> {\n const cached = publicKeyCache.get(this.clientId)\n if (cached && Date.now() - cached.fetchedAt < PUBLIC_KEY_CACHE_TTL) {\n return cached.key\n }\n\n const response = await fetch(`${this.baseUrl}/api/applications/${this.clientId}/public-key`)\n if (!response.ok) {\n throw new AuthorizationError('Failed to fetch public key')\n }\n\n const pem = await response.text()\n const key = await importSPKI(pem, 'RS256')\n\n publicKeyCache.set(this.clientId, { key, fetchedAt: Date.now() })\n return key\n }\n\n /**\n * Authenticate a request by verifying the access token from cookies.\n * No network calls except when refreshing expired tokens.\n * @param request - The incoming Request object\n * @param options - Optional settings for CSRF protection\n * @returns AuthResult if authenticated, null if not authenticated\n */\n async authenticate(\n request: Request,\n options?: { allowedOrigin?: string },\n ): Promise<AuthResult | null> {\n // CSRF protection for state-changing requests\n const method = request.method.toUpperCase()\n if (method !== 'GET' && method !== 'HEAD' && options?.allowedOrigin) {\n const origin = request.headers.get('origin') || request.headers.get('referer')\n if (!origin?.startsWith(options.allowedOrigin)) {\n return null\n }\n }\n\n const cookieHeader = request.headers.get('cookie') ?? ''\n const accessToken = parseCookie(cookieHeader, 'SIMPLELOGIN_ACCESS_TOKEN')\n const refreshTokenValue = parseCookie(cookieHeader, 'SIMPLELOGIN_REFRESH_TOKEN')\n\n if (!accessToken) {\n return null\n }\n\n const publicKey = await this.getPublicKey()\n\n try {\n // Verify the token (local, no network call)\n await jwtVerify(accessToken, publicKey)\n\n // Decode claims from JWT (no network call)\n const claims = decodeJwt(accessToken) as AccessTokenClaims\n\n return { claims, accessToken }\n } catch (error) {\n // Check if token is expired\n const isExpired =\n error instanceof Error &&\n 'code' in error &&\n (error as { code: string }).code === 'ERR_JWT_EXPIRED'\n\n if (!isExpired || !refreshTokenValue) {\n return null\n }\n\n // Token expired, try to refresh\n try {\n const tokens = await this.refreshToken(refreshTokenValue)\n const claims = decodeJwt(tokens.access_token) as AccessTokenClaims\n const cookies = createTokenCookies(tokens)\n\n return { claims, accessToken: tokens.access_token, cookies }\n } catch {\n return null\n }\n }\n }\n\n /**\n * Revoke a refresh token on the IdP\n */\n private async revokeToken(refreshToken: string): Promise<void> {\n await fetch(`${this.baseUrl}/v1/auth/revoke`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/x-www-form-urlencoded' },\n body: new URLSearchParams({\n token: refreshToken,\n client_id: this.clientId,\n client_secret: this.clientSecret,\n }),\n })\n }\n\n /**\n * Logout: revoke refresh token on IdP and clear cookies.\n * @param request - The incoming Request object (to read refresh token from cookies)\n * @param redirectTo - URL to redirect to after logout (default: '/')\n */\n async logout(request: Request, redirectTo: string = '/'): Promise<Response> {\n const cookieHeader = request.headers.get('cookie') ?? ''\n const refreshToken = parseCookie(cookieHeader, 'SIMPLELOGIN_REFRESH_TOKEN')\n\n // Revoke token on IdP (fire and forget - don't block logout on failure)\n if (refreshToken) {\n this.revokeToken(refreshToken).catch(() => {})\n }\n\n // Clear cookies regardless\n const cookies = clearTokenCookies()\n const headers = new Headers()\n headers.set('Location', redirectTo)\n for (const cookie of cookies) {\n headers.append('Set-Cookie', cookie)\n }\n return new Response(null, { status: 302, headers })\n }\n}\n","export class SousaError extends Error {\n constructor(\n message: string,\n public code: string,\n public statusCode?: number\n ) {\n super(message);\n this.name = \"SousaError\";\n }\n}\n\nexport class AuthorizationError extends SousaError {\n constructor(message: string) {\n super(message, \"AUTHORIZATION_ERROR\", 401);\n this.name = \"AuthorizationError\";\n }\n}\n\nexport class TokenError extends SousaError {\n constructor(message: string) {\n super(message, \"TOKEN_ERROR\", 400);\n this.name = \"TokenError\";\n }\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/client.ts","../src/errors.ts"],"sourcesContent":["import { decodeJwt, importSPKI, jwtVerify } from 'jose'\nimport { AuthorizationError, TokenError } from './errors'\nimport type {\n AccessTokenClaims,\n AuthResult,\n AuthorizationUrlOptions,\n AuthorizationUrlResult,\n CallbackResult,\n SimpleLoginConfig,\n TokenResponse,\n UserInfo,\n} from './types'\n\ndeclare const __SIMPLELOGIN_BASE_URL__: string\n\nconst BASE_URL = __SIMPLELOGIN_BASE_URL__\n\nfunction generateState(): string {\n const array = new Uint8Array(32)\n crypto.getRandomValues(array)\n return Array.from(array, (b) => b.toString(16).padStart(2, '0')).join('')\n}\n\nfunction generateCodeVerifier(): string {\n const array = new Uint8Array(32)\n crypto.getRandomValues(array)\n // Base64url encode (RFC 7636)\n return btoa(String.fromCharCode(...array))\n .replace(/\\+/g, '-')\n .replace(/\\//g, '_')\n .replace(/=/g, '')\n}\n\nasync function generateCodeChallenge(verifier: string): Promise<string> {\n const encoder = new TextEncoder()\n const data = encoder.encode(verifier)\n const hash = await crypto.subtle.digest('SHA-256', data)\n // Base64url encode the hash\n return btoa(String.fromCharCode(...new Uint8Array(hash)))\n .replace(/\\+/g, '-')\n .replace(/\\//g, '_')\n .replace(/=/g, '')\n}\n\nfunction getEnv(key: string): string | undefined {\n if (typeof process !== 'undefined' && process.env) {\n return process.env[key]\n }\n return undefined\n}\n\nfunction parseCookie(cookieHeader: string, name: string): string | undefined {\n const match = cookieHeader.match(new RegExp(`(?:^|;\\\\s*)${name}=([^;]*)`))\n return match ? decodeURIComponent(match[1]) : undefined\n}\n\n// Public key cache: clientId -> { key, fetchedAt }\nconst publicKeyCache = new Map<string, { key: CryptoKey; fetchedAt: number }>()\nconst PUBLIC_KEY_CACHE_TTL = 60 * 60 * 1000 // 1 hour\n\nfunction createTokenCookies(tokens: TokenResponse): string[] {\n return [\n `SIMPLELOGIN_ACCESS_TOKEN=${tokens.access_token}; HttpOnly; Secure; SameSite=Lax; Path=/`,\n `SIMPLELOGIN_REFRESH_TOKEN=${tokens.refresh_token}; HttpOnly; Secure; SameSite=Lax; Path=/`,\n ]\n}\n\nfunction clearTokenCookies(): string[] {\n return [\n 'SIMPLELOGIN_ACCESS_TOKEN=; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=0',\n 'SIMPLELOGIN_REFRESH_TOKEN=; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=0',\n ]\n}\n\nexport class SimpleLogin {\n private clientId: string\n private clientSecret: string\n private redirectUri: string\n private origin: string\n private baseUrl: string\n\n constructor(config: SimpleLoginConfig = {}) {\n this.clientId = config.clientId ?? getEnv('SIMPLELOGIN_CLIENT_ID') ?? ''\n this.clientSecret = config.clientSecret ?? getEnv('SIMPLELOGIN_CLIENT_SECRET') ?? ''\n this.redirectUri = config.redirectUri ?? getEnv('SIMPLELOGIN_REDIRECT_URI') ?? ''\n this.origin = config.origin ?? getEnv('SIMPLELOGIN_ORIGIN') ?? ''\n this.baseUrl = BASE_URL\n\n if (!this.clientId) {\n throw new Error(\n 'clientId is required. Pass it in config or set SIMPLELOGIN_CLIENT_ID env var.',\n )\n }\n if (!this.clientSecret) {\n throw new Error(\n 'clientSecret is required. Pass it in config or set SIMPLELOGIN_CLIENT_SECRET env var.',\n )\n }\n if (!this.redirectUri) {\n throw new Error(\n 'redirectUri is required. Pass it in config or set SIMPLELOGIN_REDIRECT_URI env var.',\n )\n }\n if (!this.origin) {\n throw new Error('origin is required. Pass it in config or set SIMPLELOGIN_ORIGIN env var.')\n }\n }\n\n /**\n * Returns a Response that redirects to the authorization URL with state and PKCE cookies set.\n * @param options - Optional scopes or custom state\n * @returns A 302 redirect Response ready to be returned from your route handler\n */\n async redirectToAuth(options: AuthorizationUrlOptions = {}): Promise<Response> {\n const { url, cookies } = await this.getAuthorizationUrl(options)\n const headers = new Headers()\n headers.set('Location', url)\n for (const cookie of cookies) {\n headers.append('Set-Cookie', cookie)\n }\n return new Response(null, { status: 302, headers })\n }\n\n /**\n * Generate the authorization URL to redirect users for sign-in\n * @returns The authorization URL, state, code verifier, and ready-to-use Set-Cookie header values\n */\n async getAuthorizationUrl(\n options: AuthorizationUrlOptions = {},\n ): Promise<AuthorizationUrlResult> {\n const state = options.state ?? generateState()\n const codeVerifier = generateCodeVerifier()\n const codeChallenge = await generateCodeChallenge(codeVerifier)\n\n const params = new URLSearchParams({\n client_id: this.clientId,\n redirect_uri: this.redirectUri,\n response_type: 'code',\n state,\n code_challenge: codeChallenge,\n code_challenge_method: 'S256',\n })\n\n if (options.scopes?.length) {\n params.set('scope', options.scopes.join(' '))\n }\n\n const cookies = [\n `SIMPLELOGIN_STATE=${state}; HttpOnly; Secure; SameSite=Lax; Max-Age=600; Path=/`,\n `SIMPLELOGIN_CODE_VERIFIER=${codeVerifier}; HttpOnly; Secure; SameSite=Lax; Max-Age=600; Path=/`,\n ]\n\n return {\n url: `${this.baseUrl}/v1/auth/authorize?${params.toString()}`,\n state,\n codeVerifier,\n cookies,\n }\n }\n\n /**\n * Handle the OAuth callback: verify state, exchange code, fetch user info, and return a redirect Response.\n * @param request - The incoming Request object from the callback\n * @param redirectTo - URL to redirect to after successful authentication (default: '/')\n * @returns CallbackResult with redirect Response (cookies set) and user info to store\n * @throws AuthorizationError if state is missing or doesn't match\n */\n async handleCallback(request: Request, redirectTo: string = '/'): Promise<CallbackResult> {\n const url = new URL(request.url)\n const code = url.searchParams.get('code')\n const state = url.searchParams.get('state')\n\n if (!code) {\n throw new AuthorizationError('Missing authorization code in callback')\n }\n\n if (!state) {\n throw new AuthorizationError('Missing state parameter in callback')\n }\n\n const cookieHeader = request.headers.get('cookie') ?? ''\n const expectedState = parseCookie(cookieHeader, 'SIMPLELOGIN_STATE')\n const codeVerifier = parseCookie(cookieHeader, 'SIMPLELOGIN_CODE_VERIFIER')\n\n if (!expectedState) {\n throw new AuthorizationError('Missing SIMPLELOGIN_STATE cookie')\n }\n\n if (state !== expectedState) {\n throw new AuthorizationError('Invalid state parameter')\n }\n\n if (!codeVerifier) {\n throw new AuthorizationError('Missing SIMPLELOGIN_CODE_VERIFIER cookie')\n }\n\n // Exchange code for tokens (with PKCE code_verifier)\n const tokens = await this.exchangeCode(code, codeVerifier)\n\n // Fetch user info (only time we do this)\n const user = await this.getUserInfo(tokens.access_token)\n\n // Build redirect response with token cookies\n const cookies = createTokenCookies(tokens)\n const headers = new Headers()\n headers.set('Location', redirectTo)\n for (const cookie of cookies) {\n headers.append('Set-Cookie', cookie)\n }\n // Clear the state and code verifier cookies\n headers.append(\n 'Set-Cookie',\n 'SIMPLELOGIN_STATE=; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=0',\n )\n headers.append(\n 'Set-Cookie',\n 'SIMPLELOGIN_CODE_VERIFIER=; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=0',\n )\n\n const response = new Response(null, { status: 302, headers })\n\n return { response, user }\n }\n\n /**\n * Exchange an authorization code for access and refresh tokens\n * @param code - The authorization code from the callback\n * @param codeVerifier - The PKCE code verifier\n */\n async exchangeCode(code: string, codeVerifier: string): Promise<TokenResponse> {\n const response = await fetch(`${this.baseUrl}/v1/auth/token`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/x-www-form-urlencoded',\n },\n body: new URLSearchParams({\n grant_type: 'authorization_code',\n client_id: this.clientId,\n client_secret: this.clientSecret,\n redirect_uri: this.redirectUri,\n code,\n code_verifier: codeVerifier,\n }),\n })\n\n if (!response.ok) {\n const error = await response.json().catch(() => ({}))\n throw new TokenError(error.error_description || 'Failed to exchange authorization code')\n }\n\n return response.json()\n }\n\n /**\n * Refresh an access token using a refresh token\n */\n async refreshToken(refreshToken: string): Promise<TokenResponse> {\n const response = await fetch(`${this.baseUrl}/v1/auth/token`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/x-www-form-urlencoded',\n },\n body: new URLSearchParams({\n grant_type: 'refresh_token',\n client_id: this.clientId,\n client_secret: this.clientSecret,\n refresh_token: refreshToken,\n }),\n })\n\n if (!response.ok) {\n const error = await response.json().catch(() => ({}))\n throw new TokenError(error.error_description || 'Failed to refresh token')\n }\n\n return response.json()\n }\n\n /**\n * Get user information using an access token\n */\n async getUserInfo(accessToken: string): Promise<UserInfo> {\n const response = await fetch(`${this.baseUrl}/v1/auth/userinfo`, {\n headers: {\n Authorization: `Bearer ${accessToken}`,\n },\n })\n\n if (!response.ok) {\n throw new AuthorizationError('Failed to fetch user info')\n }\n\n return response.json()\n }\n\n /**\n * Fetch and cache the public key for JWT verification\n */\n private async getPublicKey(): Promise<CryptoKey> {\n const cached = publicKeyCache.get(this.clientId)\n if (cached && Date.now() - cached.fetchedAt < PUBLIC_KEY_CACHE_TTL) {\n return cached.key\n }\n\n const response = await fetch(`${this.baseUrl}/api/applications/${this.clientId}/public-key`)\n if (!response.ok) {\n throw new AuthorizationError('Failed to fetch public key')\n }\n\n const data = await response.json()\n const pem = data['public_key']\n const key = await importSPKI(pem, 'RS256')\n\n publicKeyCache.set(this.clientId, { key, fetchedAt: Date.now() })\n return key\n }\n\n /**\n * Authenticate a request by verifying the access token from cookies.\n * No network calls except when refreshing expired tokens.\n * CSRF protection is automatic using the origin from config.\n * @param request - The incoming Request object\n * @returns AuthResult if authenticated, null if not authenticated\n */\n async authenticate(request: Request): Promise<AuthResult | null> {\n // CSRF protection for state-changing requests\n const method = request.method.toUpperCase()\n if (method !== 'GET' && method !== 'HEAD') {\n const requestOrigin = request.headers.get('origin') || request.headers.get('referer')\n if (!requestOrigin?.startsWith(this.origin)) {\n return null\n }\n }\n\n const cookieHeader = request.headers.get('cookie') ?? ''\n const accessToken = parseCookie(cookieHeader, 'SIMPLELOGIN_ACCESS_TOKEN')\n const refreshTokenValue = parseCookie(cookieHeader, 'SIMPLELOGIN_REFRESH_TOKEN')\n\n if (!accessToken) {\n return null\n }\n\n const publicKey = await this.getPublicKey()\n\n try {\n // Verify the token (local, no network call)\n await jwtVerify(accessToken, publicKey)\n\n // Decode claims from JWT (no network call)\n const claims = decodeJwt(accessToken) as AccessTokenClaims\n\n return { claims, accessToken, headers: new Headers() }\n } catch (error) {\n // Check if token is expired\n const isExpired =\n error instanceof Error &&\n 'code' in error &&\n (error as { code: string }).code === 'ERR_JWT_EXPIRED'\n\n if (!isExpired || !refreshTokenValue) {\n return null\n }\n\n // Token expired, try to refresh\n try {\n const tokens = await this.refreshToken(refreshTokenValue)\n const claims = decodeJwt(tokens.access_token) as AccessTokenClaims\n\n // Build headers with new cookies\n const headers = new Headers()\n for (const cookie of createTokenCookies(tokens)) {\n headers.append('Set-Cookie', cookie)\n }\n\n return { claims, accessToken: tokens.access_token, headers }\n } catch {\n return null\n }\n }\n }\n\n /**\n * Revoke a refresh token on the IdP\n */\n private async revokeToken(refreshToken: string): Promise<void> {\n await fetch(`${this.baseUrl}/v1/auth/revoke`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/x-www-form-urlencoded' },\n body: new URLSearchParams({\n token: refreshToken,\n client_id: this.clientId,\n client_secret: this.clientSecret,\n }),\n })\n }\n\n /**\n * Logout: revoke refresh token on IdP, clear cookies, and redirect to auth.\n * @param request - The incoming Request object (to read refresh token from cookies)\n */\n async logout(request: Request): Promise<Response> {\n const cookieHeader = request.headers.get('cookie') ?? ''\n const refreshToken = parseCookie(cookieHeader, 'SIMPLELOGIN_REFRESH_TOKEN')\n\n // Revoke token on IdP (fire and forget - don't block logout on failure)\n if (refreshToken) {\n this.revokeToken(refreshToken).catch(() => {})\n }\n\n // Get auth redirect response\n const authResponse = await this.redirectToAuth()\n\n // Merge clear cookies with auth cookies\n const headers = new Headers(authResponse.headers)\n for (const cookie of clearTokenCookies()) {\n headers.append('Set-Cookie', cookie)\n }\n\n return new Response(null, { status: 302, headers })\n }\n}\n","export class SousaError extends Error {\n constructor(\n message: string,\n public code: string,\n public statusCode?: number\n ) {\n super(message);\n this.name = \"SousaError\";\n }\n}\n\nexport class AuthorizationError extends SousaError {\n constructor(message: string) {\n super(message, \"AUTHORIZATION_ERROR\", 401);\n this.name = \"AuthorizationError\";\n }\n}\n\nexport class TokenError extends SousaError {\n constructor(message: string) {\n super(message, \"TOKEN_ERROR\", 400);\n this.name = \"TokenError\";\n }\n}\n"],"mappings":";AAAA,SAAS,WAAW,YAAY,iBAAiB;;;ACA1C,IAAM,aAAN,cAAyB,MAAM;AAAA,EACpC,YACE,SACO,MACA,YACP;AACA,UAAM,OAAO;AAHN;AACA;AAGP,SAAK,OAAO;AAAA,EACd;AACF;AAEO,IAAM,qBAAN,cAAiC,WAAW;AAAA,EACjD,YAAY,SAAiB;AAC3B,UAAM,SAAS,uBAAuB,GAAG;AACzC,SAAK,OAAO;AAAA,EACd;AACF;AAEO,IAAM,aAAN,cAAyB,WAAW;AAAA,EACzC,YAAY,SAAiB;AAC3B,UAAM,SAAS,eAAe,GAAG;AACjC,SAAK,OAAO;AAAA,EACd;AACF;;;ADRA,IAAM,WAAW;AAEjB,SAAS,gBAAwB;AAC/B,QAAM,QAAQ,IAAI,WAAW,EAAE;AAC/B,SAAO,gBAAgB,KAAK;AAC5B,SAAO,MAAM,KAAK,OAAO,CAAC,MAAM,EAAE,SAAS,EAAE,EAAE,SAAS,GAAG,GAAG,CAAC,EAAE,KAAK,EAAE;AAC1E;AAEA,SAAS,uBAA+B;AACtC,QAAM,QAAQ,IAAI,WAAW,EAAE;AAC/B,SAAO,gBAAgB,KAAK;AAE5B,SAAO,KAAK,OAAO,aAAa,GAAG,KAAK,CAAC,EACtC,QAAQ,OAAO,GAAG,EAClB,QAAQ,OAAO,GAAG,EAClB,QAAQ,MAAM,EAAE;AACrB;AAEA,eAAe,sBAAsB,UAAmC;AACtE,QAAM,UAAU,IAAI,YAAY;AAChC,QAAM,OAAO,QAAQ,OAAO,QAAQ;AACpC,QAAM,OAAO,MAAM,OAAO,OAAO,OAAO,WAAW,IAAI;AAEvD,SAAO,KAAK,OAAO,aAAa,GAAG,IAAI,WAAW,IAAI,CAAC,CAAC,EACrD,QAAQ,OAAO,GAAG,EAClB,QAAQ,OAAO,GAAG,EAClB,QAAQ,MAAM,EAAE;AACrB;AAEA,SAAS,OAAO,KAAiC;AAC/C,MAAI,OAAO,YAAY,eAAe,QAAQ,KAAK;AACjD,WAAO,QAAQ,IAAI,GAAG;AAAA,EACxB;AACA,SAAO;AACT;AAEA,SAAS,YAAY,cAAsB,MAAkC;AAC3E,QAAM,QAAQ,aAAa,MAAM,IAAI,OAAO,cAAc,IAAI,UAAU,CAAC;AACzE,SAAO,QAAQ,mBAAmB,MAAM,CAAC,CAAC,IAAI;AAChD;AAGA,IAAM,iBAAiB,oBAAI,IAAmD;AAC9E,IAAM,uBAAuB,KAAK,KAAK;AAEvC,SAAS,mBAAmB,QAAiC;AAC3D,SAAO;AAAA,IACL,4BAA4B,OAAO,YAAY;AAAA,IAC/C,6BAA6B,OAAO,aAAa;AAAA,EACnD;AACF;AAEA,SAAS,oBAA8B;AACrC,SAAO;AAAA,IACL;AAAA,IACA;AAAA,EACF;AACF;AAEO,IAAM,cAAN,MAAkB;AAAA,EACf;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EAER,YAAY,SAA4B,CAAC,GAAG;AAC1C,SAAK,WAAW,OAAO,YAAY,OAAO,uBAAuB,KAAK;AACtE,SAAK,eAAe,OAAO,gBAAgB,OAAO,2BAA2B,KAAK;AAClF,SAAK,cAAc,OAAO,eAAe,OAAO,0BAA0B,KAAK;AAC/E,SAAK,SAAS,OAAO,UAAU,OAAO,oBAAoB,KAAK;AAC/D,SAAK,UAAU;AAEf,QAAI,CAAC,KAAK,UAAU;AAClB,YAAM,IAAI;AAAA,QACR;AAAA,MACF;AAAA,IACF;AACA,QAAI,CAAC,KAAK,cAAc;AACtB,YAAM,IAAI;AAAA,QACR;AAAA,MACF;AAAA,IACF;AACA,QAAI,CAAC,KAAK,aAAa;AACrB,YAAM,IAAI;AAAA,QACR;AAAA,MACF;AAAA,IACF;AACA,QAAI,CAAC,KAAK,QAAQ;AAChB,YAAM,IAAI,MAAM,0EAA0E;AAAA,IAC5F;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,MAAM,eAAe,UAAmC,CAAC,GAAsB;AAC7E,UAAM,EAAE,KAAK,QAAQ,IAAI,MAAM,KAAK,oBAAoB,OAAO;AAC/D,UAAM,UAAU,IAAI,QAAQ;AAC5B,YAAQ,IAAI,YAAY,GAAG;AAC3B,eAAW,UAAU,SAAS;AAC5B,cAAQ,OAAO,cAAc,MAAM;AAAA,IACrC;AACA,WAAO,IAAI,SAAS,MAAM,EAAE,QAAQ,KAAK,QAAQ,CAAC;AAAA,EACpD;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,oBACJ,UAAmC,CAAC,GACH;AACjC,UAAM,QAAQ,QAAQ,SAAS,cAAc;AAC7C,UAAM,eAAe,qBAAqB;AAC1C,UAAM,gBAAgB,MAAM,sBAAsB,YAAY;AAE9D,UAAM,SAAS,IAAI,gBAAgB;AAAA,MACjC,WAAW,KAAK;AAAA,MAChB,cAAc,KAAK;AAAA,MACnB,eAAe;AAAA,MACf;AAAA,MACA,gBAAgB;AAAA,MAChB,uBAAuB;AAAA,IACzB,CAAC;AAED,QAAI,QAAQ,QAAQ,QAAQ;AAC1B,aAAO,IAAI,SAAS,QAAQ,OAAO,KAAK,GAAG,CAAC;AAAA,IAC9C;AAEA,UAAM,UAAU;AAAA,MACd,qBAAqB,KAAK;AAAA,MAC1B,6BAA6B,YAAY;AAAA,IAC3C;AAEA,WAAO;AAAA,MACL,KAAK,GAAG,KAAK,OAAO,sBAAsB,OAAO,SAAS,CAAC;AAAA,MAC3D;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,MAAM,eAAe,SAAkB,aAAqB,KAA8B;AACxF,UAAM,MAAM,IAAI,IAAI,QAAQ,GAAG;AAC/B,UAAM,OAAO,IAAI,aAAa,IAAI,MAAM;AACxC,UAAM,QAAQ,IAAI,aAAa,IAAI,OAAO;AAE1C,QAAI,CAAC,MAAM;AACT,YAAM,IAAI,mBAAmB,wCAAwC;AAAA,IACvE;AAEA,QAAI,CAAC,OAAO;AACV,YAAM,IAAI,mBAAmB,qCAAqC;AAAA,IACpE;AAEA,UAAM,eAAe,QAAQ,QAAQ,IAAI,QAAQ,KAAK;AACtD,UAAM,gBAAgB,YAAY,cAAc,mBAAmB;AACnE,UAAM,eAAe,YAAY,cAAc,2BAA2B;AAE1E,QAAI,CAAC,eAAe;AAClB,YAAM,IAAI,mBAAmB,kCAAkC;AAAA,IACjE;AAEA,QAAI,UAAU,eAAe;AAC3B,YAAM,IAAI,mBAAmB,yBAAyB;AAAA,IACxD;AAEA,QAAI,CAAC,cAAc;AACjB,YAAM,IAAI,mBAAmB,0CAA0C;AAAA,IACzE;AAGA,UAAM,SAAS,MAAM,KAAK,aAAa,MAAM,YAAY;AAGzD,UAAM,OAAO,MAAM,KAAK,YAAY,OAAO,YAAY;AAGvD,UAAM,UAAU,mBAAmB,MAAM;AACzC,UAAM,UAAU,IAAI,QAAQ;AAC5B,YAAQ,IAAI,YAAY,UAAU;AAClC,eAAW,UAAU,SAAS;AAC5B,cAAQ,OAAO,cAAc,MAAM;AAAA,IACrC;AAEA,YAAQ;AAAA,MACN;AAAA,MACA;AAAA,IACF;AACA,YAAQ;AAAA,MACN;AAAA,MACA;AAAA,IACF;AAEA,UAAM,WAAW,IAAI,SAAS,MAAM,EAAE,QAAQ,KAAK,QAAQ,CAAC;AAE5D,WAAO,EAAE,UAAU,KAAK;AAAA,EAC1B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,MAAM,aAAa,MAAc,cAA8C;AAC7E,UAAM,WAAW,MAAM,MAAM,GAAG,KAAK,OAAO,kBAAkB;AAAA,MAC5D,QAAQ;AAAA,MACR,SAAS;AAAA,QACP,gBAAgB;AAAA,MAClB;AAAA,MACA,MAAM,IAAI,gBAAgB;AAAA,QACxB,YAAY;AAAA,QACZ,WAAW,KAAK;AAAA,QAChB,eAAe,KAAK;AAAA,QACpB,cAAc,KAAK;AAAA,QACnB;AAAA,QACA,eAAe;AAAA,MACjB,CAAC;AAAA,IACH,CAAC;AAED,QAAI,CAAC,SAAS,IAAI;AAChB,YAAM,QAAQ,MAAM,SAAS,KAAK,EAAE,MAAM,OAAO,CAAC,EAAE;AACpD,YAAM,IAAI,WAAW,MAAM,qBAAqB,uCAAuC;AAAA,IACzF;AAEA,WAAO,SAAS,KAAK;AAAA,EACvB;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,aAAa,cAA8C;AAC/D,UAAM,WAAW,MAAM,MAAM,GAAG,KAAK,OAAO,kBAAkB;AAAA,MAC5D,QAAQ;AAAA,MACR,SAAS;AAAA,QACP,gBAAgB;AAAA,MAClB;AAAA,MACA,MAAM,IAAI,gBAAgB;AAAA,QACxB,YAAY;AAAA,QACZ,WAAW,KAAK;AAAA,QAChB,eAAe,KAAK;AAAA,QACpB,eAAe;AAAA,MACjB,CAAC;AAAA,IACH,CAAC;AAED,QAAI,CAAC,SAAS,IAAI;AAChB,YAAM,QAAQ,MAAM,SAAS,KAAK,EAAE,MAAM,OAAO,CAAC,EAAE;AACpD,YAAM,IAAI,WAAW,MAAM,qBAAqB,yBAAyB;AAAA,IAC3E;AAEA,WAAO,SAAS,KAAK;AAAA,EACvB;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,YAAY,aAAwC;AACxD,UAAM,WAAW,MAAM,MAAM,GAAG,KAAK,OAAO,qBAAqB;AAAA,MAC/D,SAAS;AAAA,QACP,eAAe,UAAU,WAAW;AAAA,MACtC;AAAA,IACF,CAAC;AAED,QAAI,CAAC,SAAS,IAAI;AAChB,YAAM,IAAI,mBAAmB,2BAA2B;AAAA,IAC1D;AAEA,WAAO,SAAS,KAAK;AAAA,EACvB;AAAA;AAAA;AAAA;AAAA,EAKA,MAAc,eAAmC;AAC/C,UAAM,SAAS,eAAe,IAAI,KAAK,QAAQ;AAC/C,QAAI,UAAU,KAAK,IAAI,IAAI,OAAO,YAAY,sBAAsB;AAClE,aAAO,OAAO;AAAA,IAChB;AAEA,UAAM,WAAW,MAAM,MAAM,GAAG,KAAK,OAAO,qBAAqB,KAAK,QAAQ,aAAa;AAC3F,QAAI,CAAC,SAAS,IAAI;AAChB,YAAM,IAAI,mBAAmB,4BAA4B;AAAA,IAC3D;AAEA,UAAM,OAAO,MAAM,SAAS,KAAK;AACjC,UAAM,MAAM,KAAK,YAAY;AAC7B,UAAM,MAAM,MAAM,WAAW,KAAK,OAAO;AAEzC,mBAAe,IAAI,KAAK,UAAU,EAAE,KAAK,WAAW,KAAK,IAAI,EAAE,CAAC;AAChE,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,MAAM,aAAa,SAA8C;AAE/D,UAAM,SAAS,QAAQ,OAAO,YAAY;AAC1C,QAAI,WAAW,SAAS,WAAW,QAAQ;AACzC,YAAM,gBAAgB,QAAQ,QAAQ,IAAI,QAAQ,KAAK,QAAQ,QAAQ,IAAI,SAAS;AACpF,UAAI,CAAC,eAAe,WAAW,KAAK,MAAM,GAAG;AAC3C,eAAO;AAAA,MACT;AAAA,IACF;AAEA,UAAM,eAAe,QAAQ,QAAQ,IAAI,QAAQ,KAAK;AACtD,UAAM,cAAc,YAAY,cAAc,0BAA0B;AACxE,UAAM,oBAAoB,YAAY,cAAc,2BAA2B;AAE/E,QAAI,CAAC,aAAa;AAChB,aAAO;AAAA,IACT;AAEA,UAAM,YAAY,MAAM,KAAK,aAAa;AAE1C,QAAI;AAEF,YAAM,UAAU,aAAa,SAAS;AAGtC,YAAM,SAAS,UAAU,WAAW;AAEpC,aAAO,EAAE,QAAQ,aAAa,SAAS,IAAI,QAAQ,EAAE;AAAA,IACvD,SAAS,OAAO;AAEd,YAAM,YACJ,iBAAiB,SACjB,UAAU,SACT,MAA2B,SAAS;AAEvC,UAAI,CAAC,aAAa,CAAC,mBAAmB;AACpC,eAAO;AAAA,MACT;AAGA,UAAI;AACF,cAAM,SAAS,MAAM,KAAK,aAAa,iBAAiB;AACxD,cAAM,SAAS,UAAU,OAAO,YAAY;AAG5C,cAAM,UAAU,IAAI,QAAQ;AAC5B,mBAAW,UAAU,mBAAmB,MAAM,GAAG;AAC/C,kBAAQ,OAAO,cAAc,MAAM;AAAA,QACrC;AAEA,eAAO,EAAE,QAAQ,aAAa,OAAO,cAAc,QAAQ;AAAA,MAC7D,QAAQ;AACN,eAAO;AAAA,MACT;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,MAAc,YAAY,cAAqC;AAC7D,UAAM,MAAM,GAAG,KAAK,OAAO,mBAAmB;AAAA,MAC5C,QAAQ;AAAA,MACR,SAAS,EAAE,gBAAgB,oCAAoC;AAAA,MAC/D,MAAM,IAAI,gBAAgB;AAAA,QACxB,OAAO;AAAA,QACP,WAAW,KAAK;AAAA,QAChB,eAAe,KAAK;AAAA,MACtB,CAAC;AAAA,IACH,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,MAAM,OAAO,SAAqC;AAChD,UAAM,eAAe,QAAQ,QAAQ,IAAI,QAAQ,KAAK;AACtD,UAAM,eAAe,YAAY,cAAc,2BAA2B;AAG1E,QAAI,cAAc;AAChB,WAAK,YAAY,YAAY,EAAE,MAAM,MAAM;AAAA,MAAC,CAAC;AAAA,IAC/C;AAGA,UAAM,eAAe,MAAM,KAAK,eAAe;AAG/C,UAAM,UAAU,IAAI,QAAQ,aAAa,OAAO;AAChD,eAAW,UAAU,kBAAkB,GAAG;AACxC,cAAQ,OAAO,cAAc,MAAM;AAAA,IACrC;AAEA,WAAO,IAAI,SAAS,MAAM,EAAE,QAAQ,KAAK,QAAQ,CAAC;AAAA,EACpD;AACF;","names":[]}
|