refactor auth related identifiers

master
Peter Babič 5 years ago
parent b2a95e3bb8
commit 1ca1d8b2c6
Signed by: peter.babic
GPG Key ID: 4BB075BC1884BA40
  1. 4
      src/server.spec.ts
  2. 6
      src/server.ts
  3. 87
      src/server/UserResolver.spec.ts
  4. 9
      src/server/UserResolver.ts
  5. 51
      src/server/userResolver/auth.ts

@ -38,11 +38,11 @@ describe("server should", () => {
}) })
const meResponse = await client.rawRequest(gqlToString(meQuery)) const meResponse = await client.rawRequest(gqlToString(meQuery))
const response = await fetch(refreshTokenUri, { const refreshTokenResponse = await fetch(refreshTokenUri, {
method: "POST", method: "POST",
headers: { cookie: cookieHeader }, headers: { cookie: cookieHeader },
}) })
const jsonResponse = await response.json() const jsonResponse = await refreshTokenResponse.json()
expect(cookieHeader).toMatch(/HttpOnly/) expect(cookieHeader).toMatch(/HttpOnly/)
expect(parsedCookie.Path).toBe("/refresh_token") expect(parsedCookie.Path).toBe("/refresh_token")

@ -2,8 +2,8 @@ import express = require("express")
import { ApolloServer } from "apollo-server-express" import { ApolloServer } from "apollo-server-express"
import { createSchema } from "./server/schema" import { createSchema } from "./server/schema"
import { import {
accessTokenWithRefreshCookie,
contextFunction, contextFunction,
refreshTokens,
verifiedRefreshTokenPayload, verifiedRefreshTokenPayload,
} from "./server/userResolver/auth" } from "./server/userResolver/auth"
import cookie = require("cookie") import cookie = require("cookie")
@ -21,8 +21,8 @@ export const createServer = async (port: number) => {
app.post("/refresh_token", (req, res) => { app.post("/refresh_token", (req, res) => {
try { try {
const parsedCookie = cookie.parse(req.headers.cookie!) const parsedCookie = cookie.parse(req.headers.cookie!)
const refreshTokenPayload = verifiedRefreshTokenPayload(parsedCookie.rt) const refreshPayload = verifiedRefreshTokenPayload(parsedCookie.rt)
const accessToken = refreshTokens(refreshTokenPayload.userId, res) const accessToken = accessTokenWithRefreshCookie(refreshPayload.userId, res)
res.json({ data: accessToken }) res.json({ data: accessToken })
} catch (error) { } catch (error) {
res.json({ data: null, errors: "Refresh failed: " + error }) res.json({ data: null, errors: "Refresh failed: " + error })

@ -11,31 +11,11 @@ import { AccessToken } from "./userResolver/AccessToken"
import { Context, signAccessToken, verifiedAccessTokenPayload } from "./userResolver/auth" import { Context, signAccessToken, verifiedAccessTokenPayload } from "./userResolver/auth"
import { User } from "./userResolver/User" import { User } from "./userResolver/User"
beforeAll(async () => {
initializeRollbackTransactions()
await createConnection(testingConnectionOptions())
})
afterAll(async () => {
await getConnection().close()
})
describe("resolver of user", () => { describe("resolver of user", () => {
describe("createUser mutation should", () => { describe("createUser mutation should", () => {
it( it(
"return email as it creates user with mutation", "return email as it creates user with mutation",
runInRollbackTransaction(async () => { runInRollbackTransaction(async () => {
const createUserMutation = gql`
mutation {
createUser(
email: "user-mutation@user-resolver.com"
password: "password"
) {
email
}
}
`
const response = await callSchema(createUserMutation) const response = await callSchema(createUserMutation)
expect(response.errors).toBeUndefined() expect(response.errors).toBeUndefined()
@ -50,14 +30,6 @@ describe("resolver of user", () => {
it( it(
"return emails of registered users", "return emails of registered users",
runInRollbackTransaction(async () => { runInRollbackTransaction(async () => {
const usersQuery = gql`
query {
users {
email
}
}
`
await User.create({ await User.create({
email: "users-query@user-resolver.com", email: "users-query@user-resolver.com",
}).save() }).save()
@ -73,18 +45,6 @@ describe("resolver of user", () => {
}) })
describe("accessToken query should", () => { describe("accessToken query should", () => {
const accessTokenQuery = gql`
query {
accessToken(
email: "access-token@user-resolver.com"
password: "password"
) {
jwt
jwtExpiry
}
}
`
it( it(
"return error for bad password or not-existent user", "return error for bad password or not-existent user",
runInRollbackTransaction(async () => { runInRollbackTransaction(async () => {
@ -126,14 +86,6 @@ describe("resolver of user", () => {
}) })
describe("me query should", () => { describe("me query should", () => {
const meQuery = gql`
query {
me {
email
}
}
`
it( it(
"return an error without a valid access token", "return an error without a valid access token",
runInRollbackTransaction(async () => { 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 => ({ const contextWithAuthHeader = (header: string): Context => ({
req: { req: {
headers: { headers: {

@ -1,7 +1,7 @@
import "reflect-metadata" import "reflect-metadata"
import { Arg, Authorized, Ctx, Mutation, Query } from "type-graphql" import { Arg, Authorized, Ctx, Mutation, Query } from "type-graphql"
import { AccessToken } from "./userResolver/AccessToken" import { AccessToken } from "./userResolver/AccessToken"
import { comparePassword, Context, refreshTokens } from "./userResolver/auth" import { comparePasswords, Context, accessTokenWithRefreshCookie } from "./userResolver/auth"
import { User } from "./userResolver/User" import { User } from "./userResolver/User"
export class UserResolver { export class UserResolver {
@ -18,12 +18,9 @@ export class UserResolver {
) { ) {
try { try {
const user = await User.findOne({ where: { email } }) const user = await User.findOne({ where: { email } })
await comparePasswords(user!.password, password)
if (!(await comparePassword(user!.password, password))) { return accessTokenWithRefreshCookie(user!.id, res)
throw new Error()
}
return refreshTokens(user!.id, res)
} catch (error) { } catch (error) {
throw new Error("Login credentials are invalid: " + error) throw new Error("Login credentials are invalid: " + error)
} }

@ -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 { 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 { AuthChecker } from "type-graphql"
import { AccessToken } from "./AccessToken" import { AccessToken } from "./AccessToken"
export type Context = { export const hashPassword = async (password: string) =>
req: Request await argonHash(password, { type: argon2id })
res: Response
payload?: ContextPayload
}
type ContextPayload = { export const comparePasswords = async (hash: string, plain: string) => {
userId: number if (!(await argonVerify(hash, plain, { type: argon2id }))) {
} throw new Error("Passwords do not match")
}
type JWTPayload = { return true
userId: number
iat: number
exp?: number
} }
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) => { export const signAccessToken = (payload: ContextPayload) => {
const accessTokenSecret = process.env.ACCESS_SECRET as string const accessTokenSecret = process.env.ACCESS_SECRET as string
return sign(payload, accessTokenSecret, { return jwtSign(payload, accessTokenSecret, {
expiresIn: parseInt(process.env.ACCESS_EXPIRY as string), expiresIn: parseInt(process.env.ACCESS_EXPIRY as string),
}) })
} }
@ -37,7 +26,7 @@ export const signAccessToken = (payload: ContextPayload) => {
export const signRefreshToken = (payload: ContextPayload) => { export const signRefreshToken = (payload: ContextPayload) => {
const accessTokenSecret = process.env.REFRESH_SECRET as string const accessTokenSecret = process.env.REFRESH_SECRET as string
return sign(payload, accessTokenSecret, { return jwtSign(payload, accessTokenSecret, {
expiresIn: parseInt(process.env.REFRESH_EXPIRY as string), expiresIn: parseInt(process.env.REFRESH_EXPIRY as string),
}) })
} }
@ -54,7 +43,7 @@ export const verifiedRefreshTokenPayload = (token: string) => {
return jwtVerify(token, refreshTokenSecret) as JWTPayload return jwtVerify(token, refreshTokenSecret) as JWTPayload
} }
export const refreshTokens = (userId: number, res: Response) => { export const accessTokenWithRefreshCookie = (userId: number, res: Response) => {
const accessToken = new AccessToken() const accessToken = new AccessToken()
accessToken.jwt = signAccessToken({ userId }) accessToken.jwt = signAccessToken({ userId })
accessToken.jwtExpiry = parseInt(process.env.ACCESS_EXPIRY as string) accessToken.jwtExpiry = parseInt(process.env.ACCESS_EXPIRY as string)
@ -83,3 +72,19 @@ export const customAuthChecker: AuthChecker<Context> = ({ context }) => {
} }
export const contextFunction = ({ req, res }: Context) => ({ req, res }) 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
}

Loading…
Cancel
Save