JWT Auth in Apollo Server with TypeScript

main

Since the JWT token comes in a header, we want to handle context in ApolloServer.

const server = new ApolloServer({
  // ...
  context: async ({ event: { headers } }): Promise<OperationContext> => {
    let userId = null
    try {
      userId = await userIdFromToken(
        headers["Authorization"].replace("Bearer ", "")
      )
    } catch {}

    return {
      userId,
    }
  },
})

We take Authorization from the header, remove the "Bearer" word, and pass it to the userIdFromToken function.

It takes a secret from env variables. Then we verify the token against the secret with jwt.verify.

import jwt from "jsonwebtoken"
import { assertEnvVar } from "../shared/assertEnvVar"

interface DecodedToken {
  id: string
}

export const userIdFromToken = async (token: string) => {
  const secret = assertEnvVar("SECRET")

  const decoded = jwt.verify(token, secret)

  return decoded ? (decoded as DecodedToken).id : null
}

If everything is alright, we should have an id in a decoded result.

Now, we should get the user id in the resolver as a part of the context argument.

export interface OperationContext {
  userId: string | null
  country: string | null
}

To check for userId, I run the assertUserId function. It will throw an error if the user id is missing.

import { AuthenticationError } from "apollo-server-lambda"
import { OperationContext } from "../graphql/OperationContext"

export const assertUserId = ({ userId }: OperationContext) => {
  if (!userId) {
    throw new AuthenticationError("Invalid token")
  }

  return userId
}

The API creates a token on the authorization request and sends it back to the user.

Here we have the generateAuthData function that takes the user id. It calculates token expiration time and signs the token with the secret from env variables.

import jwt from "jsonwebtoken"
import { assertEnvVar } from "../shared/assertEnvVar"

const jwtLifespanInSeconds = 15552000

const getTokenExpirationTime = (ms: number) =>
  Math.floor(Date.now() / 1000) + ms

export const generateAuthData = (id: string) => {
  const tokenExpirationTime = getTokenExpirationTime(jwtLifespanInSeconds)
  const secret = assertEnvVar("SECRET")

  return {
    token: jwt.sign({ id, exp: tokenExpirationTime }, secret),
    tokenExpirationTime,
  } as const
}