@simple-login/sdk 1.0.0 → 1.2.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/dist/index.cjs +173 -22
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +62 -12
- package/dist/index.d.ts +62 -12
- package/dist/index.js +173 -22
- package/dist/index.js.map +1 -1
- package/package.json +4 -1
package/dist/index.cjs
CHANGED
|
@@ -27,6 +27,9 @@ __export(index_exports, {
|
|
|
27
27
|
});
|
|
28
28
|
module.exports = __toCommonJS(index_exports);
|
|
29
29
|
|
|
30
|
+
// src/client.ts
|
|
31
|
+
var import_jose = require("jose");
|
|
32
|
+
|
|
30
33
|
// src/errors.ts
|
|
31
34
|
var SousaError = class extends Error {
|
|
32
35
|
constructor(message, code, statusCode) {
|
|
@@ -56,6 +59,17 @@ function generateState() {
|
|
|
56
59
|
crypto.getRandomValues(array);
|
|
57
60
|
return Array.from(array, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
58
61
|
}
|
|
62
|
+
function generateCodeVerifier() {
|
|
63
|
+
const array = new Uint8Array(32);
|
|
64
|
+
crypto.getRandomValues(array);
|
|
65
|
+
return btoa(String.fromCharCode(...array)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
|
|
66
|
+
}
|
|
67
|
+
async function generateCodeChallenge(verifier) {
|
|
68
|
+
const encoder = new TextEncoder();
|
|
69
|
+
const data = encoder.encode(verifier);
|
|
70
|
+
const hash = await crypto.subtle.digest("SHA-256", data);
|
|
71
|
+
return btoa(String.fromCharCode(...new Uint8Array(hash))).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
|
|
72
|
+
}
|
|
59
73
|
function getEnv(key) {
|
|
60
74
|
if (typeof process !== "undefined" && process.env) {
|
|
61
75
|
return process.env[key];
|
|
@@ -66,6 +80,20 @@ function parseCookie(cookieHeader, name) {
|
|
|
66
80
|
const match = cookieHeader.match(new RegExp(`(?:^|;\\s*)${name}=([^;]*)`));
|
|
67
81
|
return match ? decodeURIComponent(match[1]) : void 0;
|
|
68
82
|
}
|
|
83
|
+
var publicKeyCache = /* @__PURE__ */ new Map();
|
|
84
|
+
var PUBLIC_KEY_CACHE_TTL = 60 * 60 * 1e3;
|
|
85
|
+
function createTokenCookies(tokens) {
|
|
86
|
+
return [
|
|
87
|
+
`SIMPLELOGIN_ACCESS_TOKEN=${tokens.access_token}; HttpOnly; Secure; SameSite=Lax; Path=/`,
|
|
88
|
+
`SIMPLELOGIN_REFRESH_TOKEN=${tokens.refresh_token}; HttpOnly; Secure; SameSite=Lax; Path=/`
|
|
89
|
+
];
|
|
90
|
+
}
|
|
91
|
+
function clearTokenCookies() {
|
|
92
|
+
return [
|
|
93
|
+
"SIMPLELOGIN_ACCESS_TOKEN=; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=0",
|
|
94
|
+
"SIMPLELOGIN_REFRESH_TOKEN=; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=0"
|
|
95
|
+
];
|
|
96
|
+
}
|
|
69
97
|
var SimpleLogin = class {
|
|
70
98
|
clientId;
|
|
71
99
|
clientSecret;
|
|
@@ -93,50 +121,57 @@ var SimpleLogin = class {
|
|
|
93
121
|
}
|
|
94
122
|
}
|
|
95
123
|
/**
|
|
96
|
-
* Returns a Response that redirects to the authorization URL with
|
|
124
|
+
* Returns a Response that redirects to the authorization URL with state and PKCE cookies set.
|
|
97
125
|
* @param options - Optional scopes or custom state
|
|
98
126
|
* @returns A 302 redirect Response ready to be returned from your route handler
|
|
99
127
|
*/
|
|
100
|
-
redirectToAuth(options = {}) {
|
|
101
|
-
const { url,
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
});
|
|
128
|
+
async redirectToAuth(options = {}) {
|
|
129
|
+
const { url, cookies } = await this.getAuthorizationUrl(options);
|
|
130
|
+
const headers = new Headers();
|
|
131
|
+
headers.set("Location", url);
|
|
132
|
+
for (const cookie of cookies) {
|
|
133
|
+
headers.append("Set-Cookie", cookie);
|
|
134
|
+
}
|
|
135
|
+
return new Response(null, { status: 302, headers });
|
|
109
136
|
}
|
|
110
137
|
/**
|
|
111
138
|
* Generate the authorization URL to redirect users for sign-in
|
|
112
|
-
* @returns The authorization URL, state
|
|
139
|
+
* @returns The authorization URL, state, code verifier, and ready-to-use Set-Cookie header values
|
|
113
140
|
*/
|
|
114
|
-
getAuthorizationUrl(options = {}) {
|
|
141
|
+
async getAuthorizationUrl(options = {}) {
|
|
115
142
|
const state = options.state ?? generateState();
|
|
143
|
+
const codeVerifier = generateCodeVerifier();
|
|
144
|
+
const codeChallenge = await generateCodeChallenge(codeVerifier);
|
|
116
145
|
const params = new URLSearchParams({
|
|
117
146
|
client_id: this.clientId,
|
|
118
147
|
redirect_uri: this.redirectUri,
|
|
119
148
|
response_type: "code",
|
|
120
|
-
state
|
|
149
|
+
state,
|
|
150
|
+
code_challenge: codeChallenge,
|
|
151
|
+
code_challenge_method: "S256"
|
|
121
152
|
});
|
|
122
153
|
if (options.scopes?.length) {
|
|
123
154
|
params.set("scope", options.scopes.join(" "));
|
|
124
155
|
}
|
|
125
|
-
const
|
|
156
|
+
const cookies = [
|
|
157
|
+
`SIMPLELOGIN_STATE=${state}; HttpOnly; Secure; SameSite=Lax; Max-Age=600; Path=/`,
|
|
158
|
+
`SIMPLELOGIN_CODE_VERIFIER=${codeVerifier}; HttpOnly; Secure; SameSite=Lax; Max-Age=600; Path=/`
|
|
159
|
+
];
|
|
126
160
|
return {
|
|
127
161
|
url: `${this.baseUrl}/v1/auth/authorize?${params.toString()}`,
|
|
128
162
|
state,
|
|
129
|
-
|
|
163
|
+
codeVerifier,
|
|
164
|
+
cookies
|
|
130
165
|
};
|
|
131
166
|
}
|
|
132
167
|
/**
|
|
133
|
-
*
|
|
134
|
-
* Automatically extracts state from SIMPLELOGIN_STATE cookie and code/state from URL.
|
|
168
|
+
* Handle the OAuth callback: verify state, exchange code, fetch user info, and return a redirect Response.
|
|
135
169
|
* @param request - The incoming Request object from the callback
|
|
136
|
-
* @
|
|
170
|
+
* @param redirectTo - URL to redirect to after successful authentication (default: '/')
|
|
171
|
+
* @returns CallbackResult with redirect Response (cookies set) and user info to store
|
|
137
172
|
* @throws AuthorizationError if state is missing or doesn't match
|
|
138
173
|
*/
|
|
139
|
-
async handleCallback(request) {
|
|
174
|
+
async handleCallback(request, redirectTo = "/") {
|
|
140
175
|
const url = new URL(request.url);
|
|
141
176
|
const code = url.searchParams.get("code");
|
|
142
177
|
const state = url.searchParams.get("state");
|
|
@@ -148,18 +183,41 @@ var SimpleLogin = class {
|
|
|
148
183
|
}
|
|
149
184
|
const cookieHeader = request.headers.get("cookie") ?? "";
|
|
150
185
|
const expectedState = parseCookie(cookieHeader, "SIMPLELOGIN_STATE");
|
|
186
|
+
const codeVerifier = parseCookie(cookieHeader, "SIMPLELOGIN_CODE_VERIFIER");
|
|
151
187
|
if (!expectedState) {
|
|
152
188
|
throw new AuthorizationError("Missing SIMPLELOGIN_STATE cookie");
|
|
153
189
|
}
|
|
154
190
|
if (state !== expectedState) {
|
|
155
191
|
throw new AuthorizationError("Invalid state parameter");
|
|
156
192
|
}
|
|
157
|
-
|
|
193
|
+
if (!codeVerifier) {
|
|
194
|
+
throw new AuthorizationError("Missing SIMPLELOGIN_CODE_VERIFIER cookie");
|
|
195
|
+
}
|
|
196
|
+
const tokens = await this.exchangeCode(code, codeVerifier);
|
|
197
|
+
const user = await this.getUserInfo(tokens.access_token);
|
|
198
|
+
const cookies = createTokenCookies(tokens);
|
|
199
|
+
const headers = new Headers();
|
|
200
|
+
headers.set("Location", redirectTo);
|
|
201
|
+
for (const cookie of cookies) {
|
|
202
|
+
headers.append("Set-Cookie", cookie);
|
|
203
|
+
}
|
|
204
|
+
headers.append(
|
|
205
|
+
"Set-Cookie",
|
|
206
|
+
"SIMPLELOGIN_STATE=; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=0"
|
|
207
|
+
);
|
|
208
|
+
headers.append(
|
|
209
|
+
"Set-Cookie",
|
|
210
|
+
"SIMPLELOGIN_CODE_VERIFIER=; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=0"
|
|
211
|
+
);
|
|
212
|
+
const response = new Response(null, { status: 302, headers });
|
|
213
|
+
return { response, user };
|
|
158
214
|
}
|
|
159
215
|
/**
|
|
160
216
|
* Exchange an authorization code for access and refresh tokens
|
|
217
|
+
* @param code - The authorization code from the callback
|
|
218
|
+
* @param codeVerifier - The PKCE code verifier
|
|
161
219
|
*/
|
|
162
|
-
async exchangeCode(code) {
|
|
220
|
+
async exchangeCode(code, codeVerifier) {
|
|
163
221
|
const response = await fetch(`${this.baseUrl}/v1/auth/token`, {
|
|
164
222
|
method: "POST",
|
|
165
223
|
headers: {
|
|
@@ -170,7 +228,8 @@ var SimpleLogin = class {
|
|
|
170
228
|
client_id: this.clientId,
|
|
171
229
|
client_secret: this.clientSecret,
|
|
172
230
|
redirect_uri: this.redirectUri,
|
|
173
|
-
code
|
|
231
|
+
code,
|
|
232
|
+
code_verifier: codeVerifier
|
|
174
233
|
})
|
|
175
234
|
});
|
|
176
235
|
if (!response.ok) {
|
|
@@ -215,6 +274,98 @@ var SimpleLogin = class {
|
|
|
215
274
|
}
|
|
216
275
|
return response.json();
|
|
217
276
|
}
|
|
277
|
+
/**
|
|
278
|
+
* Fetch and cache the public key for JWT verification
|
|
279
|
+
*/
|
|
280
|
+
async getPublicKey() {
|
|
281
|
+
const cached = publicKeyCache.get(this.clientId);
|
|
282
|
+
if (cached && Date.now() - cached.fetchedAt < PUBLIC_KEY_CACHE_TTL) {
|
|
283
|
+
return cached.key;
|
|
284
|
+
}
|
|
285
|
+
const response = await fetch(`${this.baseUrl}/api/applications/${this.clientId}/public-key`);
|
|
286
|
+
if (!response.ok) {
|
|
287
|
+
throw new AuthorizationError("Failed to fetch public key");
|
|
288
|
+
}
|
|
289
|
+
const pem = await response.text();
|
|
290
|
+
const key = await (0, import_jose.importSPKI)(pem, "RS256");
|
|
291
|
+
publicKeyCache.set(this.clientId, { key, fetchedAt: Date.now() });
|
|
292
|
+
return key;
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* Authenticate a request by verifying the access token from cookies.
|
|
296
|
+
* No network calls except when refreshing expired tokens.
|
|
297
|
+
* @param request - The incoming Request object
|
|
298
|
+
* @param options - Optional settings for CSRF protection
|
|
299
|
+
* @returns AuthResult if authenticated, null if not authenticated
|
|
300
|
+
*/
|
|
301
|
+
async authenticate(request, options) {
|
|
302
|
+
const method = request.method.toUpperCase();
|
|
303
|
+
if (method !== "GET" && method !== "HEAD" && options?.allowedOrigin) {
|
|
304
|
+
const origin = request.headers.get("origin") || request.headers.get("referer");
|
|
305
|
+
if (!origin?.startsWith(options.allowedOrigin)) {
|
|
306
|
+
return null;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
const cookieHeader = request.headers.get("cookie") ?? "";
|
|
310
|
+
const accessToken = parseCookie(cookieHeader, "SIMPLELOGIN_ACCESS_TOKEN");
|
|
311
|
+
const refreshTokenValue = parseCookie(cookieHeader, "SIMPLELOGIN_REFRESH_TOKEN");
|
|
312
|
+
if (!accessToken) {
|
|
313
|
+
return null;
|
|
314
|
+
}
|
|
315
|
+
const publicKey = await this.getPublicKey();
|
|
316
|
+
try {
|
|
317
|
+
await (0, import_jose.jwtVerify)(accessToken, publicKey);
|
|
318
|
+
const claims = (0, import_jose.decodeJwt)(accessToken);
|
|
319
|
+
return { claims, accessToken };
|
|
320
|
+
} catch (error) {
|
|
321
|
+
const isExpired = error instanceof Error && "code" in error && error.code === "ERR_JWT_EXPIRED";
|
|
322
|
+
if (!isExpired || !refreshTokenValue) {
|
|
323
|
+
return null;
|
|
324
|
+
}
|
|
325
|
+
try {
|
|
326
|
+
const tokens = await this.refreshToken(refreshTokenValue);
|
|
327
|
+
const claims = (0, import_jose.decodeJwt)(tokens.access_token);
|
|
328
|
+
const cookies = createTokenCookies(tokens);
|
|
329
|
+
return { claims, accessToken: tokens.access_token, cookies };
|
|
330
|
+
} catch {
|
|
331
|
+
return null;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* Revoke a refresh token on the IdP
|
|
337
|
+
*/
|
|
338
|
+
async revokeToken(refreshToken) {
|
|
339
|
+
await fetch(`${this.baseUrl}/v1/auth/revoke`, {
|
|
340
|
+
method: "POST",
|
|
341
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
342
|
+
body: new URLSearchParams({
|
|
343
|
+
token: refreshToken,
|
|
344
|
+
client_id: this.clientId,
|
|
345
|
+
client_secret: this.clientSecret
|
|
346
|
+
})
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* Logout: revoke refresh token on IdP and clear cookies.
|
|
351
|
+
* @param request - The incoming Request object (to read refresh token from cookies)
|
|
352
|
+
* @param redirectTo - URL to redirect to after logout (default: '/')
|
|
353
|
+
*/
|
|
354
|
+
async logout(request, redirectTo = "/") {
|
|
355
|
+
const cookieHeader = request.headers.get("cookie") ?? "";
|
|
356
|
+
const refreshToken = parseCookie(cookieHeader, "SIMPLELOGIN_REFRESH_TOKEN");
|
|
357
|
+
if (refreshToken) {
|
|
358
|
+
this.revokeToken(refreshToken).catch(() => {
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
const cookies = clearTokenCookies();
|
|
362
|
+
const headers = new Headers();
|
|
363
|
+
headers.set("Location", redirectTo);
|
|
364
|
+
for (const cookie of cookies) {
|
|
365
|
+
headers.append("Set-Cookie", cookie);
|
|
366
|
+
}
|
|
367
|
+
return new Response(null, { status: 302, headers });
|
|
368
|
+
}
|
|
218
369
|
};
|
|
219
370
|
// Annotate the CommonJS export names for ESM import in node:
|
|
220
371
|
0 && (module.exports = {
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/errors.ts","../src/client.ts"],"sourcesContent":["export { SimpleLogin } from './client'\nexport { AuthorizationError, SousaError, TokenError } from './errors'\nexport type {\n AuthorizationUrlOptions,\n AuthorizationUrlResult,\n SimpleLoginConfig,\n TokenResponse,\n UserInfo,\n} from './types'\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","import { AuthorizationError, TokenError } from './errors'\nimport type {\n AuthorizationUrlOptions,\n AuthorizationUrlResult,\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 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\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 the state cookie 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 redirectToAuth(options: AuthorizationUrlOptions = {}): Response {\n const { url, cookie } = this.getAuthorizationUrl(options)\n return new Response(null, {\n status: 302,\n headers: {\n Location: url,\n 'Set-Cookie': cookie,\n },\n })\n }\n\n /**\n * Generate the authorization URL to redirect users for sign-in\n * @returns The authorization URL, state parameter, and a ready-to-use Set-Cookie header value\n */\n getAuthorizationUrl(options: AuthorizationUrlOptions = {}): AuthorizationUrlResult {\n const state = options.state ?? generateState()\n\n const params = new URLSearchParams({\n client_id: this.clientId,\n redirect_uri: this.redirectUri,\n response_type: 'code',\n state,\n })\n\n if (options.scopes?.length) {\n params.set('scope', options.scopes.join(' '))\n }\n\n const cookie = `SIMPLELOGIN_STATE=${state}; HttpOnly; Secure; SameSite=Lax; Max-Age=600; Path=/`\n\n return {\n url: `${this.baseUrl}/v1/auth/authorize?${params.toString()}`,\n state,\n cookie,\n }\n }\n\n /**\n * Verify the OAuth callback request and exchange the code for tokens.\n * Automatically extracts state from SIMPLELOGIN_STATE cookie and code/state from URL.\n * @param request - The incoming Request object from the callback\n * @returns The token response if verification succeeds\n * @throws AuthorizationError if state is missing or doesn't match\n */\n async handleCallback(request: Request): Promise<TokenResponse> {\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\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 return this.exchangeCode(code)\n }\n\n /**\n * Exchange an authorization code for access and refresh tokens\n */\n async exchangeCode(code: 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 }),\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"],"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 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":";;;;;;;;;;;;;;;;;;;;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,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,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;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,oBAAoB,UAAmC,CAAC,GAAoC;AAChG,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,MAAM,MAAM,SAAS,KAAK;AAChC,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,aACJ,SACA,SAC4B;AAE5B,UAAM,SAAS,QAAQ,OAAO,YAAY;AAC1C,QAAI,WAAW,SAAS,WAAW,UAAU,SAAS,eAAe;AACnE,YAAM,SAAS,QAAQ,QAAQ,IAAI,QAAQ,KAAK,QAAQ,QAAQ,IAAI,SAAS;AAC7E,UAAI,CAAC,QAAQ,WAAW,QAAQ,aAAa,GAAG;AAC9C,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,YAAY;AAAA,IAC/B,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;AAC5C,cAAM,UAAU,mBAAmB,MAAM;AAEzC,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;AAAA,EAOA,MAAM,OAAO,SAAkB,aAAqB,KAAwB;AAC1E,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,UAAU,kBAAkB;AAClC,UAAM,UAAU,IAAI,QAAQ;AAC5B,YAAQ,IAAI,YAAY,UAAU;AAClC,eAAW,UAAU,SAAS;AAC5B,cAAQ,OAAO,cAAc,MAAM;AAAA,IACrC;AACA,WAAO,IAAI,SAAS,MAAM,EAAE,QAAQ,KAAK,QAAQ,CAAC;AAAA,EACpD;AACF;","names":[]}
|
package/dist/index.d.cts
CHANGED
|
@@ -10,15 +10,39 @@ interface AuthorizationUrlOptions {
|
|
|
10
10
|
interface AuthorizationUrlResult {
|
|
11
11
|
url: string;
|
|
12
12
|
state: string;
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
codeVerifier: string;
|
|
14
|
+
/** Ready-to-use Set-Cookie header values for state and code verifier */
|
|
15
|
+
cookies: string[];
|
|
15
16
|
}
|
|
16
17
|
interface TokenResponse {
|
|
17
18
|
access_token: string;
|
|
18
19
|
refresh_token: string;
|
|
20
|
+
token_id: string;
|
|
19
21
|
token_type: string;
|
|
20
22
|
expires_in: number;
|
|
21
23
|
}
|
|
24
|
+
interface AccessTokenClaims {
|
|
25
|
+
sub: string;
|
|
26
|
+
application_id: string;
|
|
27
|
+
organization_id?: string;
|
|
28
|
+
is_master?: boolean;
|
|
29
|
+
type: 'access';
|
|
30
|
+
exp: number;
|
|
31
|
+
iat: number;
|
|
32
|
+
}
|
|
33
|
+
interface AuthResult {
|
|
34
|
+
/** Decoded claims from the access token JWT (no network call) */
|
|
35
|
+
claims: AccessTokenClaims;
|
|
36
|
+
accessToken: string;
|
|
37
|
+
/** Set-Cookie headers to set if tokens were refreshed */
|
|
38
|
+
cookies?: string[];
|
|
39
|
+
}
|
|
40
|
+
interface CallbackResult {
|
|
41
|
+
/** Redirect Response with token cookies set */
|
|
42
|
+
response: Response;
|
|
43
|
+
/** Full user info fetched from userinfo endpoint (store this in your BFF) */
|
|
44
|
+
user: UserInfo;
|
|
45
|
+
}
|
|
22
46
|
interface UserInfo {
|
|
23
47
|
sub: string;
|
|
24
48
|
id: string;
|
|
@@ -48,28 +72,30 @@ declare class SimpleLogin {
|
|
|
48
72
|
private baseUrl;
|
|
49
73
|
constructor(config?: SimpleLoginConfig);
|
|
50
74
|
/**
|
|
51
|
-
* Returns a Response that redirects to the authorization URL with
|
|
75
|
+
* Returns a Response that redirects to the authorization URL with state and PKCE cookies set.
|
|
52
76
|
* @param options - Optional scopes or custom state
|
|
53
77
|
* @returns A 302 redirect Response ready to be returned from your route handler
|
|
54
78
|
*/
|
|
55
|
-
redirectToAuth(options?: AuthorizationUrlOptions): Response
|
|
79
|
+
redirectToAuth(options?: AuthorizationUrlOptions): Promise<Response>;
|
|
56
80
|
/**
|
|
57
81
|
* Generate the authorization URL to redirect users for sign-in
|
|
58
|
-
* @returns The authorization URL, state
|
|
82
|
+
* @returns The authorization URL, state, code verifier, and ready-to-use Set-Cookie header values
|
|
59
83
|
*/
|
|
60
|
-
getAuthorizationUrl(options?: AuthorizationUrlOptions): AuthorizationUrlResult
|
|
84
|
+
getAuthorizationUrl(options?: AuthorizationUrlOptions): Promise<AuthorizationUrlResult>;
|
|
61
85
|
/**
|
|
62
|
-
*
|
|
63
|
-
* Automatically extracts state from SIMPLELOGIN_STATE cookie and code/state from URL.
|
|
86
|
+
* Handle the OAuth callback: verify state, exchange code, fetch user info, and return a redirect Response.
|
|
64
87
|
* @param request - The incoming Request object from the callback
|
|
65
|
-
* @
|
|
88
|
+
* @param redirectTo - URL to redirect to after successful authentication (default: '/')
|
|
89
|
+
* @returns CallbackResult with redirect Response (cookies set) and user info to store
|
|
66
90
|
* @throws AuthorizationError if state is missing or doesn't match
|
|
67
91
|
*/
|
|
68
|
-
handleCallback(request: Request): Promise<
|
|
92
|
+
handleCallback(request: Request, redirectTo?: string): Promise<CallbackResult>;
|
|
69
93
|
/**
|
|
70
94
|
* Exchange an authorization code for access and refresh tokens
|
|
95
|
+
* @param code - The authorization code from the callback
|
|
96
|
+
* @param codeVerifier - The PKCE code verifier
|
|
71
97
|
*/
|
|
72
|
-
exchangeCode(code: string): Promise<TokenResponse>;
|
|
98
|
+
exchangeCode(code: string, codeVerifier: string): Promise<TokenResponse>;
|
|
73
99
|
/**
|
|
74
100
|
* Refresh an access token using a refresh token
|
|
75
101
|
*/
|
|
@@ -78,6 +104,30 @@ declare class SimpleLogin {
|
|
|
78
104
|
* Get user information using an access token
|
|
79
105
|
*/
|
|
80
106
|
getUserInfo(accessToken: string): Promise<UserInfo>;
|
|
107
|
+
/**
|
|
108
|
+
* Fetch and cache the public key for JWT verification
|
|
109
|
+
*/
|
|
110
|
+
private getPublicKey;
|
|
111
|
+
/**
|
|
112
|
+
* Authenticate a request by verifying the access token from cookies.
|
|
113
|
+
* No network calls except when refreshing expired tokens.
|
|
114
|
+
* @param request - The incoming Request object
|
|
115
|
+
* @param options - Optional settings for CSRF protection
|
|
116
|
+
* @returns AuthResult if authenticated, null if not authenticated
|
|
117
|
+
*/
|
|
118
|
+
authenticate(request: Request, options?: {
|
|
119
|
+
allowedOrigin?: string;
|
|
120
|
+
}): Promise<AuthResult | null>;
|
|
121
|
+
/**
|
|
122
|
+
* Revoke a refresh token on the IdP
|
|
123
|
+
*/
|
|
124
|
+
private revokeToken;
|
|
125
|
+
/**
|
|
126
|
+
* Logout: revoke refresh token on IdP and clear cookies.
|
|
127
|
+
* @param request - The incoming Request object (to read refresh token from cookies)
|
|
128
|
+
* @param redirectTo - URL to redirect to after logout (default: '/')
|
|
129
|
+
*/
|
|
130
|
+
logout(request: Request, redirectTo?: string): Promise<Response>;
|
|
81
131
|
}
|
|
82
132
|
|
|
83
133
|
declare class SousaError extends Error {
|
|
@@ -92,4 +142,4 @@ declare class TokenError extends SousaError {
|
|
|
92
142
|
constructor(message: string);
|
|
93
143
|
}
|
|
94
144
|
|
|
95
|
-
export { AuthorizationError, type AuthorizationUrlOptions, type AuthorizationUrlResult, SimpleLogin, type SimpleLoginConfig, SousaError, TokenError, type TokenResponse, type UserInfo };
|
|
145
|
+
export { type AccessTokenClaims, type AuthResult, AuthorizationError, type AuthorizationUrlOptions, type AuthorizationUrlResult, type CallbackResult, SimpleLogin, type SimpleLoginConfig, SousaError, TokenError, type TokenResponse, type UserInfo };
|
package/dist/index.d.ts
CHANGED
|
@@ -10,15 +10,39 @@ interface AuthorizationUrlOptions {
|
|
|
10
10
|
interface AuthorizationUrlResult {
|
|
11
11
|
url: string;
|
|
12
12
|
state: string;
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
codeVerifier: string;
|
|
14
|
+
/** Ready-to-use Set-Cookie header values for state and code verifier */
|
|
15
|
+
cookies: string[];
|
|
15
16
|
}
|
|
16
17
|
interface TokenResponse {
|
|
17
18
|
access_token: string;
|
|
18
19
|
refresh_token: string;
|
|
20
|
+
token_id: string;
|
|
19
21
|
token_type: string;
|
|
20
22
|
expires_in: number;
|
|
21
23
|
}
|
|
24
|
+
interface AccessTokenClaims {
|
|
25
|
+
sub: string;
|
|
26
|
+
application_id: string;
|
|
27
|
+
organization_id?: string;
|
|
28
|
+
is_master?: boolean;
|
|
29
|
+
type: 'access';
|
|
30
|
+
exp: number;
|
|
31
|
+
iat: number;
|
|
32
|
+
}
|
|
33
|
+
interface AuthResult {
|
|
34
|
+
/** Decoded claims from the access token JWT (no network call) */
|
|
35
|
+
claims: AccessTokenClaims;
|
|
36
|
+
accessToken: string;
|
|
37
|
+
/** Set-Cookie headers to set if tokens were refreshed */
|
|
38
|
+
cookies?: string[];
|
|
39
|
+
}
|
|
40
|
+
interface CallbackResult {
|
|
41
|
+
/** Redirect Response with token cookies set */
|
|
42
|
+
response: Response;
|
|
43
|
+
/** Full user info fetched from userinfo endpoint (store this in your BFF) */
|
|
44
|
+
user: UserInfo;
|
|
45
|
+
}
|
|
22
46
|
interface UserInfo {
|
|
23
47
|
sub: string;
|
|
24
48
|
id: string;
|
|
@@ -48,28 +72,30 @@ declare class SimpleLogin {
|
|
|
48
72
|
private baseUrl;
|
|
49
73
|
constructor(config?: SimpleLoginConfig);
|
|
50
74
|
/**
|
|
51
|
-
* Returns a Response that redirects to the authorization URL with
|
|
75
|
+
* Returns a Response that redirects to the authorization URL with state and PKCE cookies set.
|
|
52
76
|
* @param options - Optional scopes or custom state
|
|
53
77
|
* @returns A 302 redirect Response ready to be returned from your route handler
|
|
54
78
|
*/
|
|
55
|
-
redirectToAuth(options?: AuthorizationUrlOptions): Response
|
|
79
|
+
redirectToAuth(options?: AuthorizationUrlOptions): Promise<Response>;
|
|
56
80
|
/**
|
|
57
81
|
* Generate the authorization URL to redirect users for sign-in
|
|
58
|
-
* @returns The authorization URL, state
|
|
82
|
+
* @returns The authorization URL, state, code verifier, and ready-to-use Set-Cookie header values
|
|
59
83
|
*/
|
|
60
|
-
getAuthorizationUrl(options?: AuthorizationUrlOptions): AuthorizationUrlResult
|
|
84
|
+
getAuthorizationUrl(options?: AuthorizationUrlOptions): Promise<AuthorizationUrlResult>;
|
|
61
85
|
/**
|
|
62
|
-
*
|
|
63
|
-
* Automatically extracts state from SIMPLELOGIN_STATE cookie and code/state from URL.
|
|
86
|
+
* Handle the OAuth callback: verify state, exchange code, fetch user info, and return a redirect Response.
|
|
64
87
|
* @param request - The incoming Request object from the callback
|
|
65
|
-
* @
|
|
88
|
+
* @param redirectTo - URL to redirect to after successful authentication (default: '/')
|
|
89
|
+
* @returns CallbackResult with redirect Response (cookies set) and user info to store
|
|
66
90
|
* @throws AuthorizationError if state is missing or doesn't match
|
|
67
91
|
*/
|
|
68
|
-
handleCallback(request: Request): Promise<
|
|
92
|
+
handleCallback(request: Request, redirectTo?: string): Promise<CallbackResult>;
|
|
69
93
|
/**
|
|
70
94
|
* Exchange an authorization code for access and refresh tokens
|
|
95
|
+
* @param code - The authorization code from the callback
|
|
96
|
+
* @param codeVerifier - The PKCE code verifier
|
|
71
97
|
*/
|
|
72
|
-
exchangeCode(code: string): Promise<TokenResponse>;
|
|
98
|
+
exchangeCode(code: string, codeVerifier: string): Promise<TokenResponse>;
|
|
73
99
|
/**
|
|
74
100
|
* Refresh an access token using a refresh token
|
|
75
101
|
*/
|
|
@@ -78,6 +104,30 @@ declare class SimpleLogin {
|
|
|
78
104
|
* Get user information using an access token
|
|
79
105
|
*/
|
|
80
106
|
getUserInfo(accessToken: string): Promise<UserInfo>;
|
|
107
|
+
/**
|
|
108
|
+
* Fetch and cache the public key for JWT verification
|
|
109
|
+
*/
|
|
110
|
+
private getPublicKey;
|
|
111
|
+
/**
|
|
112
|
+
* Authenticate a request by verifying the access token from cookies.
|
|
113
|
+
* No network calls except when refreshing expired tokens.
|
|
114
|
+
* @param request - The incoming Request object
|
|
115
|
+
* @param options - Optional settings for CSRF protection
|
|
116
|
+
* @returns AuthResult if authenticated, null if not authenticated
|
|
117
|
+
*/
|
|
118
|
+
authenticate(request: Request, options?: {
|
|
119
|
+
allowedOrigin?: string;
|
|
120
|
+
}): Promise<AuthResult | null>;
|
|
121
|
+
/**
|
|
122
|
+
* Revoke a refresh token on the IdP
|
|
123
|
+
*/
|
|
124
|
+
private revokeToken;
|
|
125
|
+
/**
|
|
126
|
+
* Logout: revoke refresh token on IdP and clear cookies.
|
|
127
|
+
* @param request - The incoming Request object (to read refresh token from cookies)
|
|
128
|
+
* @param redirectTo - URL to redirect to after logout (default: '/')
|
|
129
|
+
*/
|
|
130
|
+
logout(request: Request, redirectTo?: string): Promise<Response>;
|
|
81
131
|
}
|
|
82
132
|
|
|
83
133
|
declare class SousaError extends Error {
|
|
@@ -92,4 +142,4 @@ declare class TokenError extends SousaError {
|
|
|
92
142
|
constructor(message: string);
|
|
93
143
|
}
|
|
94
144
|
|
|
95
|
-
export { AuthorizationError, type AuthorizationUrlOptions, type AuthorizationUrlResult, SimpleLogin, type SimpleLoginConfig, SousaError, TokenError, type TokenResponse, type UserInfo };
|
|
145
|
+
export { type AccessTokenClaims, type AuthResult, AuthorizationError, type AuthorizationUrlOptions, type AuthorizationUrlResult, type CallbackResult, SimpleLogin, type SimpleLoginConfig, SousaError, TokenError, type TokenResponse, type UserInfo };
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
// src/client.ts
|
|
2
|
+
import { decodeJwt, importSPKI, jwtVerify } from "jose";
|
|
3
|
+
|
|
1
4
|
// src/errors.ts
|
|
2
5
|
var SousaError = class extends Error {
|
|
3
6
|
constructor(message, code, statusCode) {
|
|
@@ -27,6 +30,17 @@ function generateState() {
|
|
|
27
30
|
crypto.getRandomValues(array);
|
|
28
31
|
return Array.from(array, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
29
32
|
}
|
|
33
|
+
function generateCodeVerifier() {
|
|
34
|
+
const array = new Uint8Array(32);
|
|
35
|
+
crypto.getRandomValues(array);
|
|
36
|
+
return btoa(String.fromCharCode(...array)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
|
|
37
|
+
}
|
|
38
|
+
async function generateCodeChallenge(verifier) {
|
|
39
|
+
const encoder = new TextEncoder();
|
|
40
|
+
const data = encoder.encode(verifier);
|
|
41
|
+
const hash = await crypto.subtle.digest("SHA-256", data);
|
|
42
|
+
return btoa(String.fromCharCode(...new Uint8Array(hash))).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
|
|
43
|
+
}
|
|
30
44
|
function getEnv(key) {
|
|
31
45
|
if (typeof process !== "undefined" && process.env) {
|
|
32
46
|
return process.env[key];
|
|
@@ -37,6 +51,20 @@ function parseCookie(cookieHeader, name) {
|
|
|
37
51
|
const match = cookieHeader.match(new RegExp(`(?:^|;\\s*)${name}=([^;]*)`));
|
|
38
52
|
return match ? decodeURIComponent(match[1]) : void 0;
|
|
39
53
|
}
|
|
54
|
+
var publicKeyCache = /* @__PURE__ */ new Map();
|
|
55
|
+
var PUBLIC_KEY_CACHE_TTL = 60 * 60 * 1e3;
|
|
56
|
+
function createTokenCookies(tokens) {
|
|
57
|
+
return [
|
|
58
|
+
`SIMPLELOGIN_ACCESS_TOKEN=${tokens.access_token}; HttpOnly; Secure; SameSite=Lax; Path=/`,
|
|
59
|
+
`SIMPLELOGIN_REFRESH_TOKEN=${tokens.refresh_token}; HttpOnly; Secure; SameSite=Lax; Path=/`
|
|
60
|
+
];
|
|
61
|
+
}
|
|
62
|
+
function clearTokenCookies() {
|
|
63
|
+
return [
|
|
64
|
+
"SIMPLELOGIN_ACCESS_TOKEN=; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=0",
|
|
65
|
+
"SIMPLELOGIN_REFRESH_TOKEN=; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=0"
|
|
66
|
+
];
|
|
67
|
+
}
|
|
40
68
|
var SimpleLogin = class {
|
|
41
69
|
clientId;
|
|
42
70
|
clientSecret;
|
|
@@ -64,50 +92,57 @@ var SimpleLogin = class {
|
|
|
64
92
|
}
|
|
65
93
|
}
|
|
66
94
|
/**
|
|
67
|
-
* Returns a Response that redirects to the authorization URL with
|
|
95
|
+
* Returns a Response that redirects to the authorization URL with state and PKCE cookies set.
|
|
68
96
|
* @param options - Optional scopes or custom state
|
|
69
97
|
* @returns A 302 redirect Response ready to be returned from your route handler
|
|
70
98
|
*/
|
|
71
|
-
redirectToAuth(options = {}) {
|
|
72
|
-
const { url,
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
});
|
|
99
|
+
async redirectToAuth(options = {}) {
|
|
100
|
+
const { url, cookies } = await this.getAuthorizationUrl(options);
|
|
101
|
+
const headers = new Headers();
|
|
102
|
+
headers.set("Location", url);
|
|
103
|
+
for (const cookie of cookies) {
|
|
104
|
+
headers.append("Set-Cookie", cookie);
|
|
105
|
+
}
|
|
106
|
+
return new Response(null, { status: 302, headers });
|
|
80
107
|
}
|
|
81
108
|
/**
|
|
82
109
|
* Generate the authorization URL to redirect users for sign-in
|
|
83
|
-
* @returns The authorization URL, state
|
|
110
|
+
* @returns The authorization URL, state, code verifier, and ready-to-use Set-Cookie header values
|
|
84
111
|
*/
|
|
85
|
-
getAuthorizationUrl(options = {}) {
|
|
112
|
+
async getAuthorizationUrl(options = {}) {
|
|
86
113
|
const state = options.state ?? generateState();
|
|
114
|
+
const codeVerifier = generateCodeVerifier();
|
|
115
|
+
const codeChallenge = await generateCodeChallenge(codeVerifier);
|
|
87
116
|
const params = new URLSearchParams({
|
|
88
117
|
client_id: this.clientId,
|
|
89
118
|
redirect_uri: this.redirectUri,
|
|
90
119
|
response_type: "code",
|
|
91
|
-
state
|
|
120
|
+
state,
|
|
121
|
+
code_challenge: codeChallenge,
|
|
122
|
+
code_challenge_method: "S256"
|
|
92
123
|
});
|
|
93
124
|
if (options.scopes?.length) {
|
|
94
125
|
params.set("scope", options.scopes.join(" "));
|
|
95
126
|
}
|
|
96
|
-
const
|
|
127
|
+
const cookies = [
|
|
128
|
+
`SIMPLELOGIN_STATE=${state}; HttpOnly; Secure; SameSite=Lax; Max-Age=600; Path=/`,
|
|
129
|
+
`SIMPLELOGIN_CODE_VERIFIER=${codeVerifier}; HttpOnly; Secure; SameSite=Lax; Max-Age=600; Path=/`
|
|
130
|
+
];
|
|
97
131
|
return {
|
|
98
132
|
url: `${this.baseUrl}/v1/auth/authorize?${params.toString()}`,
|
|
99
133
|
state,
|
|
100
|
-
|
|
134
|
+
codeVerifier,
|
|
135
|
+
cookies
|
|
101
136
|
};
|
|
102
137
|
}
|
|
103
138
|
/**
|
|
104
|
-
*
|
|
105
|
-
* Automatically extracts state from SIMPLELOGIN_STATE cookie and code/state from URL.
|
|
139
|
+
* Handle the OAuth callback: verify state, exchange code, fetch user info, and return a redirect Response.
|
|
106
140
|
* @param request - The incoming Request object from the callback
|
|
107
|
-
* @
|
|
141
|
+
* @param redirectTo - URL to redirect to after successful authentication (default: '/')
|
|
142
|
+
* @returns CallbackResult with redirect Response (cookies set) and user info to store
|
|
108
143
|
* @throws AuthorizationError if state is missing or doesn't match
|
|
109
144
|
*/
|
|
110
|
-
async handleCallback(request) {
|
|
145
|
+
async handleCallback(request, redirectTo = "/") {
|
|
111
146
|
const url = new URL(request.url);
|
|
112
147
|
const code = url.searchParams.get("code");
|
|
113
148
|
const state = url.searchParams.get("state");
|
|
@@ -119,18 +154,41 @@ var SimpleLogin = class {
|
|
|
119
154
|
}
|
|
120
155
|
const cookieHeader = request.headers.get("cookie") ?? "";
|
|
121
156
|
const expectedState = parseCookie(cookieHeader, "SIMPLELOGIN_STATE");
|
|
157
|
+
const codeVerifier = parseCookie(cookieHeader, "SIMPLELOGIN_CODE_VERIFIER");
|
|
122
158
|
if (!expectedState) {
|
|
123
159
|
throw new AuthorizationError("Missing SIMPLELOGIN_STATE cookie");
|
|
124
160
|
}
|
|
125
161
|
if (state !== expectedState) {
|
|
126
162
|
throw new AuthorizationError("Invalid state parameter");
|
|
127
163
|
}
|
|
128
|
-
|
|
164
|
+
if (!codeVerifier) {
|
|
165
|
+
throw new AuthorizationError("Missing SIMPLELOGIN_CODE_VERIFIER cookie");
|
|
166
|
+
}
|
|
167
|
+
const tokens = await this.exchangeCode(code, codeVerifier);
|
|
168
|
+
const user = await this.getUserInfo(tokens.access_token);
|
|
169
|
+
const cookies = createTokenCookies(tokens);
|
|
170
|
+
const headers = new Headers();
|
|
171
|
+
headers.set("Location", redirectTo);
|
|
172
|
+
for (const cookie of cookies) {
|
|
173
|
+
headers.append("Set-Cookie", cookie);
|
|
174
|
+
}
|
|
175
|
+
headers.append(
|
|
176
|
+
"Set-Cookie",
|
|
177
|
+
"SIMPLELOGIN_STATE=; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=0"
|
|
178
|
+
);
|
|
179
|
+
headers.append(
|
|
180
|
+
"Set-Cookie",
|
|
181
|
+
"SIMPLELOGIN_CODE_VERIFIER=; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=0"
|
|
182
|
+
);
|
|
183
|
+
const response = new Response(null, { status: 302, headers });
|
|
184
|
+
return { response, user };
|
|
129
185
|
}
|
|
130
186
|
/**
|
|
131
187
|
* Exchange an authorization code for access and refresh tokens
|
|
188
|
+
* @param code - The authorization code from the callback
|
|
189
|
+
* @param codeVerifier - The PKCE code verifier
|
|
132
190
|
*/
|
|
133
|
-
async exchangeCode(code) {
|
|
191
|
+
async exchangeCode(code, codeVerifier) {
|
|
134
192
|
const response = await fetch(`${this.baseUrl}/v1/auth/token`, {
|
|
135
193
|
method: "POST",
|
|
136
194
|
headers: {
|
|
@@ -141,7 +199,8 @@ var SimpleLogin = class {
|
|
|
141
199
|
client_id: this.clientId,
|
|
142
200
|
client_secret: this.clientSecret,
|
|
143
201
|
redirect_uri: this.redirectUri,
|
|
144
|
-
code
|
|
202
|
+
code,
|
|
203
|
+
code_verifier: codeVerifier
|
|
145
204
|
})
|
|
146
205
|
});
|
|
147
206
|
if (!response.ok) {
|
|
@@ -186,6 +245,98 @@ var SimpleLogin = class {
|
|
|
186
245
|
}
|
|
187
246
|
return response.json();
|
|
188
247
|
}
|
|
248
|
+
/**
|
|
249
|
+
* Fetch and cache the public key for JWT verification
|
|
250
|
+
*/
|
|
251
|
+
async getPublicKey() {
|
|
252
|
+
const cached = publicKeyCache.get(this.clientId);
|
|
253
|
+
if (cached && Date.now() - cached.fetchedAt < PUBLIC_KEY_CACHE_TTL) {
|
|
254
|
+
return cached.key;
|
|
255
|
+
}
|
|
256
|
+
const response = await fetch(`${this.baseUrl}/api/applications/${this.clientId}/public-key`);
|
|
257
|
+
if (!response.ok) {
|
|
258
|
+
throw new AuthorizationError("Failed to fetch public key");
|
|
259
|
+
}
|
|
260
|
+
const pem = await response.text();
|
|
261
|
+
const key = await importSPKI(pem, "RS256");
|
|
262
|
+
publicKeyCache.set(this.clientId, { key, fetchedAt: Date.now() });
|
|
263
|
+
return key;
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Authenticate a request by verifying the access token from cookies.
|
|
267
|
+
* No network calls except when refreshing expired tokens.
|
|
268
|
+
* @param request - The incoming Request object
|
|
269
|
+
* @param options - Optional settings for CSRF protection
|
|
270
|
+
* @returns AuthResult if authenticated, null if not authenticated
|
|
271
|
+
*/
|
|
272
|
+
async authenticate(request, options) {
|
|
273
|
+
const method = request.method.toUpperCase();
|
|
274
|
+
if (method !== "GET" && method !== "HEAD" && options?.allowedOrigin) {
|
|
275
|
+
const origin = request.headers.get("origin") || request.headers.get("referer");
|
|
276
|
+
if (!origin?.startsWith(options.allowedOrigin)) {
|
|
277
|
+
return null;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
const cookieHeader = request.headers.get("cookie") ?? "";
|
|
281
|
+
const accessToken = parseCookie(cookieHeader, "SIMPLELOGIN_ACCESS_TOKEN");
|
|
282
|
+
const refreshTokenValue = parseCookie(cookieHeader, "SIMPLELOGIN_REFRESH_TOKEN");
|
|
283
|
+
if (!accessToken) {
|
|
284
|
+
return null;
|
|
285
|
+
}
|
|
286
|
+
const publicKey = await this.getPublicKey();
|
|
287
|
+
try {
|
|
288
|
+
await jwtVerify(accessToken, publicKey);
|
|
289
|
+
const claims = decodeJwt(accessToken);
|
|
290
|
+
return { claims, accessToken };
|
|
291
|
+
} catch (error) {
|
|
292
|
+
const isExpired = error instanceof Error && "code" in error && error.code === "ERR_JWT_EXPIRED";
|
|
293
|
+
if (!isExpired || !refreshTokenValue) {
|
|
294
|
+
return null;
|
|
295
|
+
}
|
|
296
|
+
try {
|
|
297
|
+
const tokens = await this.refreshToken(refreshTokenValue);
|
|
298
|
+
const claims = decodeJwt(tokens.access_token);
|
|
299
|
+
const cookies = createTokenCookies(tokens);
|
|
300
|
+
return { claims, accessToken: tokens.access_token, cookies };
|
|
301
|
+
} catch {
|
|
302
|
+
return null;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* Revoke a refresh token on the IdP
|
|
308
|
+
*/
|
|
309
|
+
async revokeToken(refreshToken) {
|
|
310
|
+
await fetch(`${this.baseUrl}/v1/auth/revoke`, {
|
|
311
|
+
method: "POST",
|
|
312
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
313
|
+
body: new URLSearchParams({
|
|
314
|
+
token: refreshToken,
|
|
315
|
+
client_id: this.clientId,
|
|
316
|
+
client_secret: this.clientSecret
|
|
317
|
+
})
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* Logout: revoke refresh token on IdP and clear cookies.
|
|
322
|
+
* @param request - The incoming Request object (to read refresh token from cookies)
|
|
323
|
+
* @param redirectTo - URL to redirect to after logout (default: '/')
|
|
324
|
+
*/
|
|
325
|
+
async logout(request, redirectTo = "/") {
|
|
326
|
+
const cookieHeader = request.headers.get("cookie") ?? "";
|
|
327
|
+
const refreshToken = parseCookie(cookieHeader, "SIMPLELOGIN_REFRESH_TOKEN");
|
|
328
|
+
if (refreshToken) {
|
|
329
|
+
this.revokeToken(refreshToken).catch(() => {
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
const cookies = clearTokenCookies();
|
|
333
|
+
const headers = new Headers();
|
|
334
|
+
headers.set("Location", redirectTo);
|
|
335
|
+
for (const cookie of cookies) {
|
|
336
|
+
headers.append("Set-Cookie", cookie);
|
|
337
|
+
}
|
|
338
|
+
return new Response(null, { status: 302, headers });
|
|
339
|
+
}
|
|
189
340
|
};
|
|
190
341
|
export {
|
|
191
342
|
AuthorizationError,
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/errors.ts","../src/client.ts"],"sourcesContent":["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","import { AuthorizationError, TokenError } from './errors'\nimport type {\n AuthorizationUrlOptions,\n AuthorizationUrlResult,\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 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\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 the state cookie 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 redirectToAuth(options: AuthorizationUrlOptions = {}): Response {\n const { url, cookie } = this.getAuthorizationUrl(options)\n return new Response(null, {\n status: 302,\n headers: {\n Location: url,\n 'Set-Cookie': cookie,\n },\n })\n }\n\n /**\n * Generate the authorization URL to redirect users for sign-in\n * @returns The authorization URL, state parameter, and a ready-to-use Set-Cookie header value\n */\n getAuthorizationUrl(options: AuthorizationUrlOptions = {}): AuthorizationUrlResult {\n const state = options.state ?? generateState()\n\n const params = new URLSearchParams({\n client_id: this.clientId,\n redirect_uri: this.redirectUri,\n response_type: 'code',\n state,\n })\n\n if (options.scopes?.length) {\n params.set('scope', options.scopes.join(' '))\n }\n\n const cookie = `SIMPLELOGIN_STATE=${state}; HttpOnly; Secure; SameSite=Lax; Max-Age=600; Path=/`\n\n return {\n url: `${this.baseUrl}/v1/auth/authorize?${params.toString()}`,\n state,\n cookie,\n }\n }\n\n /**\n * Verify the OAuth callback request and exchange the code for tokens.\n * Automatically extracts state from SIMPLELOGIN_STATE cookie and code/state from URL.\n * @param request - The incoming Request object from the callback\n * @returns The token response if verification succeeds\n * @throws AuthorizationError if state is missing or doesn't match\n */\n async handleCallback(request: Request): Promise<TokenResponse> {\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\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 return this.exchangeCode(code)\n }\n\n /**\n * Exchange an authorization code for access and refresh tokens\n */\n async exchangeCode(code: 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 }),\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"],"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 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":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@simple-login/sdk",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Official SDK for Simple Login",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.cjs",
|
|
@@ -42,5 +42,8 @@
|
|
|
42
42
|
},
|
|
43
43
|
"engines": {
|
|
44
44
|
"node": ">=18.0.0"
|
|
45
|
+
},
|
|
46
|
+
"dependencies": {
|
|
47
|
+
"jose": "^6.1.3"
|
|
45
48
|
}
|
|
46
49
|
}
|