358 lines
11 KiB
JavaScript
358 lines
11 KiB
JavaScript
const jwt = require("jsonwebtoken")
|
|
const utils = require("../modules/utils")
|
|
const bcrypt = require("bcryptjs")
|
|
const crypto = require("node:crypto")
|
|
const userRepository = require("../repositories/userRepository")
|
|
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}$/
|
|
const usernameRegex = /^[a-zA-Z0-9_]{3,16}$/
|
|
|
|
async function registerUser({ username, password, email, registrationCountry, preferredLanguage, clientIp }) {
|
|
const availability = await checkUsernameAvailability(username)
|
|
if (availability.status === "DUPLICATE") {
|
|
throw new DefaultError(409, "Username taken.", "ForbiddenOperationException")
|
|
}
|
|
if (availability.status === "NOT_ALLOWED") {
|
|
throw new DefaultError(400, availability.message || "Username invalid.", "InvalidUsernameException")
|
|
}
|
|
|
|
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 userRepository.addPropertyToPlayer("registrationCountry", resolvedCountry || "UNKNOWN", uuid)
|
|
await userRepository.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 = clientToken || crypto.randomUUID()
|
|
const accessToken = jwt.sign({
|
|
uuid: userResult.user.uuid,
|
|
username: userResult.user.username,
|
|
clientToken: $clientToken,
|
|
type: "Minecraft"
|
|
}, 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 authenticateWithoutPassword({ identifier, 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
|
|
}
|
|
|
|
delete userResult.user.password
|
|
|
|
const $clientToken = crypto.randomUUID()
|
|
const accessToken = jwt.sign({
|
|
uuid: userResult.user.uuid,
|
|
username: userResult.user.username,
|
|
clientToken: $clientToken,
|
|
type: "Minecraft_OAuth2"
|
|
}, 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 = clientToken || crypto.randomUUID()
|
|
const newAccessToken = jwt.sign({
|
|
uuid: userResult.user.uuid,
|
|
username: userResult.user.username,
|
|
clientToken: $clientToken,
|
|
type: "Minecraft"
|
|
}, 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.")
|
|
}
|
|
|
|
const account = await authRepository.getUser(decoded.sub)
|
|
|
|
return {
|
|
code: 200,
|
|
user: {
|
|
uuid: decoded.sub,
|
|
username: account.user.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())
|
|
}
|
|
}
|
|
|
|
async function checkUsernameAvailability(username) {
|
|
if (!usernameRegex.test(username)) {
|
|
return {
|
|
code: 200,
|
|
status: "NOT_ALLOWED",
|
|
message: "Invalid characters or length."
|
|
}
|
|
}
|
|
|
|
try {
|
|
await authRepository.getUser(username)
|
|
return {
|
|
code: 200,
|
|
status: "DUPLICATE",
|
|
message: "Username is already in use."
|
|
}
|
|
} catch (error) {
|
|
if (error.code !== 404) throw error
|
|
}
|
|
|
|
const blocklist = await authRepository.getUsernamesRules()
|
|
const normalizedUsername = username.toLowerCase()
|
|
|
|
for (const entry of blocklist) {
|
|
if (entry.type === "literal") {
|
|
if (normalizedUsername === entry.value) {
|
|
return {
|
|
code: 200,
|
|
status: "NOT_ALLOWED",
|
|
message: "Username is reserved."
|
|
}
|
|
}
|
|
}
|
|
else if (entry.type === "regex") {
|
|
if (entry.pattern.test(username)) {
|
|
return {
|
|
code: 200,
|
|
status: "NOT_ALLOWED",
|
|
message: "Username contains forbidden words."
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return {
|
|
code: 200,
|
|
status: "AVAILABLE"
|
|
}
|
|
}
|
|
|
|
module.exports = {
|
|
signout,
|
|
validate,
|
|
invalidate,
|
|
authenticate,
|
|
refreshToken,
|
|
registerUser,
|
|
verifyAccessToken,
|
|
checkUsernameAvailability,
|
|
authenticateWithoutPassword,
|
|
} |