💻
Building and hosting a WebApp
  • Getting started
  • Project setup
    • Requirements
    • Files organisation
    • Lerna
    • Linter
    • Prettier
    • GitHook
    • Testing
    • Conclusion
  • Backend
    • Files organisation
    • Environment config
    • Express API
    • Security
    • Database
    • GraphQL
    • User authentication
    • Conclusion
  • Frontend
    • Create React App
    • Files organisation
    • Styles
    • Apollo Hooks
    • Form management
    • User authentication
    • Writing tests
    • Types generation
    • Conclusion
  • DevOps
    • CI/CD
    • AWS
      • Managing secrets
      • Pricing
      • RDS
      • S3
      • Route53
      • CloudFront
      • Serverless
      • Security
      • CloudFormation
    • Conclusion
  • 🚧Stripe payment
  • 🚧File upload
Powered by GitBook
On this page
  • Writing tests
  • Adding security to the links
  • Populating the GraphQL context
  • Testing
  • Getting the context from resolvers
  • Adding headers in the playground
  • Conclusion

Was this helpful?

  1. Backend

User authentication

PreviousGraphQLNextConclusion

Last updated 5 years ago

Was this helpful?

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

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.

  • Only store enough information in the JWT to identify the user. Anyone can read its content.

eyJzZWNyZXQiOiIxMjMiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.OZ-CLQhIwvruoziWza1XEB4PwCNfMnlcljRt14_Zckw

Never store password in plain text.

Never put sensitive data in a JWT.

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: "test1@mock.com", 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: "test2@mock.com", 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.

Adding security to the links

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.

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

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

branch available on GitHub.

jwt.io
Pull Request
user-auth
jsonwebtoken
Source: http://www.commitstrip.com/en/2018/03/22/basic-functionality
Anyone can read the content of a token