Last active
January 5, 2022 16:36
-
-
Save qpwo/4b2ae3aaaf222d099b34898152118c43 to your computer and use it in GitHub Desktop.
username & password authentication in node.js + express (WITHOUT passport) in typescript in a single standalone file (minimal working example)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// January 2022, Luke Harold Miles, public domain obviously | |
// Adapted from: https://github1s.com/expressjs/express/blob/master/examples/auth/index.js | |
// You can run with `ts-node express-typescript-auth.ts` | |
// Source for this file to get updates: https://gist.github.com/qpwo/4b2ae3aaaf222d099b34898152118c43 | |
import { pbkdf2Sync, randomBytes, timingSafeEqual } from 'crypto' | |
import type { NextFunction, Request, Response } from 'express' | |
import express from 'express' | |
import type { Session } from 'express-session' | |
import session from 'express-session' | |
// TYPES | |
interface UserSession extends Session { | |
msg?: string | |
user?: User | |
} | |
type MyReq = Request & { session: UserSession } | |
interface User { | |
name: string | |
salt: string | |
hash: string | |
} | |
// CONSTANTS | |
const topBar = `<div> | |
<a href="/">home</a> | |
<a href="/restricted">/restricted</a> | |
<a href="/login">/login</a> | |
<a href="/signup">/signup</a> | |
<a href="/logout">/logout</a> | |
</div>` | |
const userPassInput = ` | |
<p> <label>Username:</label> | |
<input type="text" name="username"> </p> | |
<p> <label>Password:</label> | |
<input type="text" name="password"> </p> | |
` | |
// dummy database | |
const usersDb: Record<string, User> = {} | |
const app = (module.exports = express()) | |
// MIDDLEWARE & ROUTES | |
app.use(express.urlencoded({ extended: false })) | |
app.use( | |
session({ | |
resave: false, // don't save session if unmodified | |
saveUninitialized: false, // don't create session until something stored | |
secret: 'yqb93847ybf137bfryslalskdjnfamnduhfe', | |
}) | |
) | |
// Session-persisted message middleware | |
app.use(function (req: MyReq, res, next) { | |
const msg = req.session.msg | |
delete req.session.msg | |
res.locals.msg = `<p>Message: ${msg ?? 'no message'}</p>` | |
next() | |
}) | |
app.get('/', rootRoute) | |
app.get('/restricted', restrict, restrictedRoute) | |
app.get('/logout', logoutRoute) | |
app.get('/login', loginGetRoute) | |
app.post('/login', loginPostRoute) | |
app.get('/signup', signupGetRoute) | |
app.post('/signup', signupPostRoute) | |
// START APP | |
if (require.main === module) { | |
addUser('user', 'pass') | |
app.listen(3000) | |
console.log('Express started on port 3000') | |
} | |
// LOGIC | |
function dohash(password: string, salt: string): Buffer { | |
return pbkdf2Sync(password, salt, 310000, 32, 'sha256') | |
} | |
function makeSalt(): string { | |
return randomBytes(128).toString('base64') | |
} | |
function addUser(name: string, pass: string): User { | |
const salt = makeSalt() | |
const hash = dohash(pass, salt).toString('base64') | |
const user: User = { name, salt, hash } | |
usersDb[name] = user | |
return user | |
} | |
function authenticate( | |
name: string, | |
pass: string | |
): { user?: User; failMessage?: null } | { failMessage: string; user?: null } { | |
const user = usersDb[name] | |
if (user == null) return { failMessage: 'username not found' } | |
const hashedPassword = dohash(pass, user.salt) | |
if (!timingSafeEqual(Buffer.from(user.hash, 'base64'), hashedPassword)) { | |
return { failMessage: 'incorrect username or password' } | |
} | |
return { user } | |
} | |
// ROUTE FUNCTIONS | |
function rootRoute(req: MyReq, res: Response) { | |
res.send(`<h1>Home</h1> ${topBar} ${res.locals.msg}`) | |
} | |
function restrictedRoute(req: MyReq, res: Response) { | |
res.send( | |
`<h1>Restricted</h1> ${topBar} ${res.locals.msg} Wahoo! restricted area.` | |
) | |
} | |
function logoutRoute(req: MyReq, res: Response) { | |
// destroy the user's session to log them out -- will be re-created next request | |
req.session.destroy(() => { | |
res.redirect('/') | |
}) | |
} | |
function loginGetRoute(req: MyReq, res: Response) { | |
res.send(makeHtmlForm(res, 'login')) | |
} | |
function loginPostRoute(req: MyReq, res: Response) { | |
const result = authenticate(req.body.username, req.body.password) | |
if (result?.user != null) { | |
// success | |
// Regenerate session when signing in to prevent fixation | |
req.session.regenerate(() => { | |
req.session.user = result.user | |
req.session.msg = `Authenticated as ${result.user?.name}` | |
res.redirect('/restricted') | |
}) | |
return | |
} | |
// failure | |
req.session.msg = `Authentication failed: ${result.failMessage}` | |
res.redirect('/') | |
} | |
function signupGetRoute(req: MyReq, res: Response) { | |
res.send(makeHtmlForm(res, 'signup')) | |
} | |
function signupPostRoute(req: MyReq, res: Response) { | |
const user = addUser(req.body.username, req.body.password) | |
req.session.regenerate(() => { | |
req.session.user = user | |
req.session.msg = `Added user ${user.name}` | |
res.redirect('/restricted') | |
}) | |
} | |
// route function helpers | |
function restrict(req: MyReq, res: Response, next: NextFunction) { | |
if (req.session.user) { | |
next() | |
return | |
} | |
req.session.msg = 'Access denied!' | |
res.redirect('/login') | |
} | |
function makeHtmlForm(res: Response, route: 'signup' | 'login'): string { | |
return `<h1>${route}</h1> | |
${topBar} | |
${res.locals.msg} | |
<form method="post" action="/${route}"> | |
${userPassInput} | |
<p> <input type="submit" value="${route}"> </p> | |
</form> ` | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment