UNPKG

@qelos/auth

Version:

Express Passport authentication service

210 lines (182 loc) 7.25 kB
import { AuthRequest } from '../../../types'; import User, { UserDocument } from '../../models/user'; import { DecryptedSourceAuthentication, getIntegrationSource } from '../../services/integration-source'; import { getRequestHost } from '../../services/req-host'; import jwt from 'jsonwebtoken'; import { getCookieTokenName, getUser } from '../../services/users'; import { getSignedToken, getUniqueId, setCookie } from '../../services/tokens'; import { cookieTokenExpiration } from '../../../config'; import { setEncryptedData } from '../../services/encrypted-data'; import { emitPlatformEvent } from '@qelos/api-kit'; import { getWorkspaceConfiguration } from '../../services/workspace-configuration'; import { getWorkspaceForUser } from '../../services/workspaces'; import { uploadProfileImage } from '../../services/assets-service-api'; import logger from '../../services/logger'; const LINKEDIN_AUTH_URL = 'https://www.linkedin.com/oauth/v2/authorization' const LINKEDIN_TOKEN_URL = 'https://www.linkedin.com/oauth/v2/accessToken' type AuthWithLinkedinRequest = AuthRequest & { source: DecryptedSourceAuthentication }; export async function getLinkedinSource(req: AuthWithLinkedinRequest, res, next) { if (!req.authConfig.socialLoginsSources?.linkedin) { res.status(400).json({ message: 'LinkedIn social login does not exist' }).end(); return; } const source = await getIntegrationSource(req.headers.tenant, req.authConfig.socialLoginsSources.linkedin); if (!source) { res.status(400).json({ message: 'LinkedIn social login is not enabled' }).end(); return; } req.source = source; next(); } function getLinkedInRedirectUri(tenantHost: string): string { const fullTenantHost = tenantHost.startsWith('http://') || tenantHost.startsWith('https://') ? tenantHost : `https://${tenantHost}`; return `${fullTenantHost}/api/auth/linkedin/callback`; } export async function loginWithLinkedIn(req: AuthWithLinkedinRequest, res) { const { clientId, scope } = req.source.metadata; const redirectUri = getLinkedInRedirectUri(req.headers.tenanthost); const linkedinAuthUrl = `${LINKEDIN_AUTH_URL}?response_type=code&client_id=${clientId}&redirect_uri=${encodeURIComponent(redirectUri)}&scope=${encodeURIComponent(scope)}`; res.redirect(linkedinAuthUrl); } export async function authCallbackFromLinkedIn(req: AuthWithLinkedinRequest, res) { const { clientId } = req.source.metadata; const { clientSecret } = req.source.authentication; const authCode = Array.isArray(req.query.code) ? req.query.code[0] : req.query.code; if (!authCode || typeof authCode !== 'string') { return res.status(400).json({ message: 'Invalid authorization code' }); } const redirectUri = getLinkedInRedirectUri(req.headers.tenanthost); try { // Exchange the authorization code for an access token and ID token const tokenResponse = await fetch(LINKEDIN_TOKEN_URL, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ grant_type: 'authorization_code', code: authCode, redirect_uri: redirectUri, client_id: clientId, client_secret: clientSecret, }).toString(), }); const tokenData = await tokenResponse.json(); if (!tokenResponse.ok) { res.status(tokenResponse.status).json({ message: 'Failed to get access token', details: tokenData }).end(); return; } if (!tokenData.access_token) { res.status(400).json({ message: 'Failed to get access token' }).end(); return; } if (!tokenData.id_token) { res.status(400).json({ message: 'ID token is missing in the response' }).end(); return } let userData; if (tokenData.id_token) { userData = jwt.decode(tokenData.id_token); } let user: UserDocument; try { user = await getUser({ username: userData.email, tenant: req.headers.tenant }); user.email = userData.email; if (userData.picture) { try { user.profileImage = await uploadProfileImage( req.headers.tenant, (user._id as string), userData.picture ); } catch (error) { logger.error('failed to upload profile image', error); user.profileImage = userData.picture; // Fallback to original URL } } if (!user.emailVerified) { user.emailVerified = true; } if (!user.socialLogins?.includes('linkedin')) { user.socialLogins = user.socialLogins || []; user.socialLogins.push('linkedin'); user.markModified('socialLogins'); } await user.save(); } catch { if (typeof req.authConfig.allowSocialAutoRegistration === 'boolean' && !req.authConfig.allowSocialAutoRegistration) { res.redirect('/login?error=needs-registration&email=' + encodeURIComponent(userData.email)); return; } const tempUserId = `profile_${Date.now()}`; let profileImage = ""; if (userData.picture) { try { profileImage = await uploadProfileImage( req.headers.tenant, tempUserId, userData.picture ); } catch (error) { profileImage = userData.picture; } } user = new User({ tenant: req.headers.tenant, username: userData.email, email: userData.email, fullName: userData.name || '', firstName: userData.given_name || '', lastName: userData.family_name || '', profileImage, emailVerified: true, socialLogins: ['linkedin'], }); await user.save(); emitPlatformEvent({ tenant: req.headers.tenant, user: user.id, source: 'auth', kind: 'signup', eventName: 'user-registered', description: 'User registered via LinkedIn', metadata: { user: { tenant: user.tenant, username: user.username, email: user.email, fullName: user.fullName, firstName: user.firstName, lastName: user.lastName, profileImage: user.profileImage, roles: user.roles, _id: user._id, created: user.created, }, source: 'linkedin', }, }); } await setEncryptedData(req.headers.tenant, `${user.id}-linkedinToken`, JSON.stringify(tokenData)) let workspace; try { const wsConfig = await getWorkspaceConfiguration(req.headers.tenant) if (wsConfig.isActive) { workspace = await getWorkspaceForUser(req.headers.tenant, user._id, user.lastLogin?.workspace || user.tokens?.at(-1)?.metadata?.workspace); } } catch { logger.log('Error getting workspace in linkedin login'); } const requestHost = getRequestHost(req); const { token: newToken } = getSignedToken( user, workspace, getUniqueId(), String(cookieTokenExpiration / 1000) ); setCookie(res, getCookieTokenName(user.tenant), newToken, null, requestHost); res.redirect('/'); } catch (error) { return res.status(500).json({ message: 'Internal server error' }); } }