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.