From e8fe86ea4e92c6176d0c839bf1c76468b505daef Mon Sep 17 00:00:00 2001 From: jeffvli Date: Tue, 3 May 2022 15:31:21 -0700 Subject: [PATCH] Fix session auth Add custom session secret Set session to rolling 30 day expiry 10 minute check Custom cookie name --- src/server/lib/passport.ts | 41 +++++++++--------- src/server/middleware/authenticateLocal.ts | 35 ++++++++++++++++ src/server/middleware/index.ts | 1 + .../migration.sql | 19 +-------- src/server/prisma/schema.prisma | 17 ++------ src/server/prisma/seed.ts | 1 + src/server/routes/auth.ts | 11 ++++- src/server/routes/users.ts | 3 +- src/server/server.ts | 42 ++++++++++--------- src/server/utils/api-error.ts | 16 +++++++ src/types.ts | 10 +++++ 11 files changed, 123 insertions(+), 73 deletions(-) create mode 100644 src/server/middleware/authenticateLocal.ts rename src/server/prisma/migrations/{20220502070432_init => 20220503220232_init}/migration.sql (92%) diff --git a/src/server/lib/passport.ts b/src/server/lib/passport.ts index 9418396..b517a12 100644 --- a/src/server/lib/passport.ts +++ b/src/server/lib/passport.ts @@ -1,31 +1,28 @@ import bcrypt from 'bcryptjs'; -import passport, { PassportStatic } from 'passport'; +import passport from 'passport'; +import { Strategy } from 'passport-local'; -import orm from './prisma'; +import prisma from './prisma'; -const LocalStrategy = require('passport-local'); +passport.use( + new Strategy(async (username: string, password: string, done: any) => { + const user = await prisma.user.findUnique({ where: { username } }); -export = (p: PassportStatic) => { - p.use( - new LocalStrategy(async (username: string, password: string, done: any) => { - const user = await orm.user.findUnique({ where: { username } }); + if (user === null || user === undefined) { + return done(null, false); + } - if (!user) { - return done(null, false); - } + if (!user.enabled) { + return done(null, false, { message: 'The user is not enabled.' }); + } - if (!user.enabled) { - return done(null, false, { message: 'The user is not enabled.' }); - } + if (await bcrypt.compare(password, user.password)) { + return done(null, user); + } - if (await bcrypt.compare(password, user.password)) { - return done(null, user); - } - - return done(null, false, { message: 'Invalid credentials.' }); - }) - ); -}; + return done(null, false, { message: 'Invalid credentials.' }); + }) +); passport.serializeUser((user: any, done) => { return done(null, user.id); @@ -34,7 +31,7 @@ passport.serializeUser((user: any, done) => { passport.deserializeUser(async (id: number, done) => { return done( null, - await orm.user.findUnique({ + await prisma.user.findUnique({ where: { id, }, diff --git a/src/server/middleware/authenticateLocal.ts b/src/server/middleware/authenticateLocal.ts new file mode 100644 index 0000000..22e384a --- /dev/null +++ b/src/server/middleware/authenticateLocal.ts @@ -0,0 +1,35 @@ +import { NextFunction, Request, Response } from 'express'; +import passport from 'passport'; + +const authenticateLocal = (req: Request, res: Response, next: NextFunction) => { + passport.authenticate('local', { session: true }, (err, _user, info) => { + if (err) { + return next(err); + } + + if (!req.user) { + return res.status(401).json({ + statusCode: 401, + response: 'Error', + error: { + message: info?.message || 'Invalid authorization.', + path: req.path, + }, + }); + } + + const u: any = req.user; + + req.user = { + id: u?.id, + username: u?.username, + createdAt: u?.createdAt, + updatedAt: u?.updatedAt, + enabled: u?.enabled, + }; + + return next(); + })(req, res, next); +}; + +export default authenticateLocal; diff --git a/src/server/middleware/index.ts b/src/server/middleware/index.ts index 695c08b..6b7ad01 100644 --- a/src/server/middleware/index.ts +++ b/src/server/middleware/index.ts @@ -1 +1,2 @@ export { default as errorHandler } from './error-handler'; +export { default as authenticateLocal } from './authenticateLocal'; diff --git a/src/server/prisma/migrations/20220502070432_init/migration.sql b/src/server/prisma/migrations/20220503220232_init/migration.sql similarity index 92% rename from src/server/prisma/migrations/20220502070432_init/migration.sql rename to src/server/prisma/migrations/20220503220232_init/migration.sql index 9fdd5f1..c40e52f 100644 --- a/src/server/prisma/migrations/20220502070432_init/migration.sql +++ b/src/server/prisma/migrations/20220503220232_init/migration.sql @@ -15,17 +15,8 @@ CREATE TABLE "User" ( "password" TEXT NOT NULL, "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, "updatedAt" TIMESTAMP(3) NOT NULL, - "enabled" BOOLEAN NOT NULL, - - PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "UserRole" ( - "id" SERIAL NOT NULL, - "server" INTEGER NOT NULL, - "playlist" INTEGER NOT NULL, - "userId" INTEGER NOT NULL, + "enabled" BOOLEAN NOT NULL DEFAULT false, + "isAdmin" BOOLEAN NOT NULL DEFAULT false, PRIMARY KEY ("id") ); @@ -155,9 +146,6 @@ CREATE UNIQUE INDEX "Session.sid_unique" ON "Session"("sid"); -- CreateIndex CREATE UNIQUE INDEX "User.username_unique" ON "User"("username"); --- CreateIndex -CREATE UNIQUE INDEX "UserRole.userId_unique" ON "UserRole"("userId"); - -- CreateIndex CREATE UNIQUE INDEX "Server.url_unique" ON "Server"("url"); @@ -173,9 +161,6 @@ CREATE UNIQUE INDEX "Album.serverId_remoteId_unique" ON "Album"("serverId", "rem -- CreateIndex CREATE UNIQUE INDEX "Song.serverId_remoteId_unique" ON "Song"("serverId", "remoteId"); --- AddForeignKey -ALTER TABLE "UserRole" ADD FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; - -- AddForeignKey ALTER TABLE "Server" ADD FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/src/server/prisma/schema.prisma b/src/server/prisma/schema.prisma index 1e1af8c..0a0832b 100644 --- a/src/server/prisma/schema.prisma +++ b/src/server/prisma/schema.prisma @@ -20,20 +20,11 @@ model User { password String createdAt DateTime @default(now()) updatedAt DateTime @updatedAt - enabled Boolean + enabled Boolean @default(false) + isAdmin Boolean @default(false) - servers Server[] - tasks Task[] - userRole UserRole? -} - -model UserRole { - id Int @id @default(autoincrement()) - server Int - playlist Int - - user User @relation(fields: [userId], references: [id]) - userId Int @unique + servers Server[] + tasks Task[] } model Server { diff --git a/src/server/prisma/seed.ts b/src/server/prisma/seed.ts index 21aced5..e6044d3 100644 --- a/src/server/prisma/seed.ts +++ b/src/server/prisma/seed.ts @@ -14,6 +14,7 @@ async function main() { username: 'admin', password: hashedPassword, enabled: true, + isAdmin: true, }, }); } diff --git a/src/server/routes/auth.ts b/src/server/routes/auth.ts index 5cb2c79..909f10a 100644 --- a/src/server/routes/auth.ts +++ b/src/server/routes/auth.ts @@ -1,6 +1,7 @@ import express, { Router } from 'express'; import passport from 'passport'; +import { authenticateLocal } from '../middleware'; import { authService } from '../services'; import { getSuccessResponse } from '../utils'; @@ -10,7 +11,7 @@ authRouter.post('/login', passport.authenticate('local'), async (req, res) => { const { username } = req.body; const { statusCode, data } = await authService.login({ username }); - res.status(statusCode).json(getSuccessResponse({ statusCode, data })); + return res.status(statusCode).json(getSuccessResponse({ statusCode, data })); }); authRouter.post('/register', async (req, res) => { @@ -23,4 +24,12 @@ authRouter.post('/register', async (req, res) => { return res.status(statusCode).json(getSuccessResponse({ statusCode, data })); }); +authRouter.post('/logout', authenticateLocal, async (req, res) => { + req.session.destroy(() => { + res.redirect('/login'); + }); + + return res.sendStatus(204); +}); + export default authRouter; diff --git a/src/server/routes/users.ts b/src/server/routes/users.ts index 1aa3c2c..1238d45 100644 --- a/src/server/routes/users.ts +++ b/src/server/routes/users.ts @@ -1,10 +1,11 @@ import express, { Router } from 'express'; import orm from '../lib/prisma'; +import { authenticateLocal } from '../middleware'; const usersRouter: Router = express.Router(); -usersRouter.get('/', async (_req, res) => { +usersRouter.get('/', authenticateLocal, async (_req, res) => { const users = await orm.user.findMany(); return res.status(200).json(users); diff --git a/src/server/server.ts b/src/server/server.ts index 301cf4e..ae92027 100644 --- a/src/server/server.ts +++ b/src/server/server.ts @@ -1,5 +1,6 @@ import path from 'path'; +import { PrismaClient } from '@prisma/client'; import { PrismaSessionStore } from '@quixo3/prisma-session-store'; import cookieParser from 'cookie-parser'; import cors from 'cors'; @@ -9,32 +10,18 @@ import passport from 'passport'; import 'express-async-errors'; -import orm from './lib/prisma'; import { errorHandler } from './middleware'; import routes from './routes'; +require('./lib/passport'); + const PORT = 9321; const app = express(); - +app.set('trust proxy', 1); const staticPath = path.join(__dirname, '../sonixd-client/'); app.use(express.static(staticPath)); -app.use( - session({ - secret: 'secret', - resave: false, - saveUninitialized: false, - store: new PrismaSessionStore(orm, { - checkPeriod: 2 * 60 * 1000, - dbRecordIdIsSessionId: true, - dbRecordIdFunction: undefined, - }), - cookie: { - maxAge: 7 * 24 * 60 * 60 * 1000, - }, - }) -); app.use( cors({ origin: [`http://localhost:4343`, `${process.env.APP_BASE_URL}`], @@ -44,10 +31,27 @@ app.use( ); app.use(express.json()); app.use(express.urlencoded({ extended: false })); -app.use(cookieParser('secret')); +app.use(cookieParser()); +app.use( + session({ + secret: process.env.DB_SECRET || 'secret', + resave: true, + saveUninitialized: false, + rolling: true, + name: 'user_session', + cookie: { + maxAge: 30 * 24 * 60 * 60 * 1000, // 30 days + }, + store: new PrismaSessionStore(new PrismaClient(), { + checkPeriod: 10 * 60 * 1000, // 10 minutes + dbRecordIdIsSessionId: true, + dbRecordIdFunction: undefined, + }), + }) +); + app.use(passport.initialize()); app.use(passport.session()); -require('./lib/passport')(passport); app.get('/', (_req, res) => { res.sendFile(path.join(staticPath, 'index.html')); diff --git a/src/server/utils/api-error.ts b/src/server/utils/api-error.ts index b68f018..9284a46 100644 --- a/src/server/utils/api-error.ts +++ b/src/server/utils/api-error.ts @@ -12,10 +12,26 @@ class ApiError extends Error { return new ApiError({ statusCode: 400, message }); } + static unauthorized(message: string) { + return new ApiError({ statusCode: 401, message }); + } + + static forbidden(message: string) { + return new ApiError({ statusCode: 403, message }); + } + + static notFound(message: string) { + return new ApiError({ statusCode: 404, message }); + } + static conflict(message: string) { return new ApiError({ statusCode: 409, message }); } + static gone(message: string) { + return new ApiError({ statusCode: 410, message }); + } + static internal(message: string) { return new ApiError({ statusCode: 500, message }); } diff --git a/src/types.ts b/src/types.ts index 63a54d5..768990c 100644 --- a/src/types.ts +++ b/src/types.ts @@ -220,3 +220,13 @@ export interface Pagination { serverSide?: boolean; recordsPerPage: number; } + +export type UserResponse = { + id: number; + username: string; + password?: string; + createdAt: string; + updatedAt: string; + enabled: boolean; + isAdmin: boolean; +};