diff --git a/package.json b/package.json index 5db4d7b..c114de5 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "@nuxtjs/tailwindcss": "^6.12.2", "@prisma/client": "^6.1.0", "@tailwindcss/vite": "^4.0.6", + "argon2": "^0.41.1", "axios": "^1.7.7", "bcryptjs": "^2.4.3", "cookie-es": "^1.2.2", diff --git a/prisma/schema/auth.prisma b/prisma/schema/auth.prisma index bf862dd..8e003d2 100644 --- a/prisma/schema/auth.prisma +++ b/prisma/schema/auth.prisma @@ -7,7 +7,8 @@ model LinkedAuthMec { mec AuthMec enabled Boolean @default(true) - credentials Json + credentials Json // TODO: remove this, automate via migration + password String? user User @relation(fields: [userId], references: [id]) diff --git a/server/api/v1/auth/signin/simple.post.ts b/server/api/v1/auth/signin/simple.post.ts index db0ad35..281e7b5 100644 --- a/server/api/v1/auth/signin/simple.post.ts +++ b/server/api/v1/auth/signin/simple.post.ts @@ -1,7 +1,10 @@ import { AuthMec } from "@prisma/client"; import { JsonArray } from "@prisma/client/runtime/library"; import prisma from "~/server/internal/db/database"; -import { checkHash } from "~/server/internal/security/simple"; +import { + checkHashArgon2, + checkHashBcrypt, +} from "~/server/internal/security/simple"; import sessionHandler from "~/server/internal/session"; export default defineEventHandler(async (h3) => { @@ -19,10 +22,20 @@ export default defineEventHandler(async (h3) => { const authMek = await prisma.linkedAuthMec.findFirst({ where: { mec: AuthMec.Simple, - credentials: { - array_starts_with: username, - }, enabled: true, + OR: [ + { + // TODO: check if this is even needed with below condition + credentials: { + array_starts_with: username, + }, + }, + { + user: { + username, + }, + }, + ], }, include: { user: { @@ -38,24 +51,50 @@ export default defineEventHandler(async (h3) => { statusCode: 401, statusMessage: "Invalid username or password.", }); - - const credentials = authMek.credentials as JsonArray; - const hash = credentials.at(1); - - if (!hash || !authMek.user.enabled) + else if (!authMek.user.enabled) throw createError({ statusCode: 403, statusMessage: "Invalid or disabled account. Please contact the server administrator.", }); - if (!(await checkHash(password, hash.toString()))) - throw createError({ - statusCode: 401, - statusMessage: "Invalid username or password.", - }); + // if using old auth schema + if (Array.isArray(authMek.credentials)) { + const hash = authMek.credentials.at(1); - await sessionHandler.setUserId(h3, authMek.userId, rememberMe); + if (!hash) + throw createError({ + statusCode: 403, + statusMessage: + "Invalid password state. Please contact the server administrator.", + }); - return { result: true, userId: authMek.userId }; + if (!(await checkHashBcrypt(password, hash.toString()))) + throw createError({ + statusCode: 401, + statusMessage: "Invalid username or password.", + }); + + // TODO: send user to forgot password screen or something to force them to change their password to new system + await sessionHandler.setUserId(h3, authMek.userId, rememberMe); + return { result: true, userId: authMek.userId }; + } else { + // using new (modern) login flow + + if (authMek.password === null) + throw createError({ + statusCode: 500, + statusMessage: + "Invalid password state. Please contact the server administrator.", + }); + else if (!(await checkHashArgon2(password, authMek.password))) + throw createError({ + statusCode: 401, + statusMessage: "Invalid username or password.", + }); + + await sessionHandler.setUserId(h3, authMek.userId, rememberMe); + + return { result: true, userId: authMek.userId }; + } }); diff --git a/server/api/v1/auth/signup/simple.post.ts b/server/api/v1/auth/signup/simple.post.ts index 895ec51..f96af7f 100644 --- a/server/api/v1/auth/signup/simple.post.ts +++ b/server/api/v1/auth/signup/simple.post.ts @@ -1,6 +1,6 @@ import { AuthMec, Invitation } from "@prisma/client"; import prisma from "~/server/internal/db/database"; -import { createHash } from "~/server/internal/security/simple"; +import { createHashArgon2 } from "~/server/internal/security/simple"; import { v4 as uuidv4 } from "uuid"; import * as jdenticon from "jdenticon"; import objectHandler from "~/server/internal/objects"; @@ -97,6 +97,7 @@ export default defineEventHandler(async (h3) => { ); const user = await prisma.user.create({ data: { + id: userId, username, displayName, email, @@ -105,12 +106,13 @@ export default defineEventHandler(async (h3) => { }, }); - const hash = await createHash(password); + const hash = await createHashArgon2(password); await prisma.linkedAuthMec.create({ data: { mec: AuthMec.Simple, - credentials: [username, hash], + credentials: {}, userId: user.id, + password: hash, }, }); diff --git a/server/internal/security/simple.ts b/server/internal/security/simple.ts index 9494d7f..6482299 100644 --- a/server/internal/security/simple.ts +++ b/server/internal/security/simple.ts @@ -1,11 +1,19 @@ -import bcrypt from 'bcryptjs'; +import bcrypt from "bcryptjs"; +import * as argon2 from "argon2"; -const rounds = 10; +// const bcryptRounds = 10; +// export async function createHashBcrypt(password: string) { +// return await bcrypt.hash(password, bcryptRounds); +// } -export async function createHash(password: string) { - return bcrypt.hashSync(password, rounds); +export async function checkHashBcrypt(password: string, hash: string) { + return await bcrypt.compare(password, hash); } -export async function checkHash(password: string, hash: string) { - return bcrypt.compareSync(password, hash); -} \ No newline at end of file +export async function createHashArgon2(password: string) { + return await argon2.hash(password); +} + +export async function checkHashArgon2(password: string, hash: string) { + return await argon2.verify(hash, password); +} diff --git a/yarn.lock b/yarn.lock index b6d2c6c..baaaabe 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1266,6 +1266,11 @@ "@parcel/watcher-win32-ia32" "2.5.0" "@parcel/watcher-win32-x64" "2.5.0" +"@phc/format@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@phc/format/-/format-1.0.0.tgz#b5627003b3216dc4362125b13f48a4daa76680e4" + integrity sha512-m7X9U6BG2+J+R1lSOdCiITLLrxm+cWlNI3HUFA92oLO77ObGNzaKdh8pMLqdZcshtkKuV84olNNXDfMc4FezBQ== + "@pkgjs/parseargs@^0.11.0": version "0.11.0" resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33" @@ -2087,6 +2092,15 @@ arg@^5.0.2: resolved "https://registry.yarnpkg.com/arg/-/arg-5.0.2.tgz#c81433cc427c92c4dcf4865142dbca6f15acd59c" integrity sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg== +argon2@^0.41.1: + version "0.41.1" + resolved "https://registry.yarnpkg.com/argon2/-/argon2-0.41.1.tgz#30ce6b013e273bc7e92c558d40e66d35e5e8c63b" + integrity sha512-dqCW8kJXke8Ik+McUcMDltrbuAWETPyU6iq+4AhxqKphWi7pChB/Zgd/Tp/o8xRLbg8ksMj46F/vph9wnxpTzQ== + dependencies: + "@phc/format" "^1.0.0" + node-addon-api "^8.1.0" + node-gyp-build "^4.8.1" + argparse@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" @@ -4958,6 +4972,11 @@ node-addon-api@^7.0.0: resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-7.1.1.tgz#1aba6693b0f255258a049d621329329322aad558" integrity sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ== +node-addon-api@^8.1.0: + version "8.3.1" + resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-8.3.1.tgz#53bc8a4f8dbde3de787b9828059da94ba9fd4eed" + integrity sha512-lytcDEdxKjGJPTLEfW4mYMigRezMlyJY8W4wxJK8zE533Jlb8L8dRuObJFWg2P+AuOIxoCgKF+2Oq4d4Zd0OUA== + node-fetch-native@^1.6.3, node-fetch-native@^1.6.4: version "1.6.4" resolved "https://registry.yarnpkg.com/node-fetch-native/-/node-fetch-native-1.6.4.tgz#679fc8fd8111266d47d7e72c379f1bed9acff06e" @@ -4985,6 +5004,11 @@ node-gyp-build@^4.2.2: resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.8.2.tgz#4f802b71c1ab2ca16af830e6c1ea7dd1ad9496fa" integrity sha512-IRUxE4BVsHWXkV/SFOut4qTlagw2aM8T5/vnTsmrHJvVoKueJHRc/JaFND7QDDc61kLYUJ6qlZM3sqTSyx2dTw== +node-gyp-build@^4.8.1: + version "4.8.4" + resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.8.4.tgz#8a70ee85464ae52327772a90d66c6077a900cfc8" + integrity sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ== + node-mock-http@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/node-mock-http/-/node-mock-http-1.0.0.tgz#4b32cd509c7f46d844e68ea93fb8be405a18a42a"