Fix session auth

Add custom session secret
Set session to rolling
30 day expiry
10 minute check
Custom cookie name
This commit is contained in:
jeffvli
2022-05-03 15:31:21 -07:00
parent 0d7b593078
commit e8fe86ea4e
11 changed files with 123 additions and 73 deletions

View File

@@ -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,
},

View File

@@ -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;

View File

@@ -1 +1,2 @@
export { default as errorHandler } from './error-handler';
export { default as authenticateLocal } from './authenticateLocal';

View File

@@ -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;

View File

@@ -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 {

View File

@@ -14,6 +14,7 @@ async function main() {
username: 'admin',
password: hashedPassword,
enabled: true,
isAdmin: true,
},
});
}

View File

@@ -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;

View File

@@ -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);

View File

@@ -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'));

View File

@@ -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 });
}

View File

@@ -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;
};