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:
@@ -6,6 +6,26 @@ const oauthService = require('../services/OAuthService');
|
|||||||
* Verwaltet OAuth-Logins
|
* Verwaltet OAuth-Logins
|
||||||
*/
|
*/
|
||||||
class OAuthController {
|
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
|
* Google OAuth initiieren
|
||||||
* GET /api/auth/google
|
* GET /api/auth/google
|
||||||
@@ -14,11 +34,24 @@ class OAuthController {
|
|||||||
const state = req.query.stateToken || oauthService.createStateToken({
|
const state = req.query.stateToken || oauthService.createStateToken({
|
||||||
platform: req.query.platform === 'android' ? 'android' : 'web'
|
platform: req.query.platform === 'android' ? 'android' : 'web'
|
||||||
});
|
});
|
||||||
passport.authenticate('google', {
|
|
||||||
scope: ['profile', 'email'],
|
const clientId = process.env.GOOGLE_CLIENT_ID;
|
||||||
session: false,
|
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
|
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 {
|
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, {
|
const authResult = await oauthService.completeOAuthLogin(result.profile, result.provider, {
|
||||||
linkUserId: state.mode === 'link' ? state.userId : null
|
linkUserId: state.mode === 'link' ? state.userId : null
|
||||||
});
|
});
|
||||||
@@ -73,6 +107,7 @@ class OAuthController {
|
|||||||
email: authResult.email || '',
|
email: authResult.email || '',
|
||||||
provider: result.provider
|
provider: result.provider
|
||||||
});
|
});
|
||||||
|
res.setHeader('Set-Cookie', this.clearStateCookie());
|
||||||
return res.redirect(`${target}?${params.toString()}`);
|
return res.redirect(`${target}?${params.toString()}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,9 +118,11 @@ class OAuthController {
|
|||||||
params.set('linked', result.provider);
|
params.set('linked', result.provider);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
res.setHeader('Set-Cookie', this.clearStateCookie());
|
||||||
return res.redirect(`${target}?${params.toString()}`);
|
return res.redirect(`${target}?${params.toString()}`);
|
||||||
} catch (callbackError) {
|
} catch (callbackError) {
|
||||||
console.error('Google OAuth Callback-Verarbeitung fehlgeschlagen:', 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`);
|
return res.redirect(`${process.env.FRONTEND_URL || 'http://localhost:5010'}/login?error=oauth_failed`);
|
||||||
}
|
}
|
||||||
})(req, res, next);
|
})(req, res, next);
|
||||||
@@ -175,4 +212,3 @@ class OAuthController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
module.exports = new OAuthController();
|
module.exports = new OAuthController();
|
||||||
|
|
||||||
|
|||||||
@@ -71,6 +71,7 @@ async function linkExistingAccount() {
|
|||||||
throw new Error(result.error || 'Verknüpfung fehlgeschlagen')
|
throw new Error(result.error || 'Verknüpfung fehlgeschlagen')
|
||||||
}
|
}
|
||||||
pendingToken.value = ''
|
pendingToken.value = ''
|
||||||
|
localStorage.removeItem('timeclock_oauth_link_started')
|
||||||
await finishLogin(result.token, '/settings/profile?oauthLinked=google')
|
await finishLogin(result.token, '/settings/profile?oauthLinked=google')
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
errorMessage.value = error.message || 'Verknüpfung fehlgeschlagen'
|
errorMessage.value = error.message || 'Verknüpfung fehlgeschlagen'
|
||||||
@@ -84,6 +85,7 @@ onMounted(async () => {
|
|||||||
const error = route.query.error
|
const error = route.query.error
|
||||||
const pending = route.query.pending
|
const pending = route.query.pending
|
||||||
const linked = route.query.linked
|
const linked = route.query.linked
|
||||||
|
const linkStarted = localStorage.getItem('timeclock_oauth_link_started') === 'google'
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
status.value = 'OAuth-Login fehlgeschlagen'
|
status.value = 'OAuth-Login fehlgeschlagen'
|
||||||
@@ -102,7 +104,10 @@ onMounted(async () => {
|
|||||||
|
|
||||||
if (token) {
|
if (token) {
|
||||||
try {
|
try {
|
||||||
await finishLogin(token, linked ? '/settings/profile?oauthLinked=google' : '/')
|
if (linked || linkStarted) {
|
||||||
|
localStorage.removeItem('timeclock_oauth_link_started')
|
||||||
|
}
|
||||||
|
await finishLogin(token, linked || linkStarted ? '/settings/profile?oauthLinked=google' : '/')
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
status.value = 'Fehler beim Login'
|
status.value = 'Fehler beim Login'
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@@ -180,4 +185,3 @@ onMounted(async () => {
|
|||||||
100% { transform: rotate(360deg); }
|
100% { transform: rotate(360deg); }
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|||||||
@@ -267,6 +267,7 @@ async function linkGoogle() {
|
|||||||
if (!response.ok || !result.url) {
|
if (!response.ok || !result.url) {
|
||||||
throw new Error(result.error || 'Google-Verknüpfung konnte nicht gestartet werden')
|
throw new Error(result.error || 'Google-Verknüpfung konnte nicht gestartet werden')
|
||||||
}
|
}
|
||||||
|
localStorage.setItem('timeclock_oauth_link_started', 'google')
|
||||||
window.location.href = result.url
|
window.location.href = result.url
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
await alert(`Fehler: ${error.message}`, 'Fehler')
|
await alert(`Fehler: ${error.message}`, 'Fehler')
|
||||||
@@ -310,7 +311,11 @@ onMounted(async () => {
|
|||||||
])
|
])
|
||||||
|
|
||||||
if (route.query.oauthLinked === 'google') {
|
if (route.query.oauthLinked === 'google') {
|
||||||
|
if (googleLinked.value) {
|
||||||
await alert('Google-Konto wurde erfolgreich verknüpft', 'Erfolg')
|
await alert('Google-Konto wurde erfolgreich verknüpft', 'Erfolg')
|
||||||
|
} else {
|
||||||
|
await alert('Google-Rückmeldung erhalten, aber es ist keine Google-Verknüpfung am Account gespeichert.', 'Fehler')
|
||||||
|
}
|
||||||
router.replace('/settings/profile')
|
router.replace('/settings/profile')
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -23,8 +23,8 @@ android {
|
|||||||
applicationId = "de.tsschulz.timeclock"
|
applicationId = "de.tsschulz.timeclock"
|
||||||
minSdk = 26
|
minSdk = 26
|
||||||
targetSdk = 36
|
targetSdk = 36
|
||||||
versionCode = 5
|
versionCode = 6
|
||||||
versionName = "0.8.0-alpha4"
|
versionName = "0.8.0-alpha5"
|
||||||
buildConfigField("String", "API_BASE_URL", "\"${apiBaseUrl.replace("\\", "\\\\").replace("\"", "\\\"")}\"")
|
buildConfigField("String", "API_BASE_URL", "\"${apiBaseUrl.replace("\\", "\\\\").replace("\"", "\\\"")}\"")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user