User authentication

Now that we can get the links saved, let's add a layer of security by using jsonwebtoken.

There are 2 important things you should always do:

  • Hash password before storing them in the database. If you leak them, you won't expose the actual passwords.

Source: http://www.commitstrip.com/en/2018/03/22/basic-functionality
  • Only store enough information in the JWT to identify the user. Anyone can read its content.

Try it yourself, take this token and paste it in jwt.io.

eyJzZWNyZXQiOiIxMjMiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.OZ-CLQhIwvruoziWza1XEB4PwCNfMnlcljRt14_Zckw
Anyone can read the content of a token

Install the required dependencies:

$ yarn add jsonwebtoken bcryptjs @types/jsonwebtoken @types/bcryptjs

We first going to create a GraphQL input to structure the data we receive:

resolvers/UserAuthInput.ts
import { InputType, Field } from "type-graphql";

@InputType()
class UserAuthInput {
  @Field()
  emailAddress: string;

  @Field()
  password: string;
}

export { UserAuthInput };

Update the User entity to return the jwt with:

entities/User.ts
class User extends Model<User> {
  // ...
  @Field({ nullable: true })
  jwt?: string;
}

Add your jwtSecret to the shared config:

export interface SharedConfig {
  jwtSecret: string;
  // ...
}

export const sharedConfig: SharedConfig = {
  // ...
  jwtSecret: process.env.JWT_SECRET || "",
  // ...
};

And finally create the resolver:

import { Resolver, Query, Mutation, Arg } from "type-graphql";
import * as bcrypt from "bcryptjs";
import { User } from "../../../../entities";
import { getUserEntity } from "../../../../containers";
import { UserAuthInput } from "./UserAuthInput";
import * as jwt from "jsonwebtoken";
import { config } from "../../../../config";

@Resolver(of => User)
class UserResolver {
  private readonly _UserEntity = getUserEntity();
  @Mutation(returns => User)
  async signIn(@Arg("input")
  {
    emailAddress,
    password
  }: UserAuthInput): Promise<User> {
    const user = await this._UserEntity.findOne({ where: { emailAddress } });
    if (!user) {
      throw new Error("User not found");
    }
    if (!bcrypt.compare(password, user.password)) {
      throw new Error("Invalid password");
    }
    user.jwt = jwt.sign({ id: user.id }, config.jwtSecret);
    return user;
  }
  @Mutation(returns => User)
  async signUp(@Arg("input")
  {
    emailAddress,
    password
  }: UserAuthInput): Promise<User> {
    if (await this._UserEntity.findOne({ where: { emailAddress } })) {
      throw new Error("User with this email address already exists.");
    }
    const salt = bcrypt.genSaltSync(10);
    try {
      const user = await this._UserEntity.create({
        emailAddress,
        password: bcrypt.hashSync(password, salt)
      });
      user.jwt = jwt.sign({ id: user.id }, config.jwtSecret);
      return user;
    } catch (error) {
      console.error("Cannot create user.", error);
      throw error;
    }
  }
}

export { UserResolver };

Writing tests

Update setupTests.ts to require the environment variables:

src/setupTests.ts
require('dotenv').config()

When building the schema, GraphQL will complain if we don't have any queries. For this reason, let's create a Health resolver and add it to our schema when testing.

resolvers/Health.ts
import { Int, ObjectType, Query, Resolver, Field } from "type-graphql";

@ObjectType()
class Health {
  @Field(() => Int)
  ok: number;
}

@Resolver(of => Health)
class HealthResolver {
  @Query(returns => Health)
  async health(): Promise<Health> {
    return { ok: 1 };
  }
}

export { HealthResolver };

Finally, test the resolver:

import { ApolloServerBase, gql } from "apollo-server-core";
import {
  createTestClient
} from "apollo-server-testing";
import { buildSchema } from "type-graphql";
import { UserResolver } from "./User";
import { setUserEntity } from "../../../../containers";
import { User } from "../../../../entities";
import { HealthResolver } from "..";
import * as jwt from 'jsonwebtoken'

