@mindfulauth/core 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +64 -0
- package/dist/auth-handler.d.ts +5 -0
- package/dist/auth-handler.d.ts.map +1 -0
- package/dist/auth-handler.js +147 -0
- package/dist/auth.d.ts +9 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +56 -0
- package/dist/config.d.ts +11 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +66 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +13 -0
- package/dist/middleware.d.ts +2 -0
- package/dist/middleware.d.ts.map +1 -0
- package/dist/middleware.js +73 -0
- package/dist/security.d.ts +5 -0
- package/dist/security.d.ts.map +1 -0
- package/dist/security.js +31 -0
- package/dist/types.d.ts +22 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/package.json +51 -0
package/README.md
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# @mindful-auth/core
|
|
2
|
+
|
|
3
|
+
Core authentication library for Mindful Auth, designed for Astro applications.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @mindful-auth/core
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
### Set up middleware
|
|
14
|
+
|
|
15
|
+
In your `src/middleware.ts`:
|
|
16
|
+
|
|
17
|
+
```typescript
|
|
18
|
+
export { onRequest } from '@mindful-auth/core/middleware';
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
### Handle auth routes
|
|
22
|
+
|
|
23
|
+
In your `src/pages/auth/[...slug].ts`:
|
|
24
|
+
|
|
25
|
+
```typescript
|
|
26
|
+
import { handleAuthGet, handleAuthPost } from '@mindful-auth/core/auth-handler';
|
|
27
|
+
|
|
28
|
+
export async function GET(context: APIContext) {
|
|
29
|
+
const { params, request, url, locals } = context;
|
|
30
|
+
return handleAuthGet(params.slug || '', request, url, locals);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function POST(context: APIContext) {
|
|
34
|
+
const { params, request, url, locals } = context;
|
|
35
|
+
return handleAuthPost(params.slug || '', request, url, locals);
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### Configure your application
|
|
40
|
+
|
|
41
|
+
You can customize the configuration by importing and modifying values:
|
|
42
|
+
|
|
43
|
+
```typescript
|
|
44
|
+
import { CENTRAL_AUTH_ORIGIN, PUBLIC_ROUTES } from '@mindful-auth/core/config';
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Environment Variables
|
|
48
|
+
|
|
49
|
+
Set the following environment variable (required):
|
|
50
|
+
|
|
51
|
+
- `INTERNAL_API_KEY`: Your Mindful Auth API key
|
|
52
|
+
|
|
53
|
+
## Features
|
|
54
|
+
|
|
55
|
+
- 🔐 Session validation and management
|
|
56
|
+
- 🛡️ Security headers (CSP, HSTS, etc.)
|
|
57
|
+
- 🚦 Public/protected route handling
|
|
58
|
+
- 🔄 Auth proxy for login, registration, 2FA, etc.
|
|
59
|
+
- 🛠️ Path traversal protection
|
|
60
|
+
- ⚡ Built for Cloudflare Workers & Astro SSR
|
|
61
|
+
|
|
62
|
+
## License
|
|
63
|
+
|
|
64
|
+
MIT
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** Handle GET requests (email verification, password reset links, etc.) */
|
|
2
|
+
export declare function handleAuthGet(rawEndpoint: string, request: Request, url: URL, locals?: any): Promise<Response>;
|
|
3
|
+
/** Handle POST requests (login, 2FA, password changes, etc.) */
|
|
4
|
+
export declare function handleAuthPost(rawEndpoint: string, request: Request, url: URL, locals?: any): Promise<Response>;
|
|
5
|
+
//# sourceMappingURL=auth-handler.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth-handler.d.ts","sourceRoot":"","sources":["../src/auth-handler.ts"],"names":[],"mappings":"AAmEA,2EAA2E;AAC3E,wBAAsB,aAAa,CAAC,WAAW,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,GAAG,EAAE,GAAG,EAAE,MAAM,CAAC,EAAE,GAAG,GAAG,OAAO,CAAC,QAAQ,CAAC,CA4BpH;AAED,gEAAgE;AAChE,wBAAsB,cAAc,CAAC,WAAW,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,GAAG,EAAE,GAAG,EAAE,MAAM,CAAC,EAAE,GAAG,GAAG,OAAO,CAAC,QAAQ,CAAC,CAwDrH"}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
// Auth proxy handler for Mindful Auth
|
|
2
|
+
// Forwards authentication requests to the central Mindful Auth service
|
|
3
|
+
import { CENTRAL_AUTH_ORIGIN, ALLOWED_AUTH_METHODS, MAX_BODY_SIZE_BYTES, AUTH_PROXY_TIMEOUT_MS } from './config';
|
|
4
|
+
import { sanitizeEndpoint } from './security';
|
|
5
|
+
const JSON_HEADERS = { 'Content-Type': 'application/json' };
|
|
6
|
+
const jsonError = (error, status) => new Response(JSON.stringify({ error }), { status, headers: JSON_HEADERS });
|
|
7
|
+
/** Build proxy headers from incoming request */
|
|
8
|
+
function buildProxyHeaders(request, tenantDomain, apiKey) {
|
|
9
|
+
const headers = {
|
|
10
|
+
'Content-Type': 'application/json',
|
|
11
|
+
'X-Tenant-Domain': tenantDomain,
|
|
12
|
+
'X-Requested-With': 'XMLHttpRequest'
|
|
13
|
+
};
|
|
14
|
+
if (apiKey)
|
|
15
|
+
headers['Authorization'] = `Bearer ${apiKey}`;
|
|
16
|
+
const cookie = request.headers.get('Cookie');
|
|
17
|
+
if (cookie)
|
|
18
|
+
headers['Cookie'] = cookie;
|
|
19
|
+
const clientIp = request.headers.get('cf-connecting-ip') ||
|
|
20
|
+
request.headers.get('x-forwarded-for')?.split(',')[0]?.trim() ||
|
|
21
|
+
request.headers.get('x-real-ip');
|
|
22
|
+
if (clientIp)
|
|
23
|
+
headers['X-Forwarded-For'] = clientIp;
|
|
24
|
+
const userAgent = request.headers.get('user-agent');
|
|
25
|
+
if (userAgent)
|
|
26
|
+
headers['User-Agent'] = userAgent;
|
|
27
|
+
return headers;
|
|
28
|
+
}
|
|
29
|
+
/** Fetch with timeout */
|
|
30
|
+
async function fetchWithTimeout(url, options) {
|
|
31
|
+
const controller = new AbortController();
|
|
32
|
+
const timeoutId = setTimeout(() => controller.abort(), AUTH_PROXY_TIMEOUT_MS);
|
|
33
|
+
try {
|
|
34
|
+
return await fetch(url, { ...options, signal: controller.signal });
|
|
35
|
+
}
|
|
36
|
+
finally {
|
|
37
|
+
clearTimeout(timeoutId);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/** Extract mirrored session cookie (removes Domain to use tenant domain) */
|
|
41
|
+
function getMirroredCookie(resp) {
|
|
42
|
+
const setCookie = resp.headers.get('set-cookie');
|
|
43
|
+
return setCookie?.includes('session_id=')
|
|
44
|
+
? setCookie.replace(/Domain=[^;]+;?\s*/i, '')
|
|
45
|
+
: null;
|
|
46
|
+
}
|
|
47
|
+
/** Check if redirect is safe (relative, no open redirect) */
|
|
48
|
+
function isSafeRedirect(url) {
|
|
49
|
+
return url.startsWith('/') && !url.startsWith('//');
|
|
50
|
+
}
|
|
51
|
+
/** Build response with standard headers */
|
|
52
|
+
function buildResponse(data, status, cookie) {
|
|
53
|
+
const headers = new Headers({
|
|
54
|
+
'Content-Type': 'application/json',
|
|
55
|
+
'Cache-Control': 'no-store, no-cache, must-revalidate, private'
|
|
56
|
+
});
|
|
57
|
+
if (cookie)
|
|
58
|
+
headers.set('Set-Cookie', cookie);
|
|
59
|
+
return new Response(JSON.stringify(data), { status, headers });
|
|
60
|
+
}
|
|
61
|
+
/** Handle GET requests (email verification, password reset links, etc.) */
|
|
62
|
+
export async function handleAuthGet(rawEndpoint, request, url, locals) {
|
|
63
|
+
const internalApiKey = locals?.runtime?.env?.INTERNAL_API_KEY || import.meta.env.INTERNAL_API_KEY;
|
|
64
|
+
if (!internalApiKey)
|
|
65
|
+
console.error('[auth-handler] INTERNAL_API_KEY not configured');
|
|
66
|
+
const endpoint = sanitizeEndpoint(rawEndpoint);
|
|
67
|
+
if (!endpoint)
|
|
68
|
+
return jsonError('Bad request', 400);
|
|
69
|
+
const apiUrl = new URL(`${CENTRAL_AUTH_ORIGIN}/auth/${endpoint}`);
|
|
70
|
+
url.searchParams.forEach((v, k) => apiUrl.searchParams.set(k, v));
|
|
71
|
+
let resp;
|
|
72
|
+
try {
|
|
73
|
+
resp = await fetchWithTimeout(apiUrl.toString(), {
|
|
74
|
+
method: 'GET',
|
|
75
|
+
headers: buildProxyHeaders(request, url.hostname, internalApiKey)
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
catch (e) {
|
|
79
|
+
if (e.name === 'AbortError')
|
|
80
|
+
return jsonError('Request timeout', 504);
|
|
81
|
+
throw e;
|
|
82
|
+
}
|
|
83
|
+
// Handle redirect
|
|
84
|
+
const location = resp.headers.get('Location');
|
|
85
|
+
if (resp.status === 302 && location && isSafeRedirect(location)) {
|
|
86
|
+
return new Response(null, { status: 302, headers: { Location: location } });
|
|
87
|
+
}
|
|
88
|
+
return buildResponse(await resp.json(), resp.status, getMirroredCookie(resp));
|
|
89
|
+
}
|
|
90
|
+
/** Handle POST requests (login, 2FA, password changes, etc.) */
|
|
91
|
+
export async function handleAuthPost(rawEndpoint, request, url, locals) {
|
|
92
|
+
if (!ALLOWED_AUTH_METHODS.includes(request.method))
|
|
93
|
+
return jsonError('Method not allowed', 405);
|
|
94
|
+
const contentLength = request.headers.get('content-length');
|
|
95
|
+
if (contentLength && parseInt(contentLength) > MAX_BODY_SIZE_BYTES)
|
|
96
|
+
return jsonError('Payload too large', 413);
|
|
97
|
+
const internalApiKey = locals?.runtime?.env?.INTERNAL_API_KEY || import.meta.env.INTERNAL_API_KEY;
|
|
98
|
+
if (!internalApiKey)
|
|
99
|
+
console.error('[auth-handler] INTERNAL_API_KEY not configured');
|
|
100
|
+
const endpoint = sanitizeEndpoint(rawEndpoint);
|
|
101
|
+
if (!endpoint)
|
|
102
|
+
return jsonError('Bad request', 400);
|
|
103
|
+
// Parse request body
|
|
104
|
+
let body = null;
|
|
105
|
+
try {
|
|
106
|
+
const text = await request.text();
|
|
107
|
+
if (text) {
|
|
108
|
+
JSON.parse(text);
|
|
109
|
+
body = text;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
return jsonError('Invalid JSON', 400);
|
|
114
|
+
}
|
|
115
|
+
let resp;
|
|
116
|
+
try {
|
|
117
|
+
resp = await fetchWithTimeout(`${CENTRAL_AUTH_ORIGIN}/auth/${endpoint}`, {
|
|
118
|
+
method: 'POST',
|
|
119
|
+
headers: buildProxyHeaders(request, url.hostname, internalApiKey),
|
|
120
|
+
body
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
catch (e) {
|
|
124
|
+
if (e.name === 'AbortError')
|
|
125
|
+
return jsonError('Request timeout', 504);
|
|
126
|
+
throw e;
|
|
127
|
+
}
|
|
128
|
+
const data = await resp.json();
|
|
129
|
+
const cookie = getMirroredCookie(resp);
|
|
130
|
+
// Handle HTTP redirect
|
|
131
|
+
const location = resp.headers.get('Location');
|
|
132
|
+
if (resp.status === 302 && location && isSafeRedirect(location)) {
|
|
133
|
+
return new Response(null, { status: 302, headers: { Location: location } });
|
|
134
|
+
}
|
|
135
|
+
// Handle JSON redirect (loginsuccessredirect)
|
|
136
|
+
const redirectUrl = data.loginsuccessredirect;
|
|
137
|
+
if (typeof redirectUrl === 'string' && isSafeRedirect(redirectUrl)) {
|
|
138
|
+
return new Response(null, {
|
|
139
|
+
status: 302,
|
|
140
|
+
headers: { Location: redirectUrl, ...(cookie && { 'Set-Cookie': cookie }) }
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
// Remove sensitive data
|
|
144
|
+
delete data.sessionToken;
|
|
145
|
+
delete data.maxAgeSeconds;
|
|
146
|
+
return buildResponse(data, resp.status, cookie);
|
|
147
|
+
}
|
package/dist/auth.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { SessionValidationResult } from './types';
|
|
2
|
+
/** Validate session with Mindful Auth central service */
|
|
3
|
+
export declare function validateSession(request: Request, tenantDomain: string, pathname: string, internalApiKey: string): Promise<SessionValidationResult>;
|
|
4
|
+
/** Validate memberid in URL matches session (or just check structure if sessionRecordId is null) */
|
|
5
|
+
export declare function validateMemberIdInUrl(pathname: string, sessionRecordId: string | null): {
|
|
6
|
+
valid: boolean;
|
|
7
|
+
strippedPathname?: string;
|
|
8
|
+
};
|
|
9
|
+
//# sourceMappingURL=auth.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,SAAS,CAAC;AAEvD,yDAAyD;AACzD,wBAAsB,eAAe,CACjC,OAAO,EAAE,OAAO,EAChB,YAAY,EAAE,MAAM,EACpB,QAAQ,EAAE,MAAM,EAChB,cAAc,EAAE,MAAM,GACvB,OAAO,CAAC,uBAAuB,CAAC,CAsClC;AAED,oGAAoG;AACpG,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,MAAM,EAAE,eAAe,EAAE,MAAM,GAAG,IAAI,GAAG;IAAE,KAAK,EAAE,OAAO,CAAC;IAAC,gBAAgB,CAAC,EAAE,MAAM,CAAA;CAAE,CAerI"}
|
package/dist/auth.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// Authentication and session validation for Mindful Auth
|
|
2
|
+
import { CENTRAL_AUTH_ORIGIN, SESSION_VALIDATION_TIMEOUT_MS } from './config';
|
|
3
|
+
/** Validate session with Mindful Auth central service */
|
|
4
|
+
export async function validateSession(request, tenantDomain, pathname, internalApiKey) {
|
|
5
|
+
const sessionId = request.headers.get('Cookie')?.match(/session_id=([^;]+)/)?.[1];
|
|
6
|
+
if (!sessionId)
|
|
7
|
+
return { valid: false, reason: 'no-session' };
|
|
8
|
+
const controller = new AbortController();
|
|
9
|
+
const timeoutId = setTimeout(() => controller.abort(), SESSION_VALIDATION_TIMEOUT_MS);
|
|
10
|
+
try {
|
|
11
|
+
const clientIp = request.headers.get('cf-connecting-ip') ||
|
|
12
|
+
request.headers.get('x-forwarded-for')?.split(',')[0]?.trim() ||
|
|
13
|
+
request.headers.get('x-real-ip');
|
|
14
|
+
const resp = await fetch(`${CENTRAL_AUTH_ORIGIN}/auth/validate-session`, {
|
|
15
|
+
method: 'POST',
|
|
16
|
+
headers: {
|
|
17
|
+
'Content-Type': 'application/json',
|
|
18
|
+
'X-Tenant-Domain': tenantDomain,
|
|
19
|
+
'X-Internal-Api-Key': internalApiKey,
|
|
20
|
+
...(clientIp && { 'X-Forwarded-For': clientIp }),
|
|
21
|
+
...(request.headers.get('user-agent') && { 'User-Agent': request.headers.get('user-agent') })
|
|
22
|
+
},
|
|
23
|
+
body: JSON.stringify({ sessionId, requestedUrl: pathname }),
|
|
24
|
+
signal: controller.signal
|
|
25
|
+
});
|
|
26
|
+
const result = await resp.json();
|
|
27
|
+
if (!result.valid)
|
|
28
|
+
return { valid: false, reason: 'invalid-session' };
|
|
29
|
+
const recordId = result.data?.sub || result.recordId;
|
|
30
|
+
if (!recordId)
|
|
31
|
+
return { valid: false, reason: 'invalid-record-id' };
|
|
32
|
+
return { valid: true, recordId };
|
|
33
|
+
}
|
|
34
|
+
catch (e) {
|
|
35
|
+
console.error('[auth] Session validation failed:', e.message);
|
|
36
|
+
return { valid: false, reason: 'error' };
|
|
37
|
+
}
|
|
38
|
+
finally {
|
|
39
|
+
clearTimeout(timeoutId);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/** Validate memberid in URL matches session (or just check structure if sessionRecordId is null) */
|
|
43
|
+
export function validateMemberIdInUrl(pathname, sessionRecordId) {
|
|
44
|
+
const segments = pathname.split('/').filter(Boolean);
|
|
45
|
+
if (segments.length === 0)
|
|
46
|
+
return { valid: false };
|
|
47
|
+
const urlMemberId = segments[0];
|
|
48
|
+
// Pre-check mode: just validate URL has memberid
|
|
49
|
+
if (sessionRecordId === null) {
|
|
50
|
+
return { valid: !!urlMemberId, strippedPathname: pathname };
|
|
51
|
+
}
|
|
52
|
+
// Validate memberid matches session
|
|
53
|
+
if (urlMemberId !== sessionRecordId)
|
|
54
|
+
return { valid: false };
|
|
55
|
+
return { valid: true, strippedPathname: '/' + segments.slice(1).join('/') };
|
|
56
|
+
}
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export declare const CENTRAL_AUTH_ORIGIN = "https://api.mindfulauth.com";
|
|
2
|
+
export declare const CDN_ORIGIN = "https://cdn.mindfulauth.com";
|
|
3
|
+
export declare const ALLOWED_AUTH_METHODS: string[];
|
|
4
|
+
export declare const MAX_BODY_SIZE_BYTES = 1048576;
|
|
5
|
+
export declare const AUTH_PROXY_TIMEOUT_MS = 15000;
|
|
6
|
+
export declare const SESSION_VALIDATION_TIMEOUT_MS = 10000;
|
|
7
|
+
export declare const SKIP_ASSETS: string[];
|
|
8
|
+
export declare const PUBLIC_ROUTES: string[];
|
|
9
|
+
export declare const PUBLIC_PREFIXES: string[];
|
|
10
|
+
export declare const SECURITY_HEADERS: Record<string, string>;
|
|
11
|
+
//# sourceMappingURL=config.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,mBAAmB,gCAAgC,CAAC;AACjE,eAAO,MAAM,UAAU,gCAAgC,CAAC;AACxD,eAAO,MAAM,oBAAoB,UAAkB,CAAC;AACpD,eAAO,MAAM,mBAAmB,UAAU,CAAC;AAC3C,eAAO,MAAM,qBAAqB,QAAQ,CAAC;AAC3C,eAAO,MAAM,6BAA6B,QAAQ,CAAC;AAMnD,eAAO,MAAM,WAAW,UAA+D,CAAC;AAKxF,eAAO,MAAM,aAAa,UAQzB,CAAC;AAKF,eAAO,MAAM,eAAe,UAM3B,CAAC;AAaF,eAAO,MAAM,gBAAgB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAoBnD,CAAC"}
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// Configuration for the Astro Portal
|
|
2
|
+
export const CENTRAL_AUTH_ORIGIN = 'https://api.mindfulauth.com';
|
|
3
|
+
export const CDN_ORIGIN = 'https://cdn.mindfulauth.com';
|
|
4
|
+
export const ALLOWED_AUTH_METHODS = ['GET', 'POST'];
|
|
5
|
+
export const MAX_BODY_SIZE_BYTES = 1048576; // 1MB limit
|
|
6
|
+
export const AUTH_PROXY_TIMEOUT_MS = 15000;
|
|
7
|
+
export const SESSION_VALIDATION_TIMEOUT_MS = 10000;
|
|
8
|
+
// Static assets to skip session validation (favicon, robots.txt, etc.)
|
|
9
|
+
// SECURITY CRITICAL: Only add actual static file requests here.
|
|
10
|
+
// Examples of safe entries: /favicon.ico, /robots.txt, /sitemap.xml
|
|
11
|
+
// NEVER add application routes like /dashboard, /profile, /secure/* - this COMPLETELY bypasses authentication. If you add a route here, unauthenticated users can access it without logging in.
|
|
12
|
+
export const SKIP_ASSETS = ['/favicon.ico', '/robots.txt', '/.well-known/security.txt'];
|
|
13
|
+
// Public routes that do not require authentication
|
|
14
|
+
// ⚠️ DO NOT EDIT - These are critical for the authentication system to work correctly
|
|
15
|
+
// If you need public content in your portal, host it on your main website instead
|
|
16
|
+
export const PUBLIC_ROUTES = [
|
|
17
|
+
'/',
|
|
18
|
+
'/login',
|
|
19
|
+
'/register',
|
|
20
|
+
'/magic-login',
|
|
21
|
+
'/magic-register',
|
|
22
|
+
'/forgot-password',
|
|
23
|
+
'/resend-verification',
|
|
24
|
+
];
|
|
25
|
+
// Dynamic public routes (prefix matching)
|
|
26
|
+
// ⚠️ DO NOT EDIT - These are critical for the authentication system to work correctly
|
|
27
|
+
// If you need public content in your portal, host it on your main website instead
|
|
28
|
+
export const PUBLIC_PREFIXES = [
|
|
29
|
+
'/auth/', // API endpoints for authentication
|
|
30
|
+
'/email-verified/',
|
|
31
|
+
'/reset-password/',
|
|
32
|
+
'/verify-email/',
|
|
33
|
+
'/verify-magic-link/',
|
|
34
|
+
];
|
|
35
|
+
// Security headers for all HTML responses
|
|
36
|
+
// ⚠️ IMPORTANT: Do not remove the following domains from the CSP - they are critical for authentication:
|
|
37
|
+
// - challenges.cloudflare.com: Turnstile CAPTCHA (prevents bot attacks)
|
|
38
|
+
// - cdn.mindfulauth.com: Mindful Auth authentication scripts (handles login, 2FA, password reset)
|
|
39
|
+
// - api.mindfulauth.com: Backend API calls for session validation
|
|
40
|
+
// - cdn.jsdelivr.net: External dependencies for QR Code library (for 2fa auth)
|
|
41
|
+
// Removing these will break authentication and user will be unable to log in.
|
|
42
|
+
//
|
|
43
|
+
// NOTE on img-src: Currently allows any HTTPS image source for development flexibility.
|
|
44
|
+
// For production, restrict to trusted sources only:
|
|
45
|
+
// "img-src 'self' data: https://your-cdn.com https://example.com"
|
|
46
|
+
export const SECURITY_HEADERS = {
|
|
47
|
+
'X-Content-Type-Options': 'nosniff',
|
|
48
|
+
'X-Frame-Options': 'SAMEORIGIN',
|
|
49
|
+
'Referrer-Policy': 'strict-origin-when-cross-origin',
|
|
50
|
+
'Strict-Transport-Security': 'max-age=31536000; includeSubDomains',
|
|
51
|
+
'Permissions-Policy': 'geolocation=(), microphone=(), camera=()',
|
|
52
|
+
'Content-Security-Policy': [
|
|
53
|
+
"default-src 'self'",
|
|
54
|
+
"script-src 'self' 'unsafe-inline' https://challenges.cloudflare.com https://cdn.mindfulauth.com https://api.mindfulauth.com https://cdn.jsdelivr.net https://static.cloudflareinsights.com",
|
|
55
|
+
"style-src 'self' 'unsafe-inline'",
|
|
56
|
+
"img-src 'self' data: https:",
|
|
57
|
+
"connect-src 'self' https://challenges.cloudflare.com https://api.mindfulauth.com https://*.cloudflare.com",
|
|
58
|
+
"frame-src 'self' https://challenges.cloudflare.com",
|
|
59
|
+
"font-src 'self' data:",
|
|
60
|
+
"object-src 'none'",
|
|
61
|
+
"base-uri 'self'",
|
|
62
|
+
"form-action 'self'",
|
|
63
|
+
"upgrade-insecure-requests",
|
|
64
|
+
"block-all-mixed-content"
|
|
65
|
+
].join('; ')
|
|
66
|
+
};
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAGA,cAAc,SAAS,CAAC;AAGxB,cAAc,UAAU,CAAC;AAGzB,cAAc,QAAQ,CAAC;AAGvB,cAAc,gBAAgB,CAAC;AAG/B,cAAc,YAAY,CAAC;AAG3B,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// Mindful Auth Core - Main exports
|
|
2
|
+
// Types
|
|
3
|
+
export * from './types';
|
|
4
|
+
// Configuration
|
|
5
|
+
export * from './config';
|
|
6
|
+
// Authentication
|
|
7
|
+
export * from './auth';
|
|
8
|
+
// Auth handler for API routes
|
|
9
|
+
export * from './auth-handler';
|
|
10
|
+
// Security utilities
|
|
11
|
+
export * from './security';
|
|
12
|
+
// Middleware (also available via '@mindful-auth/core/middleware')
|
|
13
|
+
export { onRequest } from './middleware';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"middleware.d.ts","sourceRoot":"","sources":["../src/middleware.ts"],"names":[],"mappings":"AAuBA,eAAO,MAAM,SAAS,mCA+DpB,CAAC"}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// Global middleware for session validation
|
|
2
|
+
// Runs before all route handlers to enforce authentication
|
|
3
|
+
import { defineMiddleware } from 'astro:middleware';
|
|
4
|
+
import { PUBLIC_ROUTES, PUBLIC_PREFIXES, SECURITY_HEADERS, SKIP_ASSETS } from './config';
|
|
5
|
+
import { sanitizePath } from './security';
|
|
6
|
+
import { validateSession, validateMemberIdInUrl } from './auth';
|
|
7
|
+
/** Check if a path is a public route (no auth required) */
|
|
8
|
+
function isPublicRoute(pathname) {
|
|
9
|
+
return PUBLIC_ROUTES.includes(pathname) ||
|
|
10
|
+
PUBLIC_PREFIXES.some(prefix => pathname.startsWith(prefix));
|
|
11
|
+
}
|
|
12
|
+
/** Add security headers to a response */
|
|
13
|
+
function addSecurityHeaders(response) {
|
|
14
|
+
Object.entries(SECURITY_HEADERS).forEach(([key, value]) => {
|
|
15
|
+
response.headers.set(key, value);
|
|
16
|
+
});
|
|
17
|
+
return response;
|
|
18
|
+
}
|
|
19
|
+
// Main middleware function
|
|
20
|
+
export const onRequest = defineMiddleware(async (context, next) => {
|
|
21
|
+
const { request, url, redirect, locals } = context;
|
|
22
|
+
const pathname = url.pathname;
|
|
23
|
+
// Type assertion for locals (users should extend App.Locals in their project)
|
|
24
|
+
const authLocals = locals;
|
|
25
|
+
// Skip middleware for static assets FIRST (before dev mode)
|
|
26
|
+
if (SKIP_ASSETS.includes(pathname)) {
|
|
27
|
+
return next();
|
|
28
|
+
}
|
|
29
|
+
// DEV MODE: Skip auth on localhost
|
|
30
|
+
if (import.meta.env.DEV && (url.hostname === 'localhost' || url.hostname === '127.0.0.1')) {
|
|
31
|
+
// Check if public route first
|
|
32
|
+
if (isPublicRoute(pathname)) {
|
|
33
|
+
authLocals.recordId = null;
|
|
34
|
+
return next();
|
|
35
|
+
}
|
|
36
|
+
// For protected routes, extract or create mock recordId
|
|
37
|
+
// Match memberid with trailing slash
|
|
38
|
+
const match = pathname.match(/^\/([a-zA-Z0-9-]+)(?:\/|$)/);
|
|
39
|
+
authLocals.recordId = match ? match[1] : 'dev-user-123';
|
|
40
|
+
return next();
|
|
41
|
+
}
|
|
42
|
+
// Sanitize path
|
|
43
|
+
const safePath = sanitizePath(pathname);
|
|
44
|
+
if (!safePath) {
|
|
45
|
+
return new Response('Bad Request', { status: 400 });
|
|
46
|
+
}
|
|
47
|
+
// Public routes - no auth needed
|
|
48
|
+
if (isPublicRoute(safePath)) {
|
|
49
|
+
authLocals.recordId = null;
|
|
50
|
+
return addSecurityHeaders(await next());
|
|
51
|
+
}
|
|
52
|
+
// Protected route - validate session
|
|
53
|
+
const internalApiKey = authLocals.runtime?.env?.INTERNAL_API_KEY || import.meta.env.INTERNAL_API_KEY;
|
|
54
|
+
if (!internalApiKey) {
|
|
55
|
+
console.error('[middleware] CRITICAL: INTERNAL_API_KEY not configured');
|
|
56
|
+
return new Response('Internal Server Error', { status: 500 });
|
|
57
|
+
}
|
|
58
|
+
// URL must have memberid
|
|
59
|
+
if (!validateMemberIdInUrl(safePath, null).valid) {
|
|
60
|
+
return new Response('Forbidden: URL must include memberid', { status: 403 });
|
|
61
|
+
}
|
|
62
|
+
// Validate session
|
|
63
|
+
const session = await validateSession(request, url.hostname, safePath, internalApiKey);
|
|
64
|
+
if (!session.valid) {
|
|
65
|
+
return redirect('/login');
|
|
66
|
+
}
|
|
67
|
+
// Validate memberid matches session
|
|
68
|
+
if (!validateMemberIdInUrl(safePath, session.recordId).valid) {
|
|
69
|
+
return new Response('Forbidden: Invalid user ID in URL', { status: 403 });
|
|
70
|
+
}
|
|
71
|
+
authLocals.recordId = session.recordId;
|
|
72
|
+
return addSecurityHeaders(await next());
|
|
73
|
+
});
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** Sanitize endpoint path (prevents ../ traversal and encoded variants) */
|
|
2
|
+
export declare function sanitizeEndpoint(endpoint: string): string | null;
|
|
3
|
+
/** Sanitize URL path (prevents ../ traversal and encoded variants) */
|
|
4
|
+
export declare function sanitizePath(pathname: string): string | null;
|
|
5
|
+
//# sourceMappingURL=security.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"security.d.ts","sourceRoot":"","sources":["../src/security.ts"],"names":[],"mappings":"AAkBA,2EAA2E;AAC3E,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAIhE;AAED,sEAAsE;AACtE,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAI5D"}
|
package/dist/security.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// Security utilities - path traversal prevention
|
|
2
|
+
const MAX_PATH_LENGTH = 2048;
|
|
3
|
+
/** Decode and check for traversal attacks */
|
|
4
|
+
function decodeAndValidate(str) {
|
|
5
|
+
if (!str || typeof str !== 'string' || str.length > MAX_PATH_LENGTH)
|
|
6
|
+
return null;
|
|
7
|
+
try {
|
|
8
|
+
let decoded = decodeURIComponent(str);
|
|
9
|
+
// Catch double-encoded traversal attempts
|
|
10
|
+
if (decoded.includes('%'))
|
|
11
|
+
decoded = decodeURIComponent(decoded);
|
|
12
|
+
return decoded;
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
/** Sanitize endpoint path (prevents ../ traversal and encoded variants) */
|
|
19
|
+
export function sanitizeEndpoint(endpoint) {
|
|
20
|
+
const decoded = decodeAndValidate(endpoint);
|
|
21
|
+
if (!decoded || decoded.includes('..') || decoded.includes('\\'))
|
|
22
|
+
return null;
|
|
23
|
+
return decoded;
|
|
24
|
+
}
|
|
25
|
+
/** Sanitize URL path (prevents ../ traversal and encoded variants) */
|
|
26
|
+
export function sanitizePath(pathname) {
|
|
27
|
+
const decoded = decodeAndValidate(pathname);
|
|
28
|
+
if (!decoded || decoded.includes('..') || decoded.includes('\\'))
|
|
29
|
+
return null;
|
|
30
|
+
return decoded;
|
|
31
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { MiddlewareHandler } from 'astro';
|
|
2
|
+
export interface MindfulAuthLocals {
|
|
3
|
+
recordId: string | null;
|
|
4
|
+
runtime?: {
|
|
5
|
+
env?: {
|
|
6
|
+
INTERNAL_API_KEY?: string;
|
|
7
|
+
};
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
declare global {
|
|
11
|
+
namespace App {
|
|
12
|
+
interface Locals extends MindfulAuthLocals {
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
export interface SessionValidationResult {
|
|
17
|
+
valid: boolean;
|
|
18
|
+
reason?: string;
|
|
19
|
+
recordId?: string;
|
|
20
|
+
}
|
|
21
|
+
export type MindfulAuthMiddleware = MiddlewareHandler;
|
|
22
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,OAAO,CAAC;AAG/C,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,OAAO,CAAC,EAAE;QACR,GAAG,CAAC,EAAE;YACJ,gBAAgB,CAAC,EAAE,MAAM,CAAC;SAC3B,CAAC;KACH,CAAC;CACH;AAGD,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,GAAG,CAAC;QACZ,UAAU,MAAO,SAAQ,iBAAiB;SAAG;KAC9C;CACF;AAED,MAAM,WAAW,uBAAuB;IACtC,KAAK,EAAE,OAAO,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,MAAM,qBAAqB,GAAG,iBAAiB,CAAC"}
|
package/dist/types.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mindfulauth/core",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Mindful Auth core authentication library for Astro",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./middleware": {
|
|
14
|
+
"types": "./dist/middleware.d.ts",
|
|
15
|
+
"import": "./dist/middleware.js"
|
|
16
|
+
},
|
|
17
|
+
"./auth-handler": {
|
|
18
|
+
"types": "./dist/auth-handler.d.ts",
|
|
19
|
+
"import": "./dist/auth-handler.js"
|
|
20
|
+
},
|
|
21
|
+
"./config": {
|
|
22
|
+
"types": "./dist/config.d.ts",
|
|
23
|
+
"import": "./dist/config.js"
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"files": [
|
|
27
|
+
"dist",
|
|
28
|
+
"README.md"
|
|
29
|
+
],
|
|
30
|
+
"scripts": {
|
|
31
|
+
"build": "tsc",
|
|
32
|
+
"dev": "tsc --watch",
|
|
33
|
+
"prepublishOnly": "npm run build"
|
|
34
|
+
},
|
|
35
|
+
"keywords": [
|
|
36
|
+
"authentication",
|
|
37
|
+
"astro",
|
|
38
|
+
"mindfulauth",
|
|
39
|
+
"session",
|
|
40
|
+
"middleware"
|
|
41
|
+
],
|
|
42
|
+
"author": "Mindful Auth",
|
|
43
|
+
"license": "MIT",
|
|
44
|
+
"peerDependencies": {
|
|
45
|
+
"astro": "^5.0.0"
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"astro": "^5.17.1",
|
|
49
|
+
"typescript": "^5.9.3"
|
|
50
|
+
}
|
|
51
|
+
}
|