mirror of
https://github.com/aaronpo97/the-biergarten-app.git
synced 2026-02-16 10:42:08 +00:00
Implement authentication using Passport.js
This commit is contained in:
37
config/auth/cookie.ts
Normal file
37
config/auth/cookie.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { NextApiResponse } from 'next';
|
||||
import { serialize, parse } from 'cookie';
|
||||
import { SessionRequest } from './types';
|
||||
|
||||
const TOKEN_NAME = 'token';
|
||||
export const MAX_AGE = 60 * 60 * 8; // 8 hours
|
||||
|
||||
export function setTokenCookie(res: NextApiResponse, token: string) {
|
||||
const cookie = serialize(TOKEN_NAME, token, {
|
||||
maxAge: MAX_AGE,
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
path: '/',
|
||||
sameSite: 'lax',
|
||||
});
|
||||
|
||||
res.setHeader('Set-Cookie', cookie);
|
||||
}
|
||||
|
||||
export function removeTokenCookie(res: NextApiResponse) {
|
||||
const cookie = serialize(TOKEN_NAME, '', { maxAge: -1, path: '/' });
|
||||
res.setHeader('Set-Cookie', cookie);
|
||||
}
|
||||
|
||||
export function parseCookies(req: SessionRequest) {
|
||||
// For API Routes we don't need to parse the cookies.
|
||||
if (req.cookies) return req.cookies;
|
||||
|
||||
// For pages we do need to parse the cookies.
|
||||
const cookie = req.headers?.cookie;
|
||||
return parse(cookie || '');
|
||||
}
|
||||
|
||||
export function getTokenCookie(req: SessionRequest) {
|
||||
const cookies = parseCookies(req);
|
||||
return cookies[TOKEN_NAME];
|
||||
}
|
||||
24
config/auth/localStrat.ts
Normal file
24
config/auth/localStrat.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import findUserByUsername from '@/services/user/findUserByUsername';
|
||||
import Local from 'passport-local';
|
||||
import ServerError from '../util/ServerError';
|
||||
import { validatePassword } from './passwordFns';
|
||||
|
||||
const localStrat = new Local.Strategy(async (username, password, done) => {
|
||||
try {
|
||||
const user = await findUserByUsername(username);
|
||||
if (!user) {
|
||||
throw new ServerError('Username or password is incorrect.', 401);
|
||||
}
|
||||
|
||||
const isValidLogin = await validatePassword(user.hash, password);
|
||||
if (!isValidLogin) {
|
||||
throw new ServerError('Username or password is incorrect.', 401);
|
||||
}
|
||||
|
||||
done(null, { id: user.id, username: user.username });
|
||||
} catch (error) {
|
||||
done(error);
|
||||
}
|
||||
});
|
||||
|
||||
export default localStrat;
|
||||
18
config/auth/middleware/getCurrentUser.ts
Normal file
18
config/auth/middleware/getCurrentUser.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { NextApiResponse } from 'next';
|
||||
import { NextHandler } from 'next-connect';
|
||||
import { getLoginSession } from '../session';
|
||||
import { ExtendedNextApiRequest } from '../types';
|
||||
|
||||
/** Get the current user from the session. Adds the user to the request object. */
|
||||
const getCurrentUser = async (
|
||||
req: ExtendedNextApiRequest,
|
||||
res: NextApiResponse,
|
||||
next: NextHandler,
|
||||
) => {
|
||||
const session = await getLoginSession(req);
|
||||
const user = { id: session.id, username: session.username };
|
||||
req.user = user;
|
||||
next();
|
||||
};
|
||||
|
||||
export default getCurrentUser;
|
||||
6
config/auth/passwordFns.ts
Normal file
6
config/auth/passwordFns.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import argon2 from 'argon2';
|
||||
|
||||
export const hashPassword = async (password: string) => argon2.hash(password);
|
||||
|
||||
export const validatePassword = async (hash: string, password: string) =>
|
||||
argon2.verify(hash, password);
|
||||
49
config/auth/session.ts
Normal file
49
config/auth/session.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { NextApiResponse } from 'next';
|
||||
import Iron from '@hapi/iron';
|
||||
import { SessionRequest, UserInfoSchema, UserSessionSchema } from '@/config/auth/types';
|
||||
import { z } from 'zod';
|
||||
import { MAX_AGE, setTokenCookie, getTokenCookie } from './cookie';
|
||||
import ServerError from '../util/ServerError';
|
||||
|
||||
const { TOKEN_SECRET } = process.env;
|
||||
|
||||
export async function setLoginSession(
|
||||
res: NextApiResponse,
|
||||
session: z.infer<typeof UserInfoSchema>,
|
||||
) {
|
||||
if (!TOKEN_SECRET) {
|
||||
throw new ServerError('Authentication is not configured.', 500);
|
||||
}
|
||||
const createdAt = Date.now();
|
||||
const obj = { ...session, createdAt, maxAge: MAX_AGE };
|
||||
const token = await Iron.seal(obj, TOKEN_SECRET, Iron.defaults);
|
||||
|
||||
setTokenCookie(res, token);
|
||||
}
|
||||
|
||||
export async function getLoginSession(req: SessionRequest) {
|
||||
if (!TOKEN_SECRET) {
|
||||
throw new ServerError('Authentication is not configured.', 500);
|
||||
}
|
||||
|
||||
const token = getTokenCookie(req);
|
||||
if (!token) {
|
||||
throw new ServerError('You are not logged in.', 401);
|
||||
}
|
||||
|
||||
const session = await Iron.unseal(token, TOKEN_SECRET, Iron.defaults);
|
||||
|
||||
const parsed = UserSessionSchema.safeParse(session);
|
||||
|
||||
if (!parsed.success) {
|
||||
throw new ServerError('Session is invalid.', 401);
|
||||
}
|
||||
const { createdAt, maxAge } = parsed.data;
|
||||
|
||||
const expiresAt = createdAt + maxAge * 1000;
|
||||
if (Date.now() > expiresAt) {
|
||||
throw new ServerError('Session expired', 401);
|
||||
}
|
||||
|
||||
return parsed.data;
|
||||
}
|
||||
25
config/auth/types.ts
Normal file
25
config/auth/types.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { IncomingMessage } from 'http';
|
||||
import { NextApiRequest } from 'next';
|
||||
import { z } from 'zod';
|
||||
|
||||
export const UserInfoSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
username: z.string(),
|
||||
});
|
||||
|
||||
export const UserSessionSchema = UserInfoSchema.merge(
|
||||
z.object({
|
||||
createdAt: z.number(),
|
||||
maxAge: z.number(),
|
||||
}),
|
||||
);
|
||||
|
||||
export interface ExtendedNextApiRequest extends NextApiRequest {
|
||||
user?: z.infer<typeof UserInfoSchema>;
|
||||
}
|
||||
|
||||
export type SessionRequest = IncomingMessage & {
|
||||
cookies: Partial<{
|
||||
[key: string]: string;
|
||||
}>;
|
||||
};
|
||||
24
config/auth/withPageAuthRequired.ts
Normal file
24
config/auth/withPageAuthRequired.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { GetServerSideProps, GetServerSidePropsContext } from 'next';
|
||||
import { getLoginSession } from './session';
|
||||
|
||||
const withPageAuthRequired =
|
||||
(fn?: GetServerSideProps) => async (context: GetServerSidePropsContext) => {
|
||||
try {
|
||||
const { req } = context;
|
||||
await getLoginSession(req);
|
||||
|
||||
if (!fn) {
|
||||
return { props: {} };
|
||||
}
|
||||
return await fn(context);
|
||||
} catch (error) {
|
||||
return {
|
||||
redirect: {
|
||||
destination: '/login',
|
||||
permanent: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export default withPageAuthRequired;
|
||||
Reference in New Issue
Block a user