Implement state management for OAuth cookies in OAuthController, enhancing security and user experience during Google OAuth flow. Update OAuthCallback and Profile views to manage local storage for linking status and improve user feedback. Increment mobile app version to 0.8.0-alpha5 to reflect these changes.

This commit is contained in:
Torsten Schulz (local)
2026-05-15 09:22:45 +02:00
parent 58dd657ac1
commit c23d260bdc
4 changed files with 56 additions and 11 deletions

View File

@@ -6,6 +6,26 @@ const oauthService = require('../services/OAuthService');
* Verwaltet OAuth-Logins
*/
class OAuthController {
createStateCookie(stateToken) {
const secure = process.env.NODE_ENV === 'production' ? '; Secure' : '';
return `timeclock_oauth_state=${encodeURIComponent(stateToken)}; Max-Age=900; Path=/api/auth/google/callback; HttpOnly; SameSite=Lax${secure}`;
}
clearStateCookie() {
const secure = process.env.NODE_ENV === 'production' ? '; Secure' : '';
return `timeclock_oauth_state=; Max-Age=0; Path=/api/auth/google/callback; HttpOnly; SameSite=Lax${secure}`;
}
readStateCookie(req) {
const cookieHeader = req.headers.cookie || '';
const cookie = cookieHeader
.split(';')
.map(part => part.trim())
.find(part => part.startsWith('timeclock_oauth_state='));
return cookie ? decodeURIComponent(cookie.slice('timeclock_oauth_state='.length)) : null;
}
/**
* Google OAuth initiieren
* GET /api/auth/google
@@ -14,11 +34,24 @@ class OAuthController {
const state = req.query.stateToken || oauthService.createStateToken({
platform: req.query.platform === 'android' ? 'android' : 'web'
});
passport.authenticate('google', {
scope: ['profile', 'email'],
session: false,
const clientId = process.env.GOOGLE_CLIENT_ID;
const callbackUrl = process.env.GOOGLE_CALLBACK_URL || 'http://localhost:3010/api/auth/google/callback';
if (!clientId) {
return res.redirect(`${process.env.FRONTEND_URL || 'http://localhost:5010'}/login?error=oauth_not_configured`);
}
const params = new URLSearchParams({
response_type: 'code',
client_id: clientId,
redirect_uri: callbackUrl,
scope: 'profile email',
state
})(req, res, next);
});
res.setHeader('Set-Cookie', this.createStateCookie(state));
return res.redirect(`https://accounts.google.com/o/oauth2/v2/auth?${params.toString()}`);
}
/**
@@ -58,7 +91,8 @@ class OAuthController {
}
try {
const state = oauthService.verifyStateToken(req.query.state);
const stateToken = req.query.state || this.readStateCookie(req);
const state = oauthService.verifyStateToken(stateToken);
const authResult = await oauthService.completeOAuthLogin(result.profile, result.provider, {
linkUserId: state.mode === 'link' ? state.userId : null
});
@@ -73,6 +107,7 @@ class OAuthController {
email: authResult.email || '',
provider: result.provider
});
res.setHeader('Set-Cookie', this.clearStateCookie());
return res.redirect(`${target}?${params.toString()}`);
}
@@ -83,9 +118,11 @@ class OAuthController {
params.set('linked', result.provider);
}
res.setHeader('Set-Cookie', this.clearStateCookie());
return res.redirect(`${target}?${params.toString()}`);
} catch (callbackError) {
console.error('Google OAuth Callback-Verarbeitung fehlgeschlagen:', callbackError);
res.setHeader('Set-Cookie', this.clearStateCookie());
return res.redirect(`${process.env.FRONTEND_URL || 'http://localhost:5010'}/login?error=oauth_failed`);
}
})(req, res, next);
@@ -175,4 +212,3 @@ class OAuthController {
}
module.exports = new OAuthController();