Skip to content

Latest commit

 

History

History
636 lines (477 loc) · 14.7 KB

section-09.md

File metadata and controls

636 lines (477 loc) · 14.7 KB

Section 09: Authentication Strategies and Options

Table of Contents

Fundamental Authentication Strategies

  • 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

⬆ back to top

Huge Issues with Authentication Strategies

⬆ back to top

So Which Option?

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

⬆ back to top

Solving Issues with Option #2

⬆ back to top

Reminder on Cookies vs JWT's

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

⬆ back to top

Microservices Auth Requirements

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

⬆ back to top

Issues with JWT's and Server Side Rendering

⬆ back to top

Cookies and Encryption

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

⬆ back to top

Adding Session Support

cookie-session

// index.ts
app.set('trust proxy', true);
app.use(json());
app.use(
  cookieSession({
    signed: false,
    secure: true
  })
);

⬆ back to top

Note on Cookie-Session - Do Not Skip

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/cookie-session@2.0.39

⬆ back to top

Generating a JWT

jsonwebtoken

// signup.ts
// Generate JWT
const userJwt = jwt.sign(
  {
    id: user.id,
    email: user.email
  },
  'asdf'
);

// Store it on session object
req.session = {
  jwt: userJwt
};

⬆ back to top

JWT Signing Keys

BASE64 Decode JWT

⬆ back to top

Securely Storing Secrets with Kubernetes

⬆ back to top

Creating and Accessing Secrets

Creating a Secret

kubectl create secret generic jwt-secret --from-literal=JWT_KEY=asdf
kubectl get secrets
kubectl describe secret jwt-secret

⬆ back to top

Accessing Env Variables in a Pod

if(!process.env.JWT_KEY) {
  throw new Error('JWT_KEY must be defined');
}

⬆ back to top

Common Response Properties

⬆ back to top

Formatting JSON Properties

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

⬆ back to top

The Signin Flow

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

⬆ back to top

Common Request Validation Middleware

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

⬆ back to top

Sign In Logic

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

⬆ back to top

Current User Handler

⬆ back to top

Returning the Current User

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

⬆ back to top

Signing Out

import express from 'express';

const router = express.Router();

router.post('/api/users/signout', (req, res) => {
  req.session = null;

  res.send({});
});

export { router as signoutRouter };

⬆ back to top

Creating a Current User Middleware

// 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();
};

⬆ back to top

Augmenting Type Definitions

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

⬆ back to top

Requiring Auth for Route Access

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

⬆ back to top