- Section 09: Authentication Strategies and Options
- Table of Contents
- Fundamental Authentication Strategies
- Huge Issues with Authentication Strategies
- So Which Option?
- Solving Issues with Option #2
- Reminder on Cookies vs JWT's
- Microservices Auth Requirements
- Issues with JWT's and Server Side Rendering
- Cookies and Encryption
- Adding Session Support
- Note on Cookie-Session - Do Not Skip
- Generating a JWT
- JWT Signing Keys
- Securely Storing Secrets with Kubernetes
- Creating and Accessing Secrets
- Accessing Env Variables in a Pod
- Common Response Properties
- Formatting JSON Properties
- The Signin Flow
- Common Request Validation Middleware
- Sign In Logic
- Current User Handler
- Returning the Current User
- Signing Out
- Creating a Current User Middleware
- Augmenting Type Definitions
- Requiring Auth for Route Access
- User auth with microservices is an unsolved problem
- There are many ways to do it, and no one way is "right"
- I am going to outline a couple solutions then propose a solution that works, but still has downsides
Fundamental Option #1
- Individual services rely on the auth service
- Changes to auth state are immediately reflected
- Auth service goes down? Entire app is broken
Fundamental Option #2
- Individual services know how to authenticate a user
- Auth service is down? Who cares!
- Some user got banned? Darn, I just gave them the keys to my car 5 minutes ago...
We are going with Option #2 to stick with the idea of independent services
Cookies | JWT's |
---|---|
Transport mechanism | Authentication/Authorization mechanism |
Moves any kind of data between browser and server | Stores any data we want |
Automatically managed by the browser | We have to manage it manually |
Requirements for Our Auth Mechanism -> JWT
- Must be able to tell us details about a user
- Must be able to handle authorization info
- Must have a built-in, tamper-resistant way to expire or - invalidate itself
- Must be easily understood between different languages
- Must not require some kind of backing data store on the server
Requirements for Our Auth Mechanism
- Must be able to tell us details about a user
- Must be able to handle authorization info
- Must have a built-in, tamper-resistant way to expire or - invalidate itself
- Must be easily understood between different languages
- Cookie handling across languages is usually an issue when we encrypt the data in the cookie
- We will not encrypt the cookie contents.
- Remember, JWT's are tamper resistant
- You can encrypt the cookie contents if this is a big deal to you
- Must not require some kind of backing data store on the server
// index.ts
app.set('trust proxy', true);
app.use(json());
app.use(
cookieSession({
signed: false,
secure: true
})
);
The latest version of the @types/cookie-session package has a bug in it! Yes, a real bug - the type defs written out incorrectly describes the session object.
To fix this, we'll use a slightly earlier version of the package until this bug gets fixed.
Run the following inside the auth project:
npm uninstall @types/cookie-session
npm install --save-exact @types/[email protected]
// signup.ts
// Generate JWT
const userJwt = jwt.sign(
{
id: user.id,
email: user.email
},
'asdf'
);
// Store it on session object
req.session = {
jwt: userJwt
};
Creating a Secret
kubectl create secret generic jwt-secret --from-literal=JWT_KEY=asdf
kubectl get secrets
kubectl describe secret jwt-secret
if(!process.env.JWT_KEY) {
throw new Error('JWT_KEY must be defined');
}
const person = { name: 'alex' };
JSON.stringify(person)
const personTwo = {
name: 'alex',
toJSON() { return 1; }
};
JSON.stringify(personTwo)
const userSchema = new mongoose.Schema({
email: {
type: String,
required: true
},
password: {
type: String,
required: true
}
}, {
toJSON: {
transform(doc, ret) {
ret.id = ret._id;
delete ret._id;
delete ret.password;
delete ret.__v;
}
}
});
import express, { Request, Response } from 'express';
import { body, validationResult } from 'express-validator';
import { RequestValidationError } from '../errors/request-validation-error';
const router = express.Router();
router.post(
'/api/users/signin',
[
body('email')
.isEmail()
.withMessage('Email must be valid'),
body('password')
.trim()
.notEmpty()
.withMessage('You must supply a password')
],
(req: Request, res: Response) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
throw new RequestValidationError(errors.array());
}
}
);
export { router as signinRouter };
// validate-request.ts
import { Request, Response, NextFunction } from 'express';
import { validationResult } from 'express-validator';
import { RequestValidationError } from '../errors/request-validation-error';
export const validateRequest = (
req: Request,
res: Response,
next: NextFunction
) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
throw new RequestValidationError(errors.array());
}
next();
};
// signin.ts
import express, { Request, Response } from 'express';
import { body } from 'express-validator';
import { validateRequest } from '../middleware/validate-request';
const router = express.Router();
router.post(
'/api/users/signin',
[
body('email')
.isEmail()
.withMessage('Email must be valid'),
body('password')
.trim()
.notEmpty()
.withMessage('You must supply a password')
],
validateRequest,
(req: Request, res: Response) => {
}
);
export { router as signinRouter };
import express, { Request, Response } from 'express';
import { body } from 'express-validator';
import jwt from 'jsonwebtoken';
import { Password } from '../services/password';
import { User } from '../models/user';
import { validateRequest } from '../middlewares/validate-request';
import { BadRequestError } from '../errors/bad-request-error';
const router = express.Router();
router.post(
'/api/users/signin',
[
body('email')
.isEmail()
.withMessage('Email must be valid'),
body('password')
.trim()
.notEmpty()
.withMessage('You must supply a password')
],
validateRequest,
async (req: Request, res: Response) => {
const { email, password } = req.body;
const existingUser = await User.findOne({ email });
if (!existingUser) {
throw new BadRequestError('Invalid credentials');
}
const passwordsMatch = await Password.compare(
existingUser.password,
password
);
if (!passwordsMatch) {
throw new BadRequestError('Invalid Credentials');
}
// Generate JWT
const userJwt = jwt.sign(
{
id: existingUser.id,
email: existingUser.email
},
process.env.JWT_KEY!
);
// Store it on session object
req.session = {
jwt: userJwt
};
res.status(200).send(existingUser);
}
);
export { router as signinRouter };
import express from 'express';
import jwt from 'jsonwebtoken';
const router = express.Router();
router.get('/api/users/currentuser', (req, res) => {
if (!req.session?.jwt) {
return res.send({ currentUser: null });
}
try {
const payload = jwt.verify(
req.session.jwt,
process.env.JWT_KEY!
);
res.send({ currentUser: payload });
} catch (err) {
res.send({ currentUser: null });
}
});
export { router as currentUserRouter };
import express from 'express';
const router = express.Router();
router.post('/api/users/signout', (req, res) => {
req.session = null;
res.send({});
});
export { router as signoutRouter };
// current-user.ts
import { Request, Response, NextFunction } from 'express';
import jwt from 'jsonwebtoken';
export const currentUser = (
req: Request,
res: Response,
next: NextFunction
) => {
if (!req.session?.jwt) {
return next();
}
try {
const payload = jwt.verify(req.session.jwt, process.env.JWT_KEY!);
req.currentUser = payload;
} catch (err) {}
next();
};
import { Request, Response, NextFunction } from 'express';
import jwt from 'jsonwebtoken';
interface UserPayload {
id: string;
email: string;
}
declare global {
namespace Express {
interface Request {
currentUser?: UserPayload;
}
}
}
export const currentUser = (
req: Request,
res: Response,
next: NextFunction
) => {
if (!req.session?.jwt) {
return next();
}
try {
const payload = jwt.verify(
req.session.jwt,
process.env.JWT_KEY!
) as UserPayload;
req.currentUser = payload;
} catch (err) {}
next();
};
import express from 'express';
import jwt from 'jsonwebtoken';
import { currentUser } from '../middlewares/current-user';
const router = express.Router();
router.get('/api/users/currentuser', currentUser, (req, res) => {
res.send({ currentUser: req.currentUser || null });
});
export { router as currentUserRouter };
import { CustomError } from './custom-error';
export class NotAuthorizedError extends CustomError {
statusCode = 401;
constructor() {
super('Not Authorized');
Object.setPrototypeOf(this, NotAuthorizedError.prototype);
}
serializeErrors() {
return [{ message: 'Not authorized' }];
}
}
import { Request, Response, NextFunction } from 'express';
import { NotAuthorizedError } from '../errors/not-authorized-error';
export const requireAuth = (
req: Request,
res: Response,
next: NextFunction
) => {
if (!req.currentUser) {
throw new NotAuthorizedError();
}
next();
};
import express from 'express';
import jwt from 'jsonwebtoken';
import { currentUser } from '../middlewares/current-user';
import { requireAuth } from '../middlewares/require-auth';
const router = express.Router();
router.get(
'/api/users/currentuser',
currentUser,
requireAuth,
(req, res) => {
res.send({ currentUser: req.currentUser || null });
});
export { router as currentUserRouter };