Chapter 4: Authentication

Full Stack TypeScript

link Authentication

By the end of this lesson, developers will be able to:

  • Create token-based user authentication
  • Create sign up, login and current user resolvers

Introduction

In this section, we will build out our User entity and add authentication. The purpose of authentication or "auth" is to provide methods to login and sign up.

link User Entity

We have yet to edit our entity/User.ts. Let's add the following snippet to make our User entity:

import { Entity, PrimaryGeneratedColumn, Column, BaseEntity } from 'typeorm';
import { Field, ObjectType } from 'type-graphql';

@ObjectType()
@Entity()
export class User extends BaseEntity {
  @Field()
  @PrimaryGeneratedColumn()
  id: number;

  @Field()
  @Column('text', { unique: true })
  email: string;

  @Field()
  @Column('text', { unique: true })
  username: string;

  @Column()
  password: string;
}

link AuthInput

Next we will declare our new AuthInput type. Let's create a new file, called graphql-types/AuthInput.ts, and insert the following:

import { InputType, Field } from 'type-graphql';

@InputType()
export class AuthInput {
  @Field({ nullable: true })
  email?: string;

  @Field({ nullable: true })
  username?: string;

  @Field()
  password: string;
}

link ErrorField

We will also include a custom ErrorField type for error handling. Create a new file called graphql-types/AuthInput.ts, and insert the following:

import { Field, ObjectType } from "type-graphql";

@ObjectType()
export class FieldError {
  @Field()
  path: string;

  @Field()
  message: string;
}

link UserResponse

Finally, we create a custom UserResponse type. Create a new file called graphql-types/UserResponse.ts, and insert the following:

import { ObjectType, Field } from "type-graphql";
import { FieldError } from "./FieldError";
import { User } from "../entity/User";

@ObjectType()
export class UserResponse {
  @Field(() => User, { nullable: true })
  user?: User;

  @Field(() => [FieldError], { nullable: true })
  errors?: FieldError[];
}

link AuthResolver

Before creating our new resolver, we need to install bcryptjs:

yarn add bcrypt jsonwebtoken
yarn add -D @types/bcrypt @types/jsonwebtoken

link Adding Context

As we begin to create the auth resolver, we need to include a method to check the current user's token. Let's implement this inside a new file: src/utils.ts.

import jwt from 'jsonwebtoken';
import { Request } from 'express';
const APP_SECRET = process.env.SESSION_SECRET || 'aslkdfjoiq12312';

function getUserId(context: { req: Request }) {
  const authorization = context.req.headers['authorization'];
  try {
    if (authorization) {
      const token = authorization.replace('Bearer ', '');
      const user = jwt.verify(token, APP_SECRET) as any;
      return user.id;
    }
  } catch (err) {
    throw new Error(err.message);
  }
}

export { getUserId, APP_SECRET };

Now we can declare our new resolvers/AuthResolver.ts. Lets begin by adding a few top level imports:

import bcrypt from "bcryptjs";
import { Arg, Ctx, Mutation, Resolver, Query } from "type-graphql";
import { User } from "../entity/User";
import { AuthInput } from "../graphql-types/AuthInput";
import { UserResponse } from "../graphql-types/UserResponse";
import { Request, Response } from 'express';
import { getUserId } from '../utils';

const invalidLoginResponse = {
  errors: [
    {
      path: "email",
      message: "invalid login"
    }
  ]
};

Next we will define two new resolver functions: register and login:

@Resolver()
export class AuthResolver {
  @Mutation(() => UserResponse)
  async register(
    @Arg('input')
    { email, username, password }: AuthInput
  ): Promise<UserResponse> {
    const hashedPassword = await bcrypt.hash(password, 12);

    if (email) {
      const existingUser = await User.findOne({ email });
      if (existingUser) {
        return {
          errors: [
            {
              path: 'email',
              message: 'already in use'
            }
          ]
        };
      }
    }
    if (username) {
      const existingUser = await User.findOne({ username });
      if (existingUser) {
        return {
          errors: [
            {
              path: 'username',
              message: 'already in use'
            }
          ]
        };
      }
    }

    const user = await User.create({
      email,
      username,
      password: hashedPassword
    }).save();

    const payload = {
      id: user.id,
      username: user.username,
      email: user.email
    };

    const token = jwt.sign(
      payload,
      process.env.SESSION_SECRET || 'aslkdfjoiq12312'
    );

    return { user, token };
  }

  @Mutation(() => UserResponse)
  async login(
    @Arg('input') { username, email, password }: AuthInput,
    @Ctx() ctx: { req: Request; res: Response }
  ): Promise<UserResponse> {
    if (username || email) {
      const user = username
        ? await User.findOne({ where: { username } })
        : await User.findOne({ where: { email } });

      if (!user) {
        return invalidLoginResponse;
      }

      const valid = await bcrypt.compare(password, user.password);

      if (!valid) {
        return invalidLoginResponse;
      }

      const payload = {
        id: user.id,
        username: user.username,
        email: user.email
      };

      ctx.req.session!.userId = user.id;

      const token = jwt.sign(
        payload,
        process.env.SESSION_SECRET || 'aslkdfjoiq12312'
      );

      return { user, token };
    }
    return invalidLoginResponse;
  }

  @Query(() => User)
  async currentUser(
    @Ctx() ctx: { req: Request; res: Response }
  ): Promise<User | undefined> {
    const userId = getUserId(ctx);
    if (userId) {
      const user = await User.findOne(userId);
      return user;
    }
    throw new Error('User not found');
  }
}

link Auth Resolver

With these changes complete, we can import our new AuthResolver inside src/index.ts:

import { AuthResolver } from './resolvers/AuthResolver';

{/* ... */}
const schema = await buildSchema({
    // add all typescript resolvers
    // __dirname + '/resolvers/*.ts'
    resolvers: [PlaceResolver, AuthResolver],
    validate: true,
    // automatically create `schema.gql` file with schema definition in current folder
    emitSchemaFile: path.resolve(__dirname, 'schema.gql')
});

link Test Server

Let's make sure our server works:

yarn start

Open http://localhost:4000/graphql, you should see a playground for your GraphQL endpoints.

Register Mutation

Insert the following mutation to register your new user:

mutation Register($username: String!, $email: String!, $password: String!) {
  register(input: {username: $username, email: $email, password: $password}) {
    user {
      id
      username
      email
    }
    token
  }
}

At the bottom left corner, add the following query parameters:

{
  "username": "alice",
  "email": "alice@gmail.com",
  "password": "password"
}

Press the "play" button to test your mutation, and see a new user.

Viola! The new user appears with their associated auth token.

Login Mutation

Insert the following mutation to login your new user:

mutation Login($username: String, $email: String, $password: String!) {
  login(input: {username: $username, email: $email, password: $password}) {
    user {
      id
      username
      email
    }
    token
  }
}

At the bottom left corner, add the following query parameters:

{
  "username": "alice",
  "password": "password"
}

Press the "play" button to test your mutation, and see a new user.

Viola! The new user appears with their associated auth token.

WELL DONE!

Wrapping Up

Today we built token-based authentication for our TypeGraphQL server. Up next, we will establish our first associations between the User and Place models.

format_list_bulleted
help_outline