diff --git a/src/server.spec.ts b/src/server.spec.ts index fd908e7..1d2cfa5 100644 --- a/src/server.spec.ts +++ b/src/server.spec.ts @@ -38,11 +38,11 @@ describe("server should", () => { }) const meResponse = await client.rawRequest(gqlToString(meQuery)) - const response = await fetch(refreshTokenUri, { + const refreshTokenResponse = await fetch(refreshTokenUri, { method: "POST", headers: { cookie: cookieHeader }, }) - const jsonResponse = await response.json() + const jsonResponse = await refreshTokenResponse.json() expect(cookieHeader).toMatch(/HttpOnly/) expect(parsedCookie.Path).toBe("/refresh_token") diff --git a/src/server.ts b/src/server.ts index 191249a..9e3e3d7 100644 --- a/src/server.ts +++ b/src/server.ts @@ -2,8 +2,8 @@ import express = require("express") import { ApolloServer } from "apollo-server-express" import { createSchema } from "./server/schema" import { + accessTokenWithRefreshCookie, contextFunction, - refreshTokens, verifiedRefreshTokenPayload, } from "./server/userResolver/auth" import cookie = require("cookie") @@ -21,8 +21,8 @@ export const createServer = async (port: number) => { app.post("/refresh_token", (req, res) => { try { const parsedCookie = cookie.parse(req.headers.cookie!) - const refreshTokenPayload = verifiedRefreshTokenPayload(parsedCookie.rt) - const accessToken = refreshTokens(refreshTokenPayload.userId, res) + const refreshPayload = verifiedRefreshTokenPayload(parsedCookie.rt) + const accessToken = accessTokenWithRefreshCookie(refreshPayload.userId, res) res.json({ data: accessToken }) } catch (error) { res.json({ data: null, errors: "Refresh failed: " + error }) diff --git a/src/server/UserResolver.spec.ts b/src/server/UserResolver.spec.ts index ae3321a..c7ef5dd 100644 --- a/src/server/UserResolver.spec.ts +++ b/src/server/UserResolver.spec.ts @@ -11,31 +11,11 @@ import { AccessToken } from "./userResolver/AccessToken" import { Context, signAccessToken, verifiedAccessTokenPayload } from "./userResolver/auth" import { User } from "./userResolver/User" -beforeAll(async () => { - initializeRollbackTransactions() - await createConnection(testingConnectionOptions()) -}) - -afterAll(async () => { - await getConnection().close() -}) - describe("resolver of user", () => { describe("createUser mutation should", () => { it( "return email as it creates user with mutation", runInRollbackTransaction(async () => { - const createUserMutation = gql` - mutation { - createUser( - email: "user-mutation@user-resolver.com" - password: "password" - ) { - email - } - } - ` - const response = await callSchema(createUserMutation) expect(response.errors).toBeUndefined() @@ -50,14 +30,6 @@ describe("resolver of user", () => { it( "return emails of registered users", runInRollbackTransaction(async () => { - const usersQuery = gql` - query { - users { - email - } - } - ` - await User.create({ email: "users-query@user-resolver.com", }).save() @@ -73,18 +45,6 @@ describe("resolver of user", () => { }) describe("accessToken query should", () => { - const accessTokenQuery = gql` - query { - accessToken( - email: "access-token@user-resolver.com" - password: "password" - ) { - jwt - jwtExpiry - } - } - ` - it( "return error for bad password or not-existent user", runInRollbackTransaction(async () => { @@ -126,14 +86,6 @@ describe("resolver of user", () => { }) describe("me query should", () => { - const meQuery = gql` - query { - me { - email - } - } - ` - it( "return an error without a valid access token", runInRollbackTransaction(async () => { @@ -168,6 +120,45 @@ describe("resolver of user", () => { }) }) +beforeAll(async () => { + initializeRollbackTransactions() + await createConnection(testingConnectionOptions()) +}) + +afterAll(async () => { + await getConnection().close() +}) + +const createUserMutation = gql` + mutation { + createUser(email: "user-mutation@user-resolver.com", password: "password") { + email + } + } +` +const usersQuery = gql` + query { + users { + email + } + } +` +const accessTokenQuery = gql` + query { + accessToken(email: "access-token@user-resolver.com", password: "password") { + jwt + jwtExpiry + } + } +` +const meQuery = gql` + query { + me { + email + } + } +` + const contextWithAuthHeader = (header: string): Context => ({ req: { headers: { diff --git a/src/server/UserResolver.ts b/src/server/UserResolver.ts index c07d86b..fc8d42f 100644 --- a/src/server/UserResolver.ts +++ b/src/server/UserResolver.ts @@ -1,7 +1,7 @@ import "reflect-metadata" import { Arg, Authorized, Ctx, Mutation, Query } from "type-graphql" import { AccessToken } from "./userResolver/AccessToken" -import { comparePassword, Context, refreshTokens } from "./userResolver/auth" +import { comparePasswords, Context, accessTokenWithRefreshCookie } from "./userResolver/auth" import { User } from "./userResolver/User" export class UserResolver { @@ -18,12 +18,9 @@ export class UserResolver { ) { try { const user = await User.findOne({ where: { email } }) + await comparePasswords(user!.password, password) - if (!(await comparePassword(user!.password, password))) { - throw new Error() - } - - return refreshTokens(user!.id, res) + return accessTokenWithRefreshCookie(user!.id, res) } catch (error) { throw new Error("Login credentials are invalid: " + error) } diff --git a/src/server/userResolver/auth.ts b/src/server/userResolver/auth.ts index 2255966..d2e491d 100644 --- a/src/server/userResolver/auth.ts +++ b/src/server/userResolver/auth.ts @@ -1,35 +1,24 @@ -import { argon2id, hash, verify as argonVerify } from "argon2" +import { argon2id, hash as argonHash, verify as argonVerify } from "argon2" import { Request, Response } from "express" -import { sign, verify as jwtVerify } from "jsonwebtoken" +import { sign as jwtSign, verify as jwtVerify } from "jsonwebtoken" import { AuthChecker } from "type-graphql" import { AccessToken } from "./AccessToken" -export type Context = { - req: Request - res: Response - payload?: ContextPayload -} +export const hashPassword = async (password: string) => + await argonHash(password, { type: argon2id }) -type ContextPayload = { - userId: number -} +export const comparePasswords = async (hash: string, plain: string) => { + if (!(await argonVerify(hash, plain, { type: argon2id }))) { + throw new Error("Passwords do not match") + } -type JWTPayload = { - userId: number - iat: number - exp?: number + return true } -export const hashPassword = async (password: string) => - await hash(password, { type: argon2id }) - -export const comparePassword = async (hash: string, plain: string) => - await argonVerify(hash, plain, { type: argon2id }) - export const signAccessToken = (payload: ContextPayload) => { const accessTokenSecret = process.env.ACCESS_SECRET as string - return sign(payload, accessTokenSecret, { + return jwtSign(payload, accessTokenSecret, { expiresIn: parseInt(process.env.ACCESS_EXPIRY as string), }) } @@ -37,7 +26,7 @@ export const signAccessToken = (payload: ContextPayload) => { export const signRefreshToken = (payload: ContextPayload) => { const accessTokenSecret = process.env.REFRESH_SECRET as string - return sign(payload, accessTokenSecret, { + return jwtSign(payload, accessTokenSecret, { expiresIn: parseInt(process.env.REFRESH_EXPIRY as string), }) } @@ -54,7 +43,7 @@ export const verifiedRefreshTokenPayload = (token: string) => { return jwtVerify(token, refreshTokenSecret) as JWTPayload } -export const refreshTokens = (userId: number, res: Response) => { +export const accessTokenWithRefreshCookie = (userId: number, res: Response) => { const accessToken = new AccessToken() accessToken.jwt = signAccessToken({ userId }) accessToken.jwtExpiry = parseInt(process.env.ACCESS_EXPIRY as string) @@ -83,3 +72,19 @@ export const customAuthChecker: AuthChecker = ({ context }) => { } export const contextFunction = ({ req, res }: Context) => ({ req, res }) + +export type Context = { + req: Request + res: Response + payload?: ContextPayload +} + +type ContextPayload = { + userId: number +} + +type JWTPayload = { + userId: number + iat: number + exp?: number +}