prepare for refresh tokens feature

master
Peter Babič 5 years ago
parent 69233bb7e1
commit 2732ade178
Signed by: peter.babic
GPG Key ID: 4BB075BC1884BA40
  1. 204
      package-lock.json
  2. 3
      package.json
  3. 97
      src/server.spec.ts
  4. 3
      src/server.ts
  5. 38
      src/server/UserResolver.spec.ts
  6. 3
      src/server/UserResolver.ts
  7. 14
      src/server/testing.ts
  8. 8
      src/server/userResolver/ContextInterface.ts
  9. 28
      src/server/userResolver/auth.ts

204
package-lock.json generated

@ -707,6 +707,15 @@
"resolved": "https://registry.npmjs.org/@types/node/-/node-12.7.5.tgz",
"integrity": "sha512-9fq4jZVhPNW8r+UYKnxF1e2HkDWOWKM5bC2/7c9wPV835I0aOrVbS/Hw/pWPk2uKrNXQqg9Z959Kz+IYDd5p3w=="
},
"@types/node-fetch": {
"version": "2.5.2",
"resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.5.2.tgz",
"integrity": "sha512-djYYKmdNRSBtL1x4CiE9UJb9yZhwtI1VC+UxZD0psNznrUj80ywsxKlEGAE+QL1qvLjPbfb24VosjkYM6W4RSQ==",
"dev": true,
"requires": {
"@types/node": "*"
}
},
"@types/range-parser": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.3.tgz",
@ -772,6 +781,22 @@
"integrity": "sha512-gCubfBUZ6KxzoibJ+SCUc/57Ms1jz5NjHe4+dI2krNmU5zCPAphyLJYyTOg06ueIyfj+SaCUqmzun7ImlxDcKg==",
"dev": true
},
"@types/zen-observable": {
"version": "0.8.0",
"resolved": "https://registry.npmjs.org/@types/zen-observable/-/zen-observable-0.8.0.tgz",
"integrity": "sha512-te5lMAWii1uEJ4FwLjzdlbw3+n0FZNOvFXHxQDKeT0dilh7HOzdMzV2TrJVUzq8ep7J4Na8OUYPRLSQkJHAlrg==",
"dev": true
},
"@wry/context": {
"version": "0.4.4",
"resolved": "https://registry.npmjs.org/@wry/context/-/context-0.4.4.tgz",
"integrity": "sha512-LrKVLove/zw6h2Md/KZyWxIkFM6AoyKp71OqpH9Hiip1csjPVoD3tPxlbQUNxEnHENks3UGgNpSBCAfq9KWuag==",
"dev": true,
"requires": {
"@types/node": ">=6",
"tslib": "^1.9.3"
}
},
"@wry/equality": {
"version": "0.1.9",
"resolved": "https://registry.npmjs.org/@wry/equality/-/equality-0.1.9.tgz",
@ -871,6 +896,33 @@
"normalize-path": "^2.1.1"
}
},
"apollo-boost": {
"version": "0.4.4",
"resolved": "https://registry.npmjs.org/apollo-boost/-/apollo-boost-0.4.4.tgz",
"integrity": "sha512-ASngBvazmp9xNxXfJ2InAzfDwz65o4lswlEPrWoN35scXmCz8Nz4k3CboUXbrcN/G0IExkRf/W7o9Rg0cjEBqg==",
"dev": true,
"requires": {
"apollo-cache": "^1.3.2",
"apollo-cache-inmemory": "^1.6.3",
"apollo-client": "^2.6.4",
"apollo-link": "^1.0.6",
"apollo-link-error": "^1.0.3",
"apollo-link-http": "^1.3.1",
"graphql-tag": "^2.4.2",
"ts-invariant": "^0.4.0",
"tslib": "^1.9.3"
}
},
"apollo-cache": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/apollo-cache/-/apollo-cache-1.3.2.tgz",
"integrity": "sha512-+KA685AV5ETEJfjZuviRTEImGA11uNBp/MJGnaCvkgr+BYRrGLruVKBv6WvyFod27WEB2sp7SsG8cNBKANhGLg==",
"dev": true,
"requires": {
"apollo-utilities": "^1.3.2",
"tslib": "^1.9.3"
}
},
"apollo-cache-control": {
"version": "0.8.4",
"resolved": "https://registry.npmjs.org/apollo-cache-control/-/apollo-cache-control-0.8.4.tgz",
@ -880,6 +932,35 @@
"graphql-extensions": "^0.10.3"
}
},
"apollo-cache-inmemory": {
"version": "1.6.3",
"resolved": "https://registry.npmjs.org/apollo-cache-inmemory/-/apollo-cache-inmemory-1.6.3.tgz",
"integrity": "sha512-S4B/zQNSuYc0M/1Wq8dJDTIO9yRgU0ZwDGnmlqxGGmFombOZb9mLjylewSfQKmjNpciZ7iUIBbJ0mHlPJTzdXg==",
"dev": true,
"requires": {
"apollo-cache": "^1.3.2",
"apollo-utilities": "^1.3.2",
"optimism": "^0.10.0",
"ts-invariant": "^0.4.0",
"tslib": "^1.9.3"
}
},
"apollo-client": {
"version": "2.6.4",
"resolved": "https://registry.npmjs.org/apollo-client/-/apollo-client-2.6.4.tgz",
"integrity": "sha512-oWOwEOxQ9neHHVZrQhHDbI6bIibp9SHgxaLRVPoGvOFy7OH5XUykZE7hBQAVxq99tQjBzgytaZffQkeWo1B4VQ==",
"dev": true,
"requires": {
"@types/zen-observable": "^0.8.0",
"apollo-cache": "1.3.2",
"apollo-link": "^1.0.0",
"apollo-utilities": "1.3.2",
"symbol-observable": "^1.0.2",
"ts-invariant": "^0.4.0",
"tslib": "^1.9.3",
"zen-observable": "^0.8.0"
}
},
"apollo-datasource": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/apollo-datasource/-/apollo-datasource-0.6.3.tgz",
@ -941,6 +1022,39 @@
"zen-observable-ts": "^0.8.20"
}
},
"apollo-link-error": {
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/apollo-link-error/-/apollo-link-error-1.1.12.tgz",
"integrity": "sha512-psNmHyuy3valGikt/XHJfe0pKJnRX19tLLs6P6EHRxg+6q6JMXNVLYPaQBkL0FkwdTCB0cbFJAGRYCBviG8TDA==",
"dev": true,
"requires": {
"apollo-link": "^1.2.13",
"apollo-link-http-common": "^0.2.15",
"tslib": "^1.9.3"
}
},
"apollo-link-http": {
"version": "1.5.16",
"resolved": "https://registry.npmjs.org/apollo-link-http/-/apollo-link-http-1.5.16.tgz",
"integrity": "sha512-IA3xA/OcrOzINRZEECI6IdhRp/Twom5X5L9jMehfzEo2AXdeRwAMlH5LuvTZHgKD8V1MBnXdM6YXawXkTDSmJw==",
"dev": true,
"requires": {
"apollo-link": "^1.2.13",
"apollo-link-http-common": "^0.2.15",
"tslib": "^1.9.3"
}
},
"apollo-link-http-common": {
"version": "0.2.15",
"resolved": "https://registry.npmjs.org/apollo-link-http-common/-/apollo-link-http-common-0.2.15.tgz",
"integrity": "sha512-+Heey4S2IPsPyTf8Ag3PugUupASJMW894iVps6hXbvwtg1aHSNMXUYO5VG7iRHkPzqpuzT4HMBanCTXPjtGzxg==",
"dev": true,
"requires": {
"apollo-link": "^1.2.13",
"ts-invariant": "^0.4.0",
"tslib": "^1.9.3"
}
},
"apollo-server": {
"version": "2.9.3",
"resolved": "https://registry.npmjs.org/apollo-server/-/apollo-server-2.9.3.tgz",
@ -1362,6 +1476,24 @@
"integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==",
"dev": true
},
"axios": {
"version": "0.19.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.19.0.tgz",
"integrity": "sha512-1uvKqKQta3KBxIz14F2v06AEHZ/dIoeKfbTRkK1E5oqjDnuEerLmYTgJB5AiQZHJcljpg1TuRzdjDR06qNk0DQ==",
"dev": true,
"requires": {
"follow-redirects": "1.5.10",
"is-buffer": "^2.0.2"
},
"dependencies": {
"is-buffer": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.4.tgz",
"integrity": "sha512-Kq1rokWXOPXWuaMAqZiJW4XxsmD9zGx9q4aePabbn3qCRGedtH7Cm+zV8WETitMfu1wdh+Rvd6w5egwSngUX2A==",
"dev": true
}
}
},
"babel-jest": {
"version": "24.9.0",
"resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-24.9.0.tgz",
@ -1713,12 +1845,6 @@
"integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==",
"dev": true
},
"class-transformer": {
"version": "0.2.3",
"resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.2.3.tgz",
"integrity": "sha512-qsP+0xoavpOlJHuYsQJsN58HXSl8Jvveo+T37rEvCEeRfMWoytAyR0Ua/YsFgpM6AZYZ/og2PJwArwzJl1aXtQ==",
"dev": true
},
"class-utils": {
"version": "0.3.6",
"resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz",
@ -2270,15 +2396,6 @@
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
"integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k="
},
"encoding": {
"version": "0.1.12",
"resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.12.tgz",
"integrity": "sha1-U4tm8+5izRq1HsMjgp0flIDHS+s=",
"dev": true,
"requires": {
"iconv-lite": "~0.4.13"
}
},
"end-of-stream": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz",
@ -2677,6 +2794,26 @@
"locate-path": "^3.0.0"
}
},
"follow-redirects": {
"version": "1.5.10",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz",
"integrity": "sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==",
"dev": true,
"requires": {
"debug": "=3.1.0"
},
"dependencies": {
"debug": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
"integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
"dev": true,
"requires": {
"ms": "2.0.0"
}
}
}
},
"for-in": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz",
@ -3836,28 +3973,6 @@
"integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=",
"dev": true
},
"isomorphic-fetch": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz",
"integrity": "sha1-YRrhrPFPXoH3KVB0coGf6XM1WKk=",
"dev": true,
"requires": {
"node-fetch": "^1.0.1",
"whatwg-fetch": ">=0.10.0"
},
"dependencies": {
"node-fetch": {
"version": "1.7.3",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-1.7.3.tgz",
"integrity": "sha512-NhZ4CsKx7cYm2vSrBAr2PvFOe6sWDf0UYLRqA6svUYg7+/TSfVAu49jYC4BvQ4Sms9SZgdqGBgroqfDhJdTyKQ==",
"dev": true,
"requires": {
"encoding": "^0.1.11",
"is-stream": "^1.0.1"
}
}
}
},
"isstream": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz",
@ -5241,6 +5356,15 @@
"wrappy": "1"
}
},
"optimism": {
"version": "0.10.3",
"resolved": "https://registry.npmjs.org/optimism/-/optimism-0.10.3.tgz",
"integrity": "sha512-9A5pqGoQk49H6Vhjb9kPgAeeECfUDF6aIICbMDL23kDLStBn1MWk3YvcZ4xWF9CsSf6XEgvRLkXy4xof/56vVw==",
"dev": true,
"requires": {
"@wry/context": "^0.4.0"
}
},
"optimist": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz",
@ -7058,12 +7182,6 @@
"iconv-lite": "0.4.24"
}
},
"whatwg-fetch": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.0.0.tgz",
"integrity": "sha512-9GSJUgz1D4MfyKU7KRqwOjXCXTqWdFNvEr7eUBYchQiVc744mqK/MzXPNR2WsPkmkOa4ywfg8C2n8h+13Bey1Q==",
"dev": true
},
"whatwg-mimetype": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz",

