const jwt = require("jsonwebtoken") const utils = require("../modules/utils") const bcrypt = require("bcryptjs") const crypto = require("node:crypto") const authRepository = require("../repositories/authRepository") const certsManager = require("../modules/certificatesManager") const { DefaultError } = require("../errors/errors") const keys = certsManager.getKeys() const uuidRegex = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/ async function registerUser({ username, password, email, registrationCountry, preferredLanguage, clientIp }) { try { await authRepository.getUser(username) throw new DefaultError(409, "Username taken.", "ForbiddenOperationException") } catch (error) { if (error.code !== 404) throw error } if (email) { try { await authRepository.getUser(email) throw new DefaultError(409, "E-Mail taken.", "ForbiddenOperationException") } catch (error) { if (error.code !== 404) throw error } } const userRegistration = await authRepository.register(email || "", username, password) const { uuid } = userRegistration const resolvedCountry = registrationCountry || await utils.getRegistrationCountryFromIp(clientIp) await authRepository.addPropertyToPlayer("registrationCountry", resolvedCountry || "UNKNOWN", uuid) await authRepository.addPropertyToPlayer("userPreferredLanguage", preferredLanguage || "fr-FR", uuid) return { code: 200, message: "User created successfully", uuid } } async function authenticate({ identifier, password, clientToken, requireUser }) { let userResult try { userResult = await authRepository.getUser(identifier, true) } catch (error) { if (error.code === 404) { throw new DefaultError(403, "Invalid credentials. Invalid username or password.", "ForbiddenOperationException") } throw error } const passwordValidationProcess = await bcrypt.compare(password, userResult.user.password) if (!passwordValidationProcess) { throw new DefaultError(403, "Invalid credentials. Invalid username or password.", "ForbiddenOperationException") } delete userResult.user.password const $clientToken = uuidRegex.test(clientToken) ? clientToken : crypto.randomUUID() const accessToken = jwt.sign({ uuid: userResult.user.uuid, username: userResult.user.username, clientToken: $clientToken, }, keys.authenticationKeys.private, { subject: userResult.user.uuid, issuer: "LentiaYggdrasil", expiresIn: "1d", algorithm: "RS256" }) const clientSessionProcess = await authRepository.insertClientSession(accessToken, $clientToken, userResult.user.uuid) const userObject = { clientToken: clientSessionProcess.clientToken, accessToken: clientSessionProcess.accessToken, availableProfiles: [{ name: userResult.user.username, id: userResult.user.uuid, }], selectedProfile: { name: userResult.user.username, id: userResult.user.uuid, } } if (requireUser) { try { const propertiesRequest = await authRepository.getPlayerProperties(userResult.user.uuid) userObject.user = { username: userResult.user.username, properties: propertiesRequest.properties } } catch (error) { if (error.code !== 404) throw error userObject.user = { username: userResult.user.username, properties: [] } } } return { code: 200, response: userObject } } async function refreshToken({ previousAccessToken, clientToken, requireUser }) { let sessionCheck try { sessionCheck = await authRepository.getClientSession(previousAccessToken, clientToken) } catch (error) { throw new DefaultError(403, "Invalid token or session expired.", "ForbiddenOperationException") } const uuid = sessionCheck.session.uuid const userResult = await authRepository.getUser(uuid, true) delete userResult.user.password await authRepository.invalidateClientSession(previousAccessToken, clientToken) const $clientToken = uuidRegex.test(clientToken) ? clientToken : crypto.randomUUID() const newAccessToken = jwt.sign({ uuid: userResult.user.uuid, username: userResult.user.username, clientToken: $clientToken, }, keys.authenticationKeys.private, { subject: userResult.user.uuid, issuer: "LentiaYggdrasil", expiresIn: "1d", algorithm: "RS256" }) const clientSessionProcess = await authRepository.insertClientSession(newAccessToken, $clientToken, userResult.user.uuid) const userObject = { clientToken: clientSessionProcess.clientToken, accessToken: clientSessionProcess.accessToken, selectedProfile: { name: userResult.user.username, id: userResult.user.uuid, } } if (requireUser) { try { const propertiesRequest = await authRepository.getPlayerProperties(userResult.user.uuid) userObject.user = { username: userResult.user.username, properties: propertiesRequest.properties } } catch (error) { if (error.code !== 404) throw error userObject.user = { username: userResult.user.username, properties: [] } } } return { code: 200, response: userObject } } async function validate({ accessToken, clientToken }) { if (clientToken !== undefined) { await authRepository.validateClientSession(accessToken, clientToken) } else { await authRepository.validateClientSessionWithoutClientToken(accessToken) } return { code: 204 } } async function invalidate({ accessToken, clientToken }) { await authRepository.invalidateClientSession(accessToken, clientToken) return { code: 204 } } async function signout({ uuid }) { await authRepository.revokeAccessTokens(uuid) return { code: 204 } } async function verifyAccessToken({ accessToken }) { try { if (!accessToken) { throw new DefaultError(400, "Token is missing.") } const decoded = jwt.verify(accessToken, keys.authenticationKeys.public, { algorithms: ["RS256"], issuer: "LentiaYggdrasil" }) const clientToken = decoded.clientToken if (!clientToken) { throw new DefaultError(403, "Token format invalid (missing clientToken).") } try { await authRepository.validateClientSession(accessToken, clientToken) } catch (error) { throw new DefaultError(401, "Session has been revoked or invalidated.") } return { code: 200, user: { uuid: decoded.sub, username: decoded.username }, session: { clientToken } } } catch (error) { if (error instanceof DefaultError) throw error if (error.name === "TokenExpiredError") { throw new DefaultError(401, "Token has expired.") } throw new DefaultError(500, "Internal Verification Error", error.toString()) } } module.exports = { signout, validate, invalidate, registerUser, authenticate, refreshToken, verifyAccessToken, }