[BREAKING] Auth: replace current auth tokens with more secure ones
previously tokens were only like IDs, time based and incrementing counter. An attacker could easily bruteforce them. This patch changes tokens to be completely random. fixes #2
This commit is contained in:
parent
434ae5843e
commit
ebf5690519
5 changed files with 72 additions and 15 deletions
|
@ -20,12 +20,14 @@ model User {
|
||||||
}
|
}
|
||||||
|
|
||||||
model Session {
|
model Session {
|
||||||
id BigInt @id @default(dbgenerated("(((unix_timestamp() * 1000) * pow(2,22)) + floor((rand() * pow(2,12))))")) @db.UnsignedBigInt
|
id BigInt @id @default(dbgenerated("(((unix_timestamp() * 1000) * pow(2,22)) + floor((rand() * pow(2,12))))")) @db.UnsignedBigInt
|
||||||
userId BigInt @map("user") @db.UnsignedBigInt
|
userId BigInt @map("user") @db.UnsignedBigInt
|
||||||
expiry_date DateTime? @default(dbgenerated("(now() + interval 30 day)")) @db.Timestamp(0)
|
sessionToken Bytes @db.Binary(64)
|
||||||
user User @relation(fields: [userId], references: [id])
|
expiry_date DateTime? @default(dbgenerated("(now() + interval 30 day)")) @db.Timestamp(0)
|
||||||
|
user User @relation(fields: [userId], references: [id])
|
||||||
|
|
||||||
@@index([userId], map: "user_idx")
|
@@index([userId], map: "user_idx")
|
||||||
|
@@index([sessionToken])
|
||||||
@@map("sessions")
|
@@map("sessions")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { defineEventHandler, getCookie, setCookie, readBody } from "h3";
|
||||||
import { database } from "../utils/database";
|
import { database } from "../utils/database";
|
||||||
import { isString } from "../utils/isString";
|
import { isString } from "../utils/isString";
|
||||||
import { cookieSettings } from "../utils/rootUtils";
|
import { cookieSettings } from "../utils/rootUtils";
|
||||||
import Snowflake from "~/utils/snowflake";
|
import SessionToken from "../utils/SessionToken";
|
||||||
|
|
||||||
import { createError } from "#imports";
|
import { createError } from "#imports";
|
||||||
|
|
||||||
|
@ -40,14 +40,11 @@ export default defineEventHandler(async (e) => {
|
||||||
|
|
||||||
if (account === null) throw createError({ statusCode: 400, message: "Invalid username or password." });
|
if (account === null) throw createError({ statusCode: 400, message: "Invalid username or password." });
|
||||||
|
|
||||||
const sessionId = new Snowflake();
|
const session = new SessionToken(account.id);
|
||||||
|
|
||||||
await database.session.create({
|
await database.session.create({
|
||||||
data: {
|
data: session.toPrisma(),
|
||||||
id: sessionId.state,
|
|
||||||
userId: account.id,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
setCookie(e, "token", sessionId.toString(), cookieSettings);
|
setCookie(e, "token", session.toString(), cookieSettings);
|
||||||
return { message: "Login successful", token: sessionId.toString() };
|
return { message: "Login successful", token: session.toString() };
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
import { defineEventHandler, getCookie } from "h3";
|
import { defineEventHandler, getCookie } from "h3";
|
||||||
import { createError } from "#imports";
|
import SessionToken from "../utils/SessionToken";
|
||||||
|
|
||||||
import { database } from "~/server/utils/database";
|
import { database } from "~/server/utils/database";
|
||||||
import getRequestingUser from "~/server/utils/getRequestingUser";
|
import getRequestingUser from "~/server/utils/getRequestingUser";
|
||||||
|
|
||||||
|
import { createError } from "#imports";
|
||||||
|
|
||||||
const endpointsWithoutAuth: string[] = [
|
const endpointsWithoutAuth: string[] = [
|
||||||
"/dbtest",
|
"/dbtest",
|
||||||
"/echo",
|
"/echo",
|
||||||
|
@ -37,7 +39,10 @@ export async function isAuthorised(token: string | undefined): Promise<boolean>
|
||||||
try {
|
try {
|
||||||
await database.session.findUniqueOrThrow({
|
await database.session.findUniqueOrThrow({
|
||||||
where: {
|
where: {
|
||||||
id: BigInt(token),
|
...SessionToken.fromString(token).toPrisma(),
|
||||||
|
expiry_date: {
|
||||||
|
gte: new Date(),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
49
server/utils/SessionToken.ts
Normal file
49
server/utils/SessionToken.ts
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
import crypto from "node:crypto";
|
||||||
|
import { type Session } from "@prisma/client";
|
||||||
|
|
||||||
|
import Snowflake from "~/utils/snowflake";
|
||||||
|
|
||||||
|
/** Represents a Session token, without expiry data. */
|
||||||
|
export default class SessionToken {
|
||||||
|
userId: bigint;
|
||||||
|
sessionId: bigint;
|
||||||
|
sessionToken: Buffer;
|
||||||
|
|
||||||
|
constructor(userId: bigint, sessionId?: bigint, sessionToken?: Buffer) {
|
||||||
|
this.userId = userId;
|
||||||
|
this.sessionId = sessionId ?? new Snowflake().state;
|
||||||
|
this.sessionToken = sessionToken ?? crypto.randomBytes(64);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Creates SessionToken from a string.
|
||||||
|
* @param string The strinct to create from.
|
||||||
|
* @returns The SessionToken object.
|
||||||
|
*/
|
||||||
|
static fromString(string: string): SessionToken {
|
||||||
|
const parameters = string.split(".");
|
||||||
|
return new SessionToken(
|
||||||
|
Buffer.from(parameters[0], "base64").readBigUInt64LE(),
|
||||||
|
Buffer.from(parameters[1], "base64").readBigUInt64LE(),
|
||||||
|
Buffer.from(parameters[2], "base64"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
toString(): string {
|
||||||
|
const stringUserId = Buffer.copyBytesFrom(new BigUint64Array([this.userId])).toString("base64");
|
||||||
|
const stringSessionId = Buffer.copyBytesFrom(new BigUint64Array([this.sessionId])).toString("base64");
|
||||||
|
const stringSessionToken = this.sessionToken.toString("base64");
|
||||||
|
return `${stringUserId}.${stringSessionId}.${stringSessionToken}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns this SessionToken as Prisma object.
|
||||||
|
* For use in where parameter.
|
||||||
|
* @returns this as prisma object.
|
||||||
|
*/
|
||||||
|
toPrisma(): Omit<Session, "expiry_date"> {
|
||||||
|
return {
|
||||||
|
id: this.sessionId,
|
||||||
|
userId: this.userId,
|
||||||
|
sessionToken: this.sessionToken,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,7 @@
|
||||||
import { getCookie, H3Event } from "h3";
|
import { getCookie, H3Event } from "h3";
|
||||||
|
|
||||||
import { database } from "./database";
|
import { database } from "./database";
|
||||||
|
import SessionToken from "./SessionToken";
|
||||||
|
|
||||||
import { createError } from "#imports";
|
import { createError } from "#imports";
|
||||||
|
|
||||||
|
@ -9,7 +10,10 @@ export default async function getRequestingUser(e: H3Event) {
|
||||||
if (!cookie) throw createError("User not found");
|
if (!cookie) throw createError("User not found");
|
||||||
const { user } = await database.session.findUnique({
|
const { user } = await database.session.findUnique({
|
||||||
where: {
|
where: {
|
||||||
id: BigInt(cookie),
|
...SessionToken.fromString(cookie).toPrisma(),
|
||||||
|
expiry_date: {
|
||||||
|
gte: new Date(),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
select: {
|
select: {
|
||||||
user: {
|
user: {
|
||||||
|
|
Loading…
Reference in a new issue