@markwharton/pwa-core 4.0.1 → 4.1.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/server.d.ts +16 -3
- package/dist/server.js +96 -2
- package/package.json +1 -1
package/dist/server.d.ts
CHANGED
|
@@ -417,10 +417,18 @@ export declare function listEntitiesWithFilter<T extends object>(client: TableCl
|
|
|
417
417
|
* @returns A 32-character hex string suitable for Azure Table Storage row keys
|
|
418
418
|
*/
|
|
419
419
|
export declare function generateRowKey(identifier: string): string;
|
|
420
|
+
/**
|
|
421
|
+
* Authentication method for session auth.
|
|
422
|
+
* - 'magic-link': Email-based passwordless authentication (default)
|
|
423
|
+
* - 'easy-auth': Azure Static Web Apps Easy Auth (reads identity from x-ms-client-principal header)
|
|
424
|
+
*/
|
|
425
|
+
export type SessionAuthMethod = 'magic-link' | 'easy-auth';
|
|
420
426
|
/**
|
|
421
427
|
* Configuration for session-based authentication.
|
|
422
428
|
*/
|
|
423
429
|
export interface SessionAuthConfig {
|
|
430
|
+
/** Authentication method (default: 'magic-link') */
|
|
431
|
+
authMethod?: SessionAuthMethod;
|
|
424
432
|
/** Cookie name for the session (default: 'app_session') */
|
|
425
433
|
cookieName?: string;
|
|
426
434
|
/** Session duration in ms (default: 30 days) */
|
|
@@ -449,8 +457,8 @@ export interface SessionAuthConfig {
|
|
|
449
457
|
resolveUser?: (user: SessionUser) => Promise<SessionUser> | SessionUser;
|
|
450
458
|
/** Base URL for magic links and SWA preview URL validation */
|
|
451
459
|
appBaseUrl?: string;
|
|
452
|
-
/**
|
|
453
|
-
sendEmail
|
|
460
|
+
/** Callback to send magic link emails. Required for magic-link auth, ignored for easy-auth. */
|
|
461
|
+
sendEmail?: (to: string, magicLink: string) => Promise<boolean>;
|
|
454
462
|
}
|
|
455
463
|
/**
|
|
456
464
|
* Initializes session-based authentication. Call once at application startup.
|
|
@@ -481,7 +489,10 @@ export declare function initSessionAuth(config: SessionAuthConfig): void;
|
|
|
481
489
|
* });
|
|
482
490
|
*/
|
|
483
491
|
export declare function initSessionAuthFromEnv(config: {
|
|
484
|
-
|
|
492
|
+
/** Authentication method (default: 'magic-link'). Also reads AUTH_METHOD env var. */
|
|
493
|
+
authMethod?: SessionAuthMethod;
|
|
494
|
+
/** Callback to send magic link emails. Required for magic-link auth, ignored for easy-auth. */
|
|
495
|
+
sendEmail?: (to: string, magicLink: string) => Promise<boolean>;
|
|
485
496
|
/** Additional email validation. Called after built-in domain/admin checks reject.
|
|
486
497
|
* Can only widen access, never narrow it. */
|
|
487
498
|
isEmailAllowed?: (email: string) => boolean | Promise<boolean>;
|
|
@@ -597,6 +608,8 @@ export declare function validateSession<T extends SessionUser = SessionUser>(req
|
|
|
597
608
|
}>>;
|
|
598
609
|
/**
|
|
599
610
|
* Convenience function: validates session and returns user with optional refresh headers.
|
|
611
|
+
* For magic-link auth, validates session cookie and handles sliding window refresh.
|
|
612
|
+
* For easy-auth, reads identity from SWA's x-ms-client-principal header.
|
|
600
613
|
* @param request - Request object with headers.get() method
|
|
601
614
|
* @returns Result with user and optional Set-Cookie headers
|
|
602
615
|
* @example
|
package/dist/server.js
CHANGED
|
@@ -783,8 +783,9 @@ const DEFAULT_COOKIE_NAME = 'app_session';
|
|
|
783
783
|
* });
|
|
784
784
|
*/
|
|
785
785
|
function initSessionAuth(config) {
|
|
786
|
-
|
|
787
|
-
|
|
786
|
+
const method = config.authMethod ?? 'magic-link';
|
|
787
|
+
if (method === 'magic-link' && !config.sendEmail) {
|
|
788
|
+
throw new Error('sendEmail callback is required for magic-link auth');
|
|
788
789
|
}
|
|
789
790
|
sessionAuthConfig = config;
|
|
790
791
|
}
|
|
@@ -807,7 +808,11 @@ function initSessionAuth(config) {
|
|
|
807
808
|
function initSessionAuthFromEnv(config) {
|
|
808
809
|
const allowedEmailsStr = process.env.ALLOWED_EMAILS;
|
|
809
810
|
const adminEmailsStr = process.env.ADMIN_EMAILS;
|
|
811
|
+
const authMethod = config.authMethod
|
|
812
|
+
?? process.env.AUTH_METHOD
|
|
813
|
+
?? 'magic-link';
|
|
810
814
|
initSessionAuth({
|
|
815
|
+
authMethod,
|
|
811
816
|
cookieName: process.env.SESSION_COOKIE_NAME,
|
|
812
817
|
appBaseUrl: process.env.APP_BASE_URL,
|
|
813
818
|
allowedEmails: allowedEmailsStr
|
|
@@ -1010,6 +1015,12 @@ async function checkMagicLinkRateLimit(email) {
|
|
|
1010
1015
|
*/
|
|
1011
1016
|
async function createMagicLink(email, request) {
|
|
1012
1017
|
const config = getSessionAuthConfig();
|
|
1018
|
+
if (config.authMethod === 'easy-auth') {
|
|
1019
|
+
return (0, shared_1.err)('Magic links are not available in easy-auth mode', shared_1.HTTP_STATUS.BAD_REQUEST);
|
|
1020
|
+
}
|
|
1021
|
+
if (!config.sendEmail) {
|
|
1022
|
+
return (0, shared_1.err)('sendEmail callback is not configured', shared_1.HTTP_STATUS.INTERNAL_ERROR);
|
|
1023
|
+
}
|
|
1013
1024
|
const normalizedEmail = email.trim().toLowerCase();
|
|
1014
1025
|
// Validate email format
|
|
1015
1026
|
if (!isValidEmail(normalizedEmail)) {
|
|
@@ -1062,6 +1073,9 @@ async function createMagicLink(email, request) {
|
|
|
1062
1073
|
*/
|
|
1063
1074
|
async function verifyMagicLink(token) {
|
|
1064
1075
|
const config = getSessionAuthConfig();
|
|
1076
|
+
if (config.authMethod === 'easy-auth') {
|
|
1077
|
+
return (0, shared_1.err)('Magic links are not available in easy-auth mode', shared_1.HTTP_STATUS.BAD_REQUEST);
|
|
1078
|
+
}
|
|
1065
1079
|
// Look up magic link
|
|
1066
1080
|
const magicLinksClient = await getTableClient(MAGIC_LINKS_TABLE);
|
|
1067
1081
|
const magicLinkEntity = await getEntityIfExists(magicLinksClient, MAGICLINK_PARTITION, token);
|
|
@@ -1198,9 +1212,82 @@ async function validateSession(request) {
|
|
|
1198
1212
|
return (0, shared_1.err)('Session validation failed', shared_1.HTTP_STATUS.UNAUTHORIZED);
|
|
1199
1213
|
}
|
|
1200
1214
|
}
|
|
1215
|
+
/**
|
|
1216
|
+
* Parses the SWA Easy Auth x-ms-client-principal header.
|
|
1217
|
+
* @param request - Request object with headers.get() method
|
|
1218
|
+
* @returns The client principal, or null if header is missing/invalid
|
|
1219
|
+
*/
|
|
1220
|
+
function parseEasyAuthPrincipal(request) {
|
|
1221
|
+
const header = request.headers.get('x-ms-client-principal');
|
|
1222
|
+
if (!header)
|
|
1223
|
+
return null;
|
|
1224
|
+
try {
|
|
1225
|
+
const decoded = Buffer.from(header, 'base64').toString('utf-8');
|
|
1226
|
+
const principal = JSON.parse(decoded);
|
|
1227
|
+
if (!principal.userDetails)
|
|
1228
|
+
return null;
|
|
1229
|
+
return principal;
|
|
1230
|
+
}
|
|
1231
|
+
catch {
|
|
1232
|
+
return null;
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
/**
|
|
1236
|
+
* Validates Easy Auth identity and returns user from the Users table.
|
|
1237
|
+
* Creates the UserEntity on first sign-in (same as magic link flow).
|
|
1238
|
+
* @param request - Request object with x-ms-client-principal header
|
|
1239
|
+
* @returns Result with user (no session info or refresh cookies — SWA manages sessions)
|
|
1240
|
+
*/
|
|
1241
|
+
async function validateEasyAuth(request) {
|
|
1242
|
+
const config = getSessionAuthConfig();
|
|
1243
|
+
const principal = parseEasyAuthPrincipal(request);
|
|
1244
|
+
if (!principal)
|
|
1245
|
+
return (0, shared_1.err)('No Easy Auth identity', shared_1.HTTP_STATUS.UNAUTHORIZED);
|
|
1246
|
+
const email = principal.userDetails.toLowerCase();
|
|
1247
|
+
const now = new Date().toISOString();
|
|
1248
|
+
const isAdminUser = config.adminEmails
|
|
1249
|
+
? config.adminEmails.map(e => e.toLowerCase()).includes(email)
|
|
1250
|
+
: false;
|
|
1251
|
+
// Get or create user (same pattern as verifyMagicLink)
|
|
1252
|
+
const usersClient = await getTableClient(USERS_TABLE);
|
|
1253
|
+
let userEntity = await getEntityIfExists(usersClient, USER_PARTITION, email);
|
|
1254
|
+
if (!userEntity) {
|
|
1255
|
+
userEntity = {
|
|
1256
|
+
partitionKey: USER_PARTITION,
|
|
1257
|
+
rowKey: email,
|
|
1258
|
+
id: (0, crypto_1.randomUUID)(),
|
|
1259
|
+
email,
|
|
1260
|
+
isAdmin: isAdminUser || undefined,
|
|
1261
|
+
createdAt: now,
|
|
1262
|
+
lastLoginAt: now
|
|
1263
|
+
};
|
|
1264
|
+
await upsertEntity(usersClient, userEntity);
|
|
1265
|
+
}
|
|
1266
|
+
else {
|
|
1267
|
+
// Update isAdmin and lastLoginAt on each request would be too expensive.
|
|
1268
|
+
// Only update isAdmin if it changed.
|
|
1269
|
+
if ((isAdminUser || undefined) !== userEntity.isAdmin) {
|
|
1270
|
+
userEntity.isAdmin = isAdminUser || undefined;
|
|
1271
|
+
await upsertEntity(usersClient, userEntity);
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
const baseUser = {
|
|
1275
|
+
id: userEntity.id,
|
|
1276
|
+
email: userEntity.email,
|
|
1277
|
+
isAdmin: userEntity.isAdmin,
|
|
1278
|
+
createdAt: userEntity.createdAt,
|
|
1279
|
+
lastLoginAt: userEntity.lastLoginAt
|
|
1280
|
+
};
|
|
1281
|
+
const user = (config.resolveUser
|
|
1282
|
+
? await config.resolveUser(baseUser)
|
|
1283
|
+
: baseUser);
|
|
1284
|
+
return (0, shared_1.ok)({ user });
|
|
1285
|
+
}
|
|
1201
1286
|
// --- Session Operations ---
|
|
1202
1287
|
/**
|
|
1203
1288
|
* Convenience function: validates session and returns user with optional refresh headers.
|
|
1289
|
+
* For magic-link auth, validates session cookie and handles sliding window refresh.
|
|
1290
|
+
* For easy-auth, reads identity from SWA's x-ms-client-principal header.
|
|
1204
1291
|
* @param request - Request object with headers.get() method
|
|
1205
1292
|
* @returns Result with user and optional Set-Cookie headers
|
|
1206
1293
|
* @example
|
|
@@ -1209,6 +1296,13 @@ async function validateSession(request) {
|
|
|
1209
1296
|
* const { user, headers } = result.data!;
|
|
1210
1297
|
*/
|
|
1211
1298
|
async function getSessionUser(request) {
|
|
1299
|
+
const config = getSessionAuthConfig();
|
|
1300
|
+
if (config.authMethod === 'easy-auth') {
|
|
1301
|
+
const result = await validateEasyAuth(request);
|
|
1302
|
+
if (!result.ok)
|
|
1303
|
+
return result;
|
|
1304
|
+
return (0, shared_1.ok)({ user: result.data.user });
|
|
1305
|
+
}
|
|
1212
1306
|
const result = await validateSession(request);
|
|
1213
1307
|
if (!result.ok)
|
|
1214
1308
|
return result;
|