@ -31,8 +31,11 @@
"@types/jest": "^24.0.18",
"@types/jsonwebtoken": "^8.3.3",
"@types/node": "^12.7.5",
"@types/node-fetch": "^2.5.2",
"apollo-boost": "^0.4.4",
"apollo-server-testing": "^2.9.6",
"jest": "^24.9.0",
"node-fetch": "^2.6.0",
"ts-jest": "^24.1.0",
"ts-node-dev": "^1.0.0-pre.42",
"typeorm-transactional-cls-hooked": "^0.1.8",

@ -1,26 +1,85 @@
import { ApolloServer } from "apollo-server-express"
import { createTestClient } from "apollo-server-testing"
import { createConnection, getConnection } from "typeorm"
import ApolloClient, { gql } from "apollo-boost"
import fetch from "node-fetch"
import { createConnection } from "typeorm"
import { createServer } from "./server"
import { testingConnectionOptions } from "./server/testing"
import auth = require("./server/userResolver/auth")
import {
initializeRollbackTransactions,
runInRollbackTransaction,
testingConnectionOptions,
} from "./server/testing"
describe("app should", () => {
it("call the context function on apollo server", async () => {
const spy = jest.spyOn(auth, "contextFunction")
await createConnection(testingConnectionOptions())
const port = 4001
const port = 4001
const server = (await createServer(port)) as any
const { query } = createTestClient(server)
await query({ query: "{me{email}}" })
beforeAll(async () => {
initializeRollbackTransactions()
await createConnection(testingConnectionOptions())
await createServer(port)
})
describe("server should", () => {
it(
"handle auth user me request",
runInRollbackTransaction(async () => {
const uri = `http://localhost:${port}/graphql`
let client = new ApolloClient({ uri, fetch })
const createUserMutation = gql`
mutation {
createUser(email: "email@email.com", password: "password") {
email
}
}
`
await client.mutate({ mutation: createUserMutation })
const loginTokensQuery = gql`
query {
loginTokens(email: "email@email.com", password: "password") {
accessToken
}
}
`
const tokens = await client.query({ query: loginTokensQuery })
const accessToken = tokens.data.loginTokens.accessToken
client = new ApolloClient({
uri,
fetch,
request: operation => {
operation.setContext({
headers: {
authorization: "Bearer " + accessToken,
},
})
},
})
const meQuery = gql`
query {
me {
email
}
}
`
const meResponse = await client.query({ query: meQuery })
const meEmail = meResponse.data.me.email
expect(meEmail).toBe("email@email.com")
})
)
expect(server).toBeInstanceOf(ApolloServer)
expect(spy).toHaveBeenCalledTimes(1)
it(
"receive no refresh token without auth header",
runInRollbackTransaction(async () => {
const uri = `http://localhost:${port}/refresh_token`
spy.mockRestore()
const response = await fetch(uri, { method: "POST" })
const jsonResponse = await response.json()
await server.stop()
await getConnection().close()
})
expect(jsonResponse.data).toBeNull()
expect(jsonResponse.errors).not.toBeUndefined()
})
)
})

@ -13,6 +13,9 @@ export const createServer = async (port: number) => {
})
const app = express()
app.post("/refresh_token", (_req, res) =>
res.send({ data: null, errors: "Invalid access token" })
)
server.applyMiddleware({ app })
app.listen({ port })

@ -1,20 +1,19 @@
import { gql } from "apollo-server"
import { Request, Response } from "express"
import { createConnection, getConnection } from "typeorm"
import {
initializeTransactionalContext,
patchTypeORMRepositoryWithBaseRepository,
} from "typeorm-transactional-cls-hooked"
import { callSchema } from "./schema"
import { runInTransaction, testingConnectionOptions } from "./testing"
import { ContextInterface, signAccessToken, verifyAccessToken } from "./userResolver/auth"
import {
initializeRollbackTransactions,
runInRollbackTransaction,
testingConnectionOptions,
} from "./testing"
import { signAccessToken, verifyAccessToken } from "./userResolver/auth"
import { ContextInterface } from "./userResolver/ContextInterface"
import { LoginTokens } from "./userResolver/LoginTokens"
import { User } from "./userResolver/User"
beforeAll(async () => {
initializeTransactionalContext()
patchTypeORMRepositoryWithBaseRepository()
initializeRollbackTransactions()
await createConnection(testingConnectionOptions())
})
@ -26,7 +25,7 @@ describe("resolver of user", () => {
describe("createUser mutation should", () => {
it(
"return email as it creates user with mutation",
runInTransaction(async () => {
runInRollbackTransaction(async () => {
const createUserMutation = gql`
mutation {
createUser(email: "email@email.com", password: "password") {
@ -48,7 +47,7 @@ describe("resolver of user", () => {
describe("users query should", () => {
it(
"return emails of registered users",
runInTransaction(async () => {
runInRollbackTransaction(async () => {
const usersQuery = gql`
query {
users {
@ -82,7 +81,7 @@ describe("resolver of user", () => {
it(
"return error for non-existent user",
runInTransaction(async () => {
runInRollbackTransaction(async () => {
const response = await callSchema(loginTokensQuery)
expect(response.errors).not.toBeUndefined()
@ -92,7 +91,7 @@ describe("resolver of user", () => {
it(
"return error for bad password",
runInTransaction(async () => {
runInRollbackTransaction(async () => {
await User.create({
email: "email@email.com",
password: "BAD-password",
@ -107,7 +106,7 @@ describe("resolver of user", () => {
it(
"return a valid access token with good credentials",
runInTransaction(async () => {
runInRollbackTransaction(async () => {
await User.create({
email: "email@email.com",
password: "good-password",
@ -115,12 +114,17 @@ describe("resolver of user", () => {
const response = await callSchema(loginTokensQuery)
const accessToken = response.data!.loginTokens.accessToken
const accessTokenPayload = verifyAccessToken(accessToken)
const loginTokens = new LoginTokens()
loginTokens.accessToken = accessToken
const fifteenMinutes = 900
const accessTokenLifetime =
accessTokenPayload.exp! - accessTokenPayload.iat!
expect(accessTokenLifetime).toBe(fifteenMinutes)
expect(accessTokenPayload).toBeTruthy()
expect(response.errors).toBeUndefined()
expect(response.data).toMatchObject({ loginTokens })
expect(verifyAccessToken(accessToken)).toBeTruthy()
})
)
})
@ -136,7 +140,7 @@ describe("resolver of user", () => {
it(
"return an error without a valid jwt token",
runInTransaction(async () => {
runInRollbackTransaction(async () => {
const contextWithInvalidToken = contextWithAuthHeader(
"Bearer INVALID-TOKEN"
)
@ -149,7 +153,7 @@ describe("resolver of user", () => {
it(
"return an user with a valid jwt token",
runInTransaction(async () => {
runInRollbackTransaction(async () => {
const user = await User.create({
email: "email@email.com",
}).save()

@ -1,6 +1,7 @@
import "reflect-metadata"
import { Arg, Authorized, Ctx, Mutation, Query } from "type-graphql"
import { comparePassword, ContextInterface, signAccessToken } from "./userResolver/auth"
import { comparePassword, signAccessToken } from "./userResolver/auth"
import { ContextInterface } from "./userResolver/ContextInterface"
import { LoginTokens } from "./userResolver/LoginTokens"
import { User } from "./userResolver/User"

@ -1,5 +1,10 @@
import { ConnectionOptions } from "typeorm"
import { Propagation, Transactional } from "typeorm-transactional-cls-hooked"
import {
initializeTransactionalContext,
patchTypeORMRepositoryWithBaseRepository,
Propagation,
Transactional,
} from "typeorm-transactional-cls-hooked"
import { connectionOptions } from "./connection"
export const testingConnectionOptions = () => {
@ -8,6 +13,11 @@ export const testingConnectionOptions = () => {
return { ...connectionOptions(), database } as ConnectionOptions
}
export const initializeRollbackTransactions = () => {
initializeTransactionalContext()
patchTypeORMRepositoryWithBaseRepository()
}
type RunFunction = () => Promise<void> | void
class RollbackError extends Error {
@ -26,7 +36,7 @@ class TransactionCreator {
}
}
export function runInTransaction(func: RunFunction) {
export function runInRollbackTransaction(func: RunFunction) {
return async () => {
try {
await TransactionCreator.run(func)

@ -0,0 +1,8 @@
import { Request, Response } from "express"
import { ContextPayload } from "./auth"
export interface ContextInterface {
req: Request
res: Response
payload?: ContextPayload
}

@ -1,16 +1,16 @@
import { argon2id, hash, verify as argonVerify } from "argon2"
import { Request, Response } from "express"
import { sign, verify as jwtVerify } from "jsonwebtoken"
import { AuthChecker } from "type-graphql"
import { ContextInterface } from "./ContextInterface"
export type Payload = {
export type ContextPayload = {
userId: number
}
export interface ContextInterface {
req: Request
res: Response
payload?: Payload
type AccessTokenPayload = {
userId: number
iat: number
exp?: number
}
export const hashPassword = async (password: string) =>
@ -19,20 +19,26 @@ export const hashPassword = async (password: string) =>
export const comparePassword = async (hash: string, plain: string) =>
await argonVerify(hash, plain, { type: argon2id })
export const signAccessToken = (payload: Payload) => {
return sign(payload, process.env.ACCESS_SECRET!)
export const signAccessToken = (payload: ContextPayload) => {
const accessTokenSecret = process.env.ACCESS_SECRET as string
return sign(payload, accessTokenSecret, {
expiresIn: process.env.ACCESS_EXP,
})
}
export const verifyAccessToken = (token: string) => {
return jwtVerify(token, process.env.ACCESS_SECRET!)
const accessTokenSecret = process.env.ACCESS_SECRET as string
return jwtVerify(token, accessTokenSecret) as AccessTokenPayload
}
export const customAuthChecker: AuthChecker<ContextInterface> = ({ context }) => {
try {
const authHeader = context.req.headers["authorization"]
const accessToken = authHeader!.split(" ")[1]
const payload = verifyAccessToken(accessToken)
context.payload = payload as any
const accessTokenPayload = verifyAccessToken(accessToken)
context.payload = accessTokenPayload as ContextPayload
return true
} catch (error) {

Loading…
Cancel
Save