describe("routers/GraphQL/Resolvers/User", () => {
  describe("mutation/signIn", () => {
    it("should return a jwt for this user", async () => {
      const schema = await buildSchema({
        resolvers: [HealthResolver, UserResolver]
      });
      const server = new ApolloServerBase({ schema });
      const testClient = createTestClient(server);
      const user = { emailAddress: "[email protected]", password: "123" };
      const findOne = jest.fn();
      findOne.mockResolvedValue({ ...user, id: 'userid', });
      setUserEntity(({ findOne } as unknown) as typeof User);

      const { data, errors } = await testClient.mutate({
        mutation: gql`
          mutation signIn($input: UserAuthInput!) {
            signIn(input: $input) {
              id
              emailAddress
              jwt
            }
          }
        `,
        variables: { input: user }
      });
      expect(errors).toBeUndefined();
      expect(data!.signIn).toMatchObject({ id: "userid", emailAddress: user.emailAddress })
      expect(jwt.decode(data!.signIn.jwt)).toMatchObject({ id: 'userid' })
    });
  });
  describe("mutation/signUp", () => {
    it("should create a user and return the jwt", async () => {
      const schema = await buildSchema({
        resolvers: [HealthResolver, UserResolver]
      });
      const server = new ApolloServerBase({ schema });
      const testClient = createTestClient(server);
      const user = { emailAddress: "[email protected]", password: "123" };
      const findOne = jest.fn();
      const create = jest.fn();
      create.mockResolvedValue({ id: "userid", ...user });
      findOne.mockResolvedValue(null);
      setUserEntity(({ findOne, create } as unknown) as typeof User);

      const { data, errors } = await testClient.mutate({
        mutation: gql`
          mutation signUp($input: UserAuthInput!) {
            signUp(input: $input) {
              id
              emailAddress
              jwt
            }
          }
        `,
        variables: { input: user }
      });
      expect(errors).toBeUndefined();
      expect(data!.signUp).toMatchObject({ id: "userid", emailAddress: user.emailAddress })
      expect(jwt.decode(data!.signUp.jwt)).toMatchObject({ id: 'userid' })
    });
  });
});

Tests are running in parallel. We recreate the server for each test to avoid conflicts.

How do we know which user just sent a request? GraphQL uses context to pass down information to all resolvers and we will use the Authorization header to send the JWT which each request.

Populating the GraphQL context

routers/GraphQL/index.ts
import "reflect-metadata";
import { ApolloServer } from "apollo-server-express";

import { Router } from "express";

import { buildSchema } from "type-graphql";
import * as resolvers from "./resolvers";

import * as jwt from "jsonwebtoken";
import { config } from "../../config";
import { User } from "../../entities";

export interface GraphQLContext {
  user?: User;
}

export class GraphQLRouter {
  public readonly router = Router();
  constructor() {
    this.buildSchema();
  }
  async buildSchema() {
    const schema = await buildSchema({
      resolvers: Object.values(resolvers),
      validate: false
    });
    const apolloServer = new ApolloServer({
      schema,
      context: ({ req }): GraphQLContext => {
        if (!req.headers.authorization) {
          return {};
        }
        if (req.headers.authorization.slice(0, 7).trim() !== "Bearer") {
          return {};
        }
        try {
          return {
            user: jwt.verify(
              req.headers.authorization.slice(7),
              config.jwtSecret
            ) as User
          };
        } catch (error) {
          return {};
        }
      }
    });
    apolloServer.applyMiddleware({ app: this.router, path: "/" });
  }
}

Testing

We can mock the context when creating the test server:

const server = new ApolloServerBase({
  schema,
  context: { user: { id: "userid" } }
});

Getting the context from resolvers

Add the argument @Ctx() context to a resolver:

@Mutation(returns => Link)
  async addLink(
    @Arg("input") input: LinkInput,
    @Ctx() context: GraphQLContext
  ): Promise<Link> {
    if (!context.user) {
      throw new Error("Unauthorised");
    }
    const link = await this._LinkEntity.create({
      ...input,
      userId: context.user.id
    });
    return link;
  }

Adding headers in the playground

Use the tab HTTP HEADERS at the bottom left.

Conclusion

The backend is now ready to interact with the frontend. We can interact safely with the database and we have enough to implement anything we might need.

A lot is going on in this chapter. Have look at this Pull Request to see all the changes with comments.

user-auth branch available on GitHub.

Last updated

Was this helpful?