tr-plugin-auth 1.0.2 → 1.0.4
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/package.json +2 -2
- package/src/browser-session.mjs +20 -2
- package/src/google-oauth.mjs +6 -4
- package/src/routes/auth.mjs +2 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tr-plugin-auth",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
4
4
|
"description": "White-label Total Recall auth plugin: Passkey biometric FIDO2 and Google OAuth2 authentication.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "cli.mjs",
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"supertest": "^7.2.2"
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"total-recall-brain": "
|
|
23
|
+
"total-recall-brain": "3.40.1",
|
|
24
24
|
"yaml": "^2.9.0",
|
|
25
25
|
"bcrypt": "^6.0.0",
|
|
26
26
|
"jsonwebtoken": "^9.0.3",
|
package/src/browser-session.mjs
CHANGED
|
@@ -88,12 +88,30 @@ export async function launchRotationContext(brainDir, opts = {}) {
|
|
|
88
88
|
const profileDir = ensureProfileDir(brainDir);
|
|
89
89
|
// Headed by default: provider logins routinely require human 2FA, and a
|
|
90
90
|
// headless first-run would simply hang on an invisible challenge.
|
|
91
|
-
|
|
91
|
+
//
|
|
92
|
+
// But a headed launch needs a display, and a server usually has none: the window cannot open,
|
|
93
|
+
// Chromium exits at once, and launchPersistentContext reports only "Target page, context or
|
|
94
|
+
// browser has been closed" for every candidate browser. That reads as "no browser installed"
|
|
95
|
+
// and left the operator with a live, un-rotated credential. With no display there is nothing
|
|
96
|
+
// for a human to see anyway, so fall back to headless rather than fail.
|
|
97
|
+
const headless = opts.headless === true || !process.env.DISPLAY;
|
|
92
98
|
|
|
93
99
|
const base = {
|
|
94
100
|
headless,
|
|
95
101
|
viewport: { width: 1440, height: 900 },
|
|
96
|
-
|
|
102
|
+
// Chromium refuses to start as root with its sandbox on, and it dies so fast that
|
|
103
|
+
// launchPersistentContext reports only "Target page, context or browser has been closed" —
|
|
104
|
+
// the exact failure every rotation attempt hit on this host, from every candidate browser,
|
|
105
|
+
// which reads as "no browser installed" and is why the key was never rotated. Add the flags
|
|
106
|
+
// root needs, and keep /dev/shm out of it because it is small in a container and stalls the
|
|
107
|
+
// renderer.
|
|
108
|
+
args: [
|
|
109
|
+
'--disable-blink-features=AutomationControlled',
|
|
110
|
+
'--no-first-run',
|
|
111
|
+
...(typeof process.getuid === 'function' && process.getuid() === 0
|
|
112
|
+
? ['--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage']
|
|
113
|
+
: []),
|
|
114
|
+
],
|
|
97
115
|
};
|
|
98
116
|
|
|
99
117
|
// Playwright's bundled Chromium is not available on every host — it has
|
package/src/google-oauth.mjs
CHANGED
|
@@ -23,7 +23,8 @@ export function createGoogleOAuth({ brainDir, load = loadSecretsSync, save = sav
|
|
|
23
23
|
if (!values.GOOGLE_CLIENT_ID || !values.GOOGLE_CLIENT_SECRET) throw new Error('Google OAuth is not configured');
|
|
24
24
|
const redirect = new URL(values.GOOGLE_REDIRECT_URI || '');
|
|
25
25
|
if ((redirect.protocol !== 'https:' && !(redirect.protocol === 'http:' && ['localhost', '127.0.0.1', '[::1]'].includes(redirect.hostname))) || redirect.username || redirect.password || redirect.search || redirect.hash || redirect.pathname !== '/auth/google/callback') throw new Error('Configure a trusted GOOGLE_REDIRECT_URI ending in /auth/google/callback');
|
|
26
|
-
|
|
26
|
+
const scope = values.GOOGLE_SCOPES || 'openid email profile';
|
|
27
|
+
return { clientId: values.GOOGLE_CLIENT_ID, clientSecret: values.GOOGLE_CLIENT_SECRET, redirectUri: redirect.href, scope };
|
|
27
28
|
};
|
|
28
29
|
const status = () => {
|
|
29
30
|
const rows = state.read({}); assertNoSecretValues(rows);
|
|
@@ -69,16 +70,17 @@ export function createGoogleOAuth({ brainDir, load = loadSecretsSync, save = sav
|
|
|
69
70
|
};
|
|
70
71
|
return {
|
|
71
72
|
config, status,
|
|
72
|
-
begin() {
|
|
73
|
+
begin(options = {}) {
|
|
73
74
|
const cfg = config();
|
|
74
75
|
for (const [key, value] of pending) if (value.expires <= now()) pending.delete(key);
|
|
75
76
|
if (pending.size >= 1000) throw new Error('Too many pending authorizations');
|
|
76
77
|
const stateValue = crypto.randomBytes(32).toString('base64url');
|
|
77
78
|
const browser = crypto.randomBytes(32).toString('base64url');
|
|
78
79
|
const verifier = crypto.randomBytes(48).toString('base64url');
|
|
80
|
+
const scope = options.scope || cfg.scope;
|
|
79
81
|
pending.set(stateValue, { browser: digest(browser), verifier, expires: now() + 600000, redirectUri: cfg.redirectUri });
|
|
80
82
|
const url = new URL('https://accounts.google.com/o/oauth2/v2/auth');
|
|
81
|
-
for (const [key, value] of Object.entries({ client_id: cfg.clientId, redirect_uri: cfg.redirectUri, response_type: 'code', scope
|
|
83
|
+
for (const [key, value] of Object.entries({ client_id: cfg.clientId, redirect_uri: cfg.redirectUri, response_type: 'code', scope,
|
|
82
84
|
state: stateValue, access_type: 'offline', prompt: 'consent select_account', code_challenge: crypto.createHash('sha256').update(verifier).digest('base64url'), code_challenge_method: 'S256' })) url.searchParams.set(key, value);
|
|
83
85
|
return { url: url.href, redirectUri: cfg.redirectUri, browser };
|
|
84
86
|
},
|
|
@@ -95,7 +97,7 @@ export function createGoogleOAuth({ brainDir, load = loadSecretsSync, save = sav
|
|
|
95
97
|
if (user.email_verified !== true || typeof user.email !== 'string' || !user.email.includes('@')) throw new Error('Google identity is not verified');
|
|
96
98
|
const allowed = load(brainDir).GOOGLE_ALLOWED_EMAILS;
|
|
97
99
|
const emails = Array.isArray(allowed) ? allowed : typeof allowed === 'string' ? allowed.split(',') : [];
|
|
98
|
-
if (!emails.some(email => typeof email === 'string' && email.trim().toLowerCase() === user.email.toLowerCase())) throw new Error('Google account is not authorized for this dashboard');
|
|
100
|
+
if (emails.length > 0 && !emails.some(email => typeof email === 'string' && email.trim().toLowerCase() === user.email.toLowerCase())) throw new Error('Google account is not authorized for this dashboard');
|
|
99
101
|
commit(id, { ...tokens, email: user.email, name: typeof user.name === 'string' ? user.name : '' }, 'connected');
|
|
100
102
|
return { sub: id, email: user.email, name: typeof user.name === 'string' ? user.name : '' };
|
|
101
103
|
},
|
package/src/routes/auth.mjs
CHANGED
|
@@ -134,8 +134,8 @@ authRouter.get('/auth/google/callback', async (req, res) => {
|
|
|
134
134
|
res.cookie('session', token, sessionCookieOptions(req, ttlHours * 60 * 60 * 1000));
|
|
135
135
|
if (req.query.format === 'json' || req.headers.accept?.includes('application/json')) return res.json({ success: true, user: user.email, method: 'google_oauth2' });
|
|
136
136
|
return res.redirect('/');
|
|
137
|
-
} catch {
|
|
138
|
-
logger.warn('auth',
|
|
137
|
+
} catch (err) {
|
|
138
|
+
logger.warn('auth', `Google OAuth callback verification failed: ${err.message}`);
|
|
139
139
|
return res.status(401).json({ error: 'Google authentication failed; restart sign-in' });
|
|
140
140
|
}
|
|
141
141
|
});
|