Files
stechuhr3/backend/src/services/OAuthService.js
Torsten Schulz (local) afd0d2935d Add method to generate unique IDs for AuthIdentity records in OAuthService
Implement getNextAuthIdentityId to ensure unique ID assignment during OAuth identity creation. Update existing identity creation calls to utilize this new method, enhancing data integrity in the OAuth flow.
2026-05-15 09:27:30 +02:00

298 lines
8.3 KiB
JavaScript

const jwt = require('jsonwebtoken');
const crypto = require('crypto');
const database = require('../config/database');
/**
* OAuth Service
* Verwaltet OAuth/SSO-Logins (Google, GitHub, etc.)
*/
class OAuthService {
constructor() {
this.jwtSecret = process.env.JWT_SECRET || 'your-secret-key-change-in-production';
this.jwtExpiration = process.env.JWT_EXPIRATION || '24h';
}
/**
* OAuth-Login/Registrierung
* @param {Object} profile - OAuth-Provider-Profil
* @param {string} provider - Provider-Name (google, github, etc.)
* @returns {Promise<Object>} Token und Benutzer-Info
*/
async authenticateWithProvider(profile, provider) {
return this.completeOAuthLogin(profile, provider);
}
async completeOAuthLogin(profile, provider, options = {}) {
const { User, AuthInfo, AuthIdentity, AuthToken } = database.getModels();
const providerId = profile.id;
const email = profile.emails && profile.emails[0] ? profile.emails[0].value : null;
const displayName = profile.displayName || profile.name || email;
if (!providerId) {
throw new Error('OAuth Provider-ID fehlt');
}
// Prüfen ob OAuth-Identity bereits existiert
let authIdentity = await AuthIdentity.findOne({
where: {
provider,
identity: providerId
},
include: [{
model: database.getModels().AuthInfo,
as: 'authInfo',
include: [{
model: User,
as: 'user'
}]
}]
});
let user, authInfo;
if (authIdentity && authIdentity.authInfo) {
// Bestehender OAuth-Benutzer
authInfo = authIdentity.authInfo;
user = authInfo.user;
if (options.linkUserId && Number(user.id) !== Number(options.linkUserId)) {
throw new Error('Dieser Google-Account ist bereits mit einem anderen Benutzer verknüpft');
}
} else {
if (options.linkUserId) {
authInfo = await AuthInfo.findOne({
where: { user_id: options.linkUserId },
include: [{ model: User, as: 'user' }]
});
if (!authInfo) {
throw new Error('Benutzer für Verknüpfung nicht gefunden');
}
authIdentity = await AuthIdentity.create({
id: await this.getNextAuthIdentityId(AuthIdentity),
auth_info_id: authInfo.id,
provider,
identity: providerId,
version: 0
});
user = authInfo.user;
} else {
// Neuer OAuth-Benutzer oder explizite Verknüpfung mit bestehendem Account
// Prüfen ob Benutzer mit dieser E-Mail bereits existiert
if (email) {
authInfo = await AuthInfo.findOne({
where: { email },
include: [{
model: User,
as: 'user'
}]
});
}
if (authInfo) {
return {
requiresLink: true,
pendingToken: this.createPendingToken(profile, provider),
email,
displayName
};
} else {
// Neuen Benutzer erstellen
user = await User.create({
full_name: displayName,
role: 0,
daily_hours: 8,
week_hours: 40,
week_workdays: 5,
preferred_title_type: 0,
version: 0,
last_change: new Date()
});
// Auth-Info erstellen (ohne Passwort für OAuth-only Accounts)
authInfo = await AuthInfo.create({
user_id: user.id,
email: email || `${provider}_${providerId}@oauth.local`,
password_hash: '', // Kein Passwort für OAuth-only
password_method: 'oauth',
password_salt: '',
status: 1,
failed_login_attempts: 0,
email_token: '',
email_token_role: 0,
unverified_email: '',
version: 0
});
// OAuth-Identity erstellen
authIdentity = await AuthIdentity.create({
id: await this.getNextAuthIdentityId(AuthIdentity),
auth_info_id: authInfo.id,
provider,
identity: providerId,
version: 0
});
}
}
}
return this.createLoginResult(user, authInfo, provider);
}
async getNextAuthIdentityId(AuthIdentity) {
const maxId = await AuthIdentity.max('id');
return Number(maxId || 0) + 1;
}
createPendingToken(profile, provider) {
return jwt.sign(
{
provider,
providerId: profile.id,
email: profile.emails && profile.emails[0] ? profile.emails[0].value : null,
displayName: profile.displayName || profile.name || null,
type: 'oauth-pending'
},
this.jwtSecret,
{ expiresIn: '15m' }
);
}
verifyPendingToken(pendingToken) {
const data = jwt.verify(pendingToken, this.jwtSecret);
if (data.type !== 'oauth-pending' || !data.provider || !data.providerId) {
throw new Error('Ungültiger OAuth-Verknüpfungstoken');
}
return data;
}
createStateToken(data) {
return jwt.sign({ ...data, type: 'oauth-state' }, this.jwtSecret, { expiresIn: '15m' });
}
verifyStateToken(stateToken) {
if (!stateToken) return {};
const data = jwt.verify(stateToken, this.jwtSecret);
if (data.type !== 'oauth-state') {
throw new Error('Ungültiger OAuth-State');
}
return data;
}
async linkPendingToPasswordAccount(pendingToken, email, password) {
const authService = require('./AuthService');
const pending = this.verifyPendingToken(pendingToken);
const loginResult = await authService.login(email, password, '0');
const profile = {
id: pending.providerId,
displayName: pending.displayName || loginResult.user.full_name,
emails: pending.email ? [{ value: pending.email }] : []
};
return this.completeOAuthLogin(profile, pending.provider, { linkUserId: loginResult.user.id });
}
async linkPendingToAuthenticatedUser(pendingToken, userId) {
const pending = this.verifyPendingToken(pendingToken);
const profile = {
id: pending.providerId,
displayName: pending.displayName || null,
emails: pending.email ? [{ value: pending.email }] : []
};
return this.completeOAuthLogin(profile, pending.provider, { linkUserId: userId });
}
createLoginResult(user, authInfo, provider) {
const { AuthToken } = database.getModels();
// JWT Token generieren
const token = jwt.sign(
{
userId: user.id,
email: authInfo.email,
role: user.role,
provider
},
this.jwtSecret,
{ expiresIn: this.jwtExpiration }
);
// Token in Datenbank speichern
const expiresAt = new Date();
expiresAt.setHours(expiresAt.getHours() + 24);
return AuthToken.create({
auth_info_id: authInfo.id,
value: crypto.createHash('sha256').update(token).digest('hex'),
expires: expiresAt,
version: 0
}).then(() => ({
token,
user: {
id: user.id,
full_name: user.full_name,
email: authInfo.email,
role: user.role,
provider
}
}));
}
/**
* OAuth-Identity für Benutzer abrufen
* @param {number} userId - Benutzer-ID
* @returns {Promise<Array>} Liste verknüpfter OAuth-Provider
*/
async getUserIdentities(userId) {
const { AuthInfo, AuthIdentity } = database.getModels();
const authInfo = await AuthInfo.findOne({
where: { user_id: userId },
include: [{
model: AuthIdentity,
as: 'identities'
}]
});
if (!authInfo || !authInfo.identities) {
return [];
}
return authInfo.identities.map(identity => ({
provider: identity.provider,
identity: identity.identity,
id: identity.id
}));
}
/**
* OAuth-Identity entfernen
* @param {number} userId - Benutzer-ID
* @param {string} provider - Provider-Name
* @returns {Promise<boolean>} Erfolg
*/
async unlinkProvider(userId, provider) {
const { AuthInfo, AuthIdentity } = database.getModels();
const authInfo = await AuthInfo.findOne({
where: { user_id: userId }
});
if (!authInfo) {
throw new Error('Benutzer nicht gefunden');
}
const deleted = await AuthIdentity.destroy({
where: {
auth_info_id: authInfo.id,
provider
}
});
return deleted > 0;
}
}
module.exports = new OAuthService();