@chemmangat/msal-next 1.2.1 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,88 @@
1
+ import { cookies, headers } from 'next/headers';
2
+
3
+ // src/utils/getServerSession.ts
4
+
5
+ // src/utils/validation.ts
6
+ function safeJsonParse(jsonString, validator) {
7
+ try {
8
+ const parsed = JSON.parse(jsonString);
9
+ if (validator(parsed)) {
10
+ return parsed;
11
+ }
12
+ console.warn("[Validation] JSON validation failed");
13
+ return null;
14
+ } catch (error) {
15
+ console.error("[Validation] JSON parse error:", error);
16
+ return null;
17
+ }
18
+ }
19
+ function isValidAccountData(data) {
20
+ return typeof data === "object" && data !== null && typeof data.homeAccountId === "string" && data.homeAccountId.length > 0 && typeof data.username === "string" && data.username.length > 0 && (data.name === void 0 || typeof data.name === "string");
21
+ }
22
+
23
+ // src/utils/getServerSession.ts
24
+ async function getServerSession() {
25
+ try {
26
+ const cookieStore = await cookies();
27
+ const headersList = await headers();
28
+ const msalAccount = cookieStore.get("msal.account");
29
+ const msalToken = cookieStore.get("msal.token");
30
+ if (msalAccount?.value) {
31
+ const accountData = safeJsonParse(
32
+ msalAccount.value,
33
+ isValidAccountData
34
+ );
35
+ if (accountData) {
36
+ return {
37
+ isAuthenticated: true,
38
+ accountId: accountData.homeAccountId,
39
+ username: accountData.username,
40
+ accessToken: msalToken?.value
41
+ };
42
+ } else {
43
+ console.warn("[ServerSession] Invalid account data in cookie");
44
+ }
45
+ }
46
+ const authHeader = headersList.get("x-msal-authenticated");
47
+ if (authHeader === "true") {
48
+ const username = headersList.get("x-msal-username");
49
+ return {
50
+ isAuthenticated: true,
51
+ username: username || void 0
52
+ };
53
+ }
54
+ return {
55
+ isAuthenticated: false
56
+ };
57
+ } catch (error) {
58
+ console.error("[ServerSession] Error reading session:", error);
59
+ return {
60
+ isAuthenticated: false
61
+ };
62
+ }
63
+ }
64
+ async function setServerSessionCookie(account, accessToken) {
65
+ try {
66
+ const accountData = {
67
+ homeAccountId: account.homeAccountId,
68
+ username: account.username,
69
+ name: account.name
70
+ };
71
+ await fetch("/api/auth/session", {
72
+ method: "POST",
73
+ headers: {
74
+ "Content-Type": "application/json"
75
+ },
76
+ body: JSON.stringify({
77
+ account: accountData,
78
+ token: accessToken
79
+ })
80
+ });
81
+ } catch (error) {
82
+ console.error("[ServerSession] Failed to set session cookie:", error);
83
+ }
84
+ }
85
+
86
+ export { getServerSession, setServerSessionCookie };
87
+ //# sourceMappingURL=server.mjs.map
88
+ //# sourceMappingURL=server.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/utils/validation.ts","../src/utils/getServerSession.ts"],"names":[],"mappings":"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","file":"server.mjs","sourcesContent":["/**\r\n * Security utilities for input validation and sanitization\r\n */\r\n\r\n/**\r\n * Validate account data structure from cookie\r\n */\r\nexport interface ValidatedAccountData {\r\n homeAccountId: string;\r\n username: string;\r\n name?: string;\r\n}\r\n\r\n/**\r\n * Safely parse and validate JSON from untrusted sources\r\n */\r\nexport function safeJsonParse<T>(\r\n jsonString: string,\r\n validator: (data: any) => data is T\r\n): T | null {\r\n try {\r\n const parsed = JSON.parse(jsonString);\r\n if (validator(parsed)) {\r\n return parsed;\r\n }\r\n console.warn('[Validation] JSON validation failed');\r\n return null;\r\n } catch (error) {\r\n console.error('[Validation] JSON parse error:', error);\r\n return null;\r\n }\r\n}\r\n\r\n/**\r\n * Validate account data structure\r\n */\r\nexport function isValidAccountData(data: any): data is ValidatedAccountData {\r\n return (\r\n typeof data === 'object' &&\r\n data !== null &&\r\n typeof data.homeAccountId === 'string' &&\r\n data.homeAccountId.length > 0 &&\r\n typeof data.username === 'string' &&\r\n data.username.length > 0 &&\r\n (data.name === undefined || typeof data.name === 'string')\r\n );\r\n}\r\n\r\n/**\r\n * Sanitize error messages to prevent information disclosure\r\n */\r\nexport function sanitizeError(error: unknown): string {\r\n if (error instanceof Error) {\r\n // Remove sensitive information from error messages\r\n const message = error.message;\r\n \r\n // Remove potential tokens or secrets (anything that looks like a JWT or long hex string)\r\n const sanitized = message\r\n .replace(/[A-Za-z0-9_-]{20,}\\.[A-Za-z0-9_-]{20,}\\.[A-Za-z0-9_-]{20,}/g, '[TOKEN_REDACTED]')\r\n .replace(/[a-f0-9]{32,}/gi, '[SECRET_REDACTED]')\r\n .replace(/Bearer\\s+[^\\s]+/gi, 'Bearer [REDACTED]');\r\n \r\n return sanitized;\r\n }\r\n \r\n return 'An unexpected error occurred';\r\n}\r\n\r\n/**\r\n * Validate redirect URI to prevent open redirect vulnerabilities\r\n */\r\nexport function isValidRedirectUri(uri: string, allowedOrigins: string[]): boolean {\r\n try {\r\n const url = new URL(uri);\r\n \r\n // Check if the origin is in the allowed list\r\n return allowedOrigins.some(allowed => {\r\n const allowedUrl = new URL(allowed);\r\n return url.origin === allowedUrl.origin;\r\n });\r\n } catch {\r\n return false;\r\n }\r\n}\r\n\r\n/**\r\n * Validate scope strings to prevent injection\r\n */\r\nexport function isValidScope(scope: string): boolean {\r\n // Scopes should only contain alphanumeric characters, dots, hyphens, and underscores\r\n return /^[a-zA-Z0-9._-]+$/.test(scope);\r\n}\r\n\r\n/**\r\n * Validate array of scopes\r\n */\r\nexport function validateScopes(scopes: string[]): boolean {\r\n return Array.isArray(scopes) && scopes.every(isValidScope);\r\n}\r\n","import { cookies, headers } from 'next/headers';\r\nimport { safeJsonParse, isValidAccountData, ValidatedAccountData } from './validation';\r\n\r\nexport interface ServerSession {\r\n /**\r\n * Whether user is authenticated\r\n */\r\n isAuthenticated: boolean;\r\n\r\n /**\r\n * User's account ID from MSAL cache\r\n */\r\n accountId?: string;\r\n\r\n /**\r\n * User's username/email\r\n */\r\n username?: string;\r\n\r\n /**\r\n * Access token (if available in cookie)\r\n * @deprecated Storing tokens in cookies is not recommended for security reasons\r\n */\r\n accessToken?: string;\r\n}\r\n\r\n/**\r\n * Server-side session helper for Next.js App Router\r\n * \r\n * Note: This is a basic implementation that reads from cookies.\r\n * For production use, consider implementing a proper session store.\r\n * \r\n * @example\r\n * ```tsx\r\n * // In a Server Component or Route Handler\r\n * import { getServerSession } from '@chemmangat/msal-next';\r\n * \r\n * export default async function Page() {\r\n * const session = await getServerSession();\r\n * \r\n * if (!session.isAuthenticated) {\r\n * redirect('/login');\r\n * }\r\n * \r\n * return <div>Welcome {session.username}</div>;\r\n * }\r\n * ```\r\n */\r\nexport async function getServerSession(): Promise<ServerSession> {\r\n try {\r\n const cookieStore = await cookies();\r\n const headersList = await headers();\r\n\r\n // Try to read MSAL session from cookies\r\n // MSAL stores data in sessionStorage/localStorage by default,\r\n // so this requires custom implementation to sync to cookies\r\n const msalAccount = cookieStore.get('msal.account');\r\n const msalToken = cookieStore.get('msal.token');\r\n\r\n if (msalAccount?.value) {\r\n // Safely parse and validate account data\r\n const accountData = safeJsonParse<ValidatedAccountData>(\r\n msalAccount.value,\r\n isValidAccountData\r\n );\r\n\r\n if (accountData) {\r\n return {\r\n isAuthenticated: true,\r\n accountId: accountData.homeAccountId,\r\n username: accountData.username,\r\n accessToken: msalToken?.value,\r\n };\r\n } else {\r\n console.warn('[ServerSession] Invalid account data in cookie');\r\n }\r\n }\r\n\r\n // Fallback: check for custom auth header\r\n const authHeader = headersList.get('x-msal-authenticated');\r\n if (authHeader === 'true') {\r\n const username = headersList.get('x-msal-username');\r\n return {\r\n isAuthenticated: true,\r\n username: username || undefined,\r\n };\r\n }\r\n\r\n return {\r\n isAuthenticated: false,\r\n };\r\n } catch (error) {\r\n console.error('[ServerSession] Error reading session:', error);\r\n return {\r\n isAuthenticated: false,\r\n };\r\n }\r\n}\r\n\r\n/**\r\n * Helper to set server session in cookies (call from client-side after auth)\r\n * \r\n * @example\r\n * ```tsx\r\n * // After successful login\r\n * await setServerSessionCookie(account, accessToken);\r\n * ```\r\n */\r\nexport async function setServerSessionCookie(account: any, accessToken?: string): Promise<void> {\r\n try {\r\n const accountData = {\r\n homeAccountId: account.homeAccountId,\r\n username: account.username,\r\n name: account.name,\r\n };\r\n\r\n // Set cookies via API route\r\n await fetch('/api/auth/session', {\r\n method: 'POST',\r\n headers: {\r\n 'Content-Type': 'application/json',\r\n },\r\n body: JSON.stringify({\r\n account: accountData,\r\n token: accessToken,\r\n }),\r\n });\r\n } catch (error) {\r\n console.error('[ServerSession] Failed to set session cookie:', error);\r\n }\r\n}\r\n"]}
package/package.json CHANGED
@@ -1,61 +1,77 @@
1
- {
2
- "name": "@chemmangat/msal-next",
3
- "version": "1.2.1",
4
- "description": "Fully configurable MSAL authentication package for Next.js App Router",
5
- "main": "./dist/index.js",
6
- "module": "./dist/index.mjs",
7
- "types": "./dist/index.d.ts",
8
- "exports": {
9
- ".": {
10
- "types": "./dist/index.d.ts",
11
- "import": "./dist/index.mjs",
12
- "require": "./dist/index.js"
13
- }
14
- },
15
- "files": [
16
- "dist",
17
- "README.md"
18
- ],
19
- "scripts": {
20
- "build": "tsup",
21
- "dev": "tsup --watch",
22
- "prepublishOnly": "npm run build"
23
- },
24
- "keywords": [
25
- "msal",
26
- "nextjs",
27
- "authentication",
28
- "azure-ad",
29
- "microsoft",
30
- "oauth",
31
- "next.js",
32
- "app-router",
33
- "typescript"
34
- ],
35
- "author": "Chemmangat",
36
- "license": "MIT",
37
- "repository": {
38
- "type": "git",
39
- "url": "https://github.com/chemmangat/msal-next.git",
40
- "directory": "packages/core"
41
- },
42
- "homepage": "https://github.com/chemmangat/msal-next#readme",
43
- "bugs": {
44
- "url": "https://github.com/chemmangat/msal-next/issues"
45
- },
46
- "peerDependencies": {
47
- "@azure/msal-browser": "^3.11.0 || ^4.0.0",
48
- "@azure/msal-react": "^2.0.0 || ^3.0.0",
49
- "next": ">=14.0.0",
50
- "react": ">=18.0.0",
51
- "react-dom": ">=18.0.0"
52
- },
53
- "devDependencies": {
54
- "@azure/msal-browser": "^3.11.1",
55
- "@azure/msal-react": "^2.0.15",
56
- "@types/react": "^18.2.0",
57
- "react": "^18.2.0",
58
- "tsup": "^8.0.1",
59
- "typescript": "^5.3.0"
60
- }
61
- }
1
+ {
2
+ "name": "@chemmangat/msal-next",
3
+ "version": "2.0.1",
4
+ "description": "Production-grade MSAL authentication package for Next.js App Router with minimal boilerplate",
5
+ "main": "./dist/index.js",
6
+ "module": "./dist/index.mjs",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.mjs",
12
+ "require": "./dist/index.js"
13
+ },
14
+ "./server": {
15
+ "types": "./dist/server.d.ts",
16
+ "import": "./dist/server.mjs",
17
+ "require": "./dist/server.js"
18
+ }
19
+ },
20
+ "files": [
21
+ "dist",
22
+ "README.md",
23
+ "SECURITY.md"
24
+ ],
25
+ "scripts": {
26
+ "build": "tsup",
27
+ "dev": "tsup --watch",
28
+ "test": "vitest run",
29
+ "test:watch": "vitest",
30
+ "test:coverage": "vitest run --coverage",
31
+ "prepublishOnly": "npm run build"
32
+ },
33
+ "keywords": [
34
+ "msal",
35
+ "nextjs",
36
+ "authentication",
37
+ "azure-ad",
38
+ "microsoft",
39
+ "oauth",
40
+ "next.js",
41
+ "app-router",
42
+ "typescript",
43
+ "sso",
44
+ "microsoft-graph"
45
+ ],
46
+ "author": "Hari Manoj (chemmangat)",
47
+ "license": "MIT",
48
+ "repository": {
49
+ "type": "git",
50
+ "url": "https://github.com/chemmangat/msal-next.git",
51
+ "directory": "packages/core"
52
+ },
53
+ "homepage": "https://github.com/chemmangat/msal-next#readme",
54
+ "bugs": {
55
+ "url": "https://github.com/chemmangat/msal-next/issues"
56
+ },
57
+ "peerDependencies": {
58
+ "@azure/msal-browser": "^3.11.0 || ^4.0.0",
59
+ "@azure/msal-react": "^2.0.0 || ^3.0.0",
60
+ "next": ">=14.0.0",
61
+ "react": ">=18.0.0",
62
+ "react-dom": ">=18.0.0"
63
+ },
64
+ "devDependencies": {
65
+ "@azure/msal-browser": "^3.11.1",
66
+ "@azure/msal-react": "^2.0.15",
67
+ "@testing-library/react": "^14.0.0",
68
+ "@testing-library/react-hooks": "^8.0.1",
69
+ "@types/react": "^18.2.0",
70
+ "@vitest/coverage-v8": "^1.0.0",
71
+ "jsdom": "^23.0.0",
72
+ "react": "^18.2.0",
73
+ "tsup": "^8.0.1",
74
+ "typescript": "^5.3.0",
75
+ "vitest": "^1.0.0"
76
+ }
77
+ }