pass me query bad and good token tests

master
Peter Babič 5 years ago
parent 1d1e5230d8
commit d4a6faad5a
Signed by: peter.babic
GPG Key ID: 4BB075BC1884BA40
  1. 1
      .gitignore
  2. 99
      package-lock.json
  3. 7
      package.json
  4. 4
      src/User.ts
  5. 150
      src/User/UserResolver.spec.ts
  6. 62
      src/User/UserResolver.ts
  7. 33
      src/app.ts
  8. 59
      src/auth.ts
  9. 105
      src/modules/User/UserResolver.spec.ts
  10. 54
      src/modules/User/UserResolver.ts
  11. 24
      src/schema.ts
  12. 10
      src/utils/argon2.ts
  13. 15
      src/utils/callSchema.ts
  14. 40
      src/utils/createSchema.ts
  15. 12
      src/utils/jwt.ts

1
.gitignore vendored

@ -1,5 +1,6 @@
*.key
*.pub
.vscode/
# Created by https://www.gitignore.io/api/node
# Edit at https://www.gitignore.io/?templates=node

99
package-lock.json generated

@ -560,6 +560,16 @@
"@types/serve-static": "*"
}
},
"@types/express-jwt": {
"version": "0.0.42",
"resolved": "https://registry.npmjs.org/@types/express-jwt/-/express-jwt-0.0.42.tgz",
"integrity": "sha512-WszgUddvM1t5dPpJ3LhWNH8kfNN8GPIBrAGxgIYXVCEGx6Bx4A036aAuf/r5WH9DIEdlmp7gHOYvSM6U87B0ag==",
"dev": true,
"requires": {
"@types/express": "*",
"@types/express-unless": "*"
}
},
"@types/express-serve-static-core": {
"version": "4.16.9",
"resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.16.9.tgz",
@ -569,11 +579,14 @@
"@types/range-parser": "*"
}
},
"@types/faker": {
"version": "4.1.5",
"resolved": "https://registry.npmjs.org/@types/faker/-/faker-4.1.5.tgz",
"integrity": "sha512-YSDqoBEWYGdNk53xSkkb6REaUaVSlIjxIAGjj/nbLzlZOit7kUU+nA2zC2qQkIVO4MQ+3zl4Sz7aw+kbpHHHUQ==",
"dev": true
"@types/express-unless": {
"version": "0.5.1",
"resolved": "https://registry.npmjs.org/@types/express-unless/-/express-unless-0.5.1.tgz",
"integrity": "sha512-5fuvg7C69lemNgl0+v+CUxDYWVPSfXHhJPst4yTLcqi4zKJpORCxnDrnnilk3k0DTq/WrAUdvXFs01+vUqUZHw==",
"dev": true,
"requires": {
"@types/express": "*"
}
},
"@types/fs-capacitor": {
"version": "2.0.0",
@ -1016,9 +1029,9 @@
"integrity": "sha512-MO4oJ129vuCcbqwr5ZwgxqGGiLz3hCyowz0bstUF7MR+vNGe4oe3DWajC9lv4CxrhcqUHQOeOPViOdIo1IxE3g=="
},
"apollo-server-express": {
"version": "2.9.3",
"resolved": "https://registry.npmjs.org/apollo-server-express/-/apollo-server-express-2.9.3.tgz",
"integrity": "sha512-Hkfs+ce6GqaoSzDOJs8Pj7W3YUjH0BzGglo5HMsOXOnjPZ0pJE9v8fmK76rlkITLw7GjvIq5GKlafymC31FMBw==",
"version": "2.9.4",
"resolved": "https://registry.npmjs.org/apollo-server-express/-/apollo-server-express-2.9.4.tgz",
"integrity": "sha512-diX9n81E0tIJ0Sy2bHvDGPM9QsFBsZ76Nx/dszinY00ViyWG0yIAYEYWeRbsoKTeNDWWTvlMrh/3Eu2oaCIEhQ==",
"requires": {
"@apollographql/graphql-playground-html": "1.6.24",
"@types/accepts": "^1.3.5",
@ -1026,7 +1039,7 @@
"@types/cors": "^2.8.4",
"@types/express": "4.17.1",
"accepts": "^1.3.5",
"apollo-server-core": "^2.9.3",
"apollo-server-core": "^2.9.4",
"apollo-server-types": "^0.2.4",
"body-parser": "^1.18.3",
"cors": "^2.8.4",
@ -1036,6 +1049,36 @@
"parseurl": "^1.3.2",
"subscriptions-transport-ws": "^0.9.16",
"type-is": "^1.6.16"
},
"dependencies": {
"apollo-server-core": {
"version": "2.9.4",
"resolved": "https://registry.npmjs.org/apollo-server-core/-/apollo-server-core-2.9.4.tgz",
"integrity": "sha512-6mzipnn9woJxgo/JQFWTlY13svS7HCr0ZsN035eRmKOsXzROfB9ugXcTuc6MP94ICM7TlB/DtJOP+bLX53mijw==",
"requires": {
"@apollographql/apollo-tools": "^0.4.0",
"@apollographql/graphql-playground-html": "1.6.24",
"@types/graphql-upload": "^8.0.0",
"@types/ws": "^6.0.0",
"apollo-cache-control": "^0.8.4",
"apollo-datasource": "^0.6.3",
"apollo-engine-reporting": "^1.4.6",
"apollo-server-caching": "^0.5.0",
"apollo-server-env": "^2.4.3",
"apollo-server-errors": "^2.3.3",
"apollo-server-plugin-base": "^0.6.4",
"apollo-server-types": "^0.2.4",
"apollo-tracing": "^0.8.4",
"fast-json-stable-stringify": "^2.0.0",
"graphql-extensions": "^0.10.3",
"graphql-tag": "^2.9.2",
"graphql-tools": "^4.0.0",
"graphql-upload": "^8.0.2",
"sha.js": "^2.4.11",
"subscriptions-transport-ws": "^0.9.11",
"ws": "^6.0.0"
}
}
}
},
"apollo-server-plugin-base": {
@ -1173,6 +1216,11 @@
"integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==",
"dev": true
},
"async": {
"version": "1.5.2",
"resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz",
"integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo="
},
"async-limiter": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz",
@ -2312,6 +2360,22 @@
}
}
},
"express-jwt": {
"version": "5.3.1",
"resolved": "https://registry.npmjs.org/express-jwt/-/express-jwt-5.3.1.tgz",
"integrity": "sha512-1C9RNq0wMp/JvsH/qZMlg3SIPvKu14YkZ4YYv7gJQ1Vq+Dv8LH9tLKenS5vMNth45gTlEUGx+ycp9IHIlaHP/g==",
"requires": {
"async": "^1.5.0",
"express-unless": "^0.3.0",
"jsonwebtoken": "^8.1.0",
"lodash.set": "^4.0.0"
}
},
"express-unless": {
"version": "0.3.1",
"resolved": "https://registry.npmjs.org/express-unless/-/express-unless-0.3.1.tgz",
"integrity": "sha1-JVfBRudb65A+LSR/m1ugFFJpbiA="
},
"extend": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
@ -2410,12 +2474,6 @@
"integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=",
"dev": true
},
"faker": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/faker/-/faker-4.1.0.tgz",
"integrity": "sha1-HkW7vsxndLPBlfrSg1EJxtdIzD8=",
"dev": true
},
"fast-deep-equal": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz",
@ -3241,9 +3299,9 @@
"dev": true
},
"handlebars": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.2.0.tgz",
"integrity": "sha512-Kb4xn5Qh1cxAKvQnzNWZ512DhABzyFNmsaJf3OAkWNa4NkaqWcNI8Tao8Tasi0/F4JD9oyG0YxuFyvyR57d+Gw==",
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.3.1.tgz",
"integrity": "sha512-c0HoNHzDiHpBt4Kqe99N8tdLPKAnGCQ73gYMPWtAYM4PwGnf7xl8PBUHJqh9ijlzt2uQKaSRxbXRt+rZ7M2/kA==",
"dev": true,
"requires": {
"neo-async": "^2.6.0",
@ -4477,6 +4535,11 @@
"resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
"integrity": "sha1-DdOXEhPHxW34gJd9UEyI+0cal6w="
},
"lodash.set": {
"version": "4.3.2",
"resolved": "https://registry.npmjs.org/lodash.set/-/lodash.set-4.3.2.tgz",
"integrity": "sha1-2HV7HagH3eJIFrDWqEvqGnYjCyM="
},
"lodash.sortby": {
"version": "4.7.0",
"resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz",

@ -5,7 +5,7 @@
"main": "src/app.js",
"scripts": {
"dev": "ts-node-dev --respawn src/app.ts",
"test": "jest --watch",
"test": "jest -i --watch",
"gen:key": "ssh-keygen -t rsa -b 2048 -f src/utils/keys/jwtRS256.key && openssl rsa -in src/utils/keys/jwtRS256.key -pubout -outform PEM -out src/utils/keys/jwtRS256.key.pub"
},
"keywords": [],
@ -13,8 +13,10 @@
"license": "ISC",
"dependencies": {
"apollo-server": "^2.9.3",
"apollo-server-express": "^2.9.4",
"argon2": "^0.24.1",
"dotenv": "^8.1.0",
"express": "^4.17.1",
"graphql": "^14.5.4",
"jsonwebtoken": "^8.5.1",
"pg": "^7.12.1",
@ -23,14 +25,13 @@
"typeorm": "^0.2.18"
},
"devDependencies": {
"@types/faker": "^4.1.5",
"@types/express": "^4.17.1",
"@types/graphql": "^14.5.0",
"@types/jest": "^24.0.18",
"@types/js-cookie": "^2.2.2",
"@types/jsonwebtoken": "^8.3.3",
"@types/node": "^12.7.5",
"class-transformer": "^0.2.3",
"faker": "^4.1.0",
"jest": "^24.9.0",
"ts-jest": "^24.1.0",
"ts-node-dev": "^1.0.0-pre.42",

@ -1,7 +1,7 @@
import "reflect-metadata"
import { Field, ObjectType } from "type-graphql"
import { BaseEntity, BeforeInsert, Column, Entity, PrimaryGeneratedColumn } from "typeorm"
import * as argon2 from "../utils/argon2"
import { hashPassword } from "./auth"
@ObjectType()
@Entity()
@ -18,6 +18,6 @@ export class User extends BaseEntity {
@BeforeInsert()
async hashPassword() {
this.password = await argon2.hashIncludingOptions(this.password)
this.password = await hashPassword(this.password)
}
}

@ -0,0 +1,150 @@
import { gql } from "apollo-server"
import { createConnection, getConnection } from "typeorm"
import { signToken, verifyToken } from "../auth"
import { callSchema } from "../schema"
import { User } from "../User"
beforeAll(async () => {
return await createConnection({
type: "postgres",
host: "localhost",
port: 5432,
database: "testing",
username: "postgres",
password: "postgres",
// dropSchema: true,
entities: [User],
synchronize: true,
logging: false,
})
})
afterAll(async () => {
return await getConnection().close()
})
afterEach(async () => {
return await getConnection().synchronize(true)
})
describe("resolver of user", () => {
describe("createUser mutation should", () => {
it("return email as it creates user with mutation", async () => {
const createUserMutation = gql`
mutation {
createUser(email: "email@email.com", password: "password") {
email
}
}
`
const response = await callSchema(createUserMutation)
expect(response.errors).toBeUndefined()
expect(response.data).toMatchObject({
createUser: { email: "email@email.com" },
})
})
})
describe("users query should", () => {
it("return emails of registered users", async () => {
const usersQuery = gql`
query {
users {
email
}
}
`
const user = await User.create({
email: "email@email.com",
}).save()
const response = await callSchema(usersQuery)
expect(response.errors).toBeUndefined()
expect(response.data).toMatchObject({
users: [{ email: user.email }],
})
})
})
describe("loginTokens query should", () => {
const loginTokensQuery = gql`
query {
loginTokens(email: "email@email.com", password: "good-password") {
accessToken
}
}
`
it("return error for non-existent user", async () => {
const response = await callSchema(loginTokensQuery)
expect(response.errors).not.toBeUndefined()
expect(response.data).toBeNull()
})
it("return error for bad password", async () => {
await User.create({
email: "email@email.com",
password: "BAD-password",
}).save()
const response = await callSchema(loginTokensQuery)
expect(response.errors).not.toBeUndefined()
expect(response.data).toBeNull()
})
it("return a valid access token with good credentials", async () => {
await User.create({
email: "email@email.com",
password: "good-password",
}).save()
const response = await callSchema(loginTokensQuery)
const token = response.data!.loginTokens.accessToken
expect(verifyToken(token)).toBeTruthy()
})
})
describe("me query should", () => {
const meQuery = gql`
query {
me {
email
}
}
`
it("return an error without a valid jwt token", async () => {
const response = await callSchema(meQuery)
expect(response.errors).not.toBeUndefined()
expect(response.data).toBeNull()
})
it("return an user with a valid jwt token", async () => {
const user = await User.create({
email: "email@email.com",
}).save()
const context = {
req: {
headers: {
authorization: "Bearer " + signToken({ userId: user.id }),
},
},
}
const response = await callSchema(meQuery, context)
expect(response.errors).toBeUndefined()
expect(response.data).toMatchObject({
me: { email: user.email },
})
})
})
})

@ -0,0 +1,62 @@
import "reflect-metadata"
import { Arg, Authorized, Ctx, Field, Mutation, ObjectType, Query, Resolver } from "type-graphql"
import { comparePassword, MyContext, signToken } from "../auth"
import { User } from "../User"
@ObjectType()
class LoginTokens {
@Field()
accessToken: string = ""
}
@Resolver(() => User)
export class UserResolver {
@Query(() => [User])
async users() {
return await User.find()
}
@Query(() => LoginTokens)
async loginTokens(
@Arg("email") email: string,
@Arg("password") password: string
): Promise<LoginTokens> {
const user = await User.findOne({ where: { email } })
if (!user) {
throw new Error("could not find user")
}
const passwordValid = await comparePassword(user.password, password)
if (!passwordValid) {
throw new Error("password not valid")
}
const accessToken = signToken({ userId: user.id })
return {
accessToken,
}
}
@Query(() => User)
@Authorized()
async me(@Ctx() { payload }: MyContext) {
const id = payload!.userId
const user = await User.findOne({ where: { id } })
return user
}
@Mutation(() => User)
async createUser(
@Arg("email") email: string,
@Arg("password") password: string
): Promise<User> {
return await User.create({
email,
password,
}).save()
}
}

@ -1,13 +1,11 @@
require("dotenv").config()
import { ApolloServer } from "apollo-server"
import { ApolloServer } from "apollo-server-express"
import { createConnection } from "typeorm"
import { User } from "./modules/User"
import { createSchema } from "./utils/createSchema"
const PORT = process.env.PORT || 4000
async function bootstrap() {
import { createSchema } from "./schema"
import { User } from "./User"
import express = require("express")
;(async () => {
await createConnection({
type: "postgres",
host: "localhost",
@ -21,18 +19,19 @@ async function bootstrap() {
logging: false,
})
// ... Building schema here
const schema = await createSchema()
// Create the GraphQL server
const server = new ApolloServer({
schema,
schema: await createSchema(),
playground: true,
introspection: true,
debug: true,
context: ({ req, res }) => ({ req, res }),
})
// Start the server
const { url } = await server.listen(PORT)
console.log(`Server is running, GraphQL Playground available at ${url}`)
}
const app = express()
server.applyMiddleware({ app })
bootstrap()
const PORT = process.env.PORT || 4000
app.listen({ port: PORT }, () =>
console.log(`🚀 Server ready at http://localhost:${PORT}${server.graphqlPath}. `)
)
})()

@ -0,0 +1,59 @@
import { argon2id, hash, verify as argonVerify } from "argon2"
import { Request, Response } from "express"
import { readFileSync } from "fs"
import { sign, verify as jwtVerify } from "jsonwebtoken"
import { join } from "path"
import { AuthChecker } from "type-graphql"
export type Payload = {
userId: number
}
export interface MyContext {
req: Request
res: Response
payload?: Payload
}
export async function hashPassword(password: string) {
return await hash(password, { type: argon2id })
}
export async function comparePassword(hash: string, plain: string) {
return await argonVerify(hash, plain, { type: argon2id })
}
export function signToken(payload: Payload) {
const PRIVATE_KEY = readFileSync(join(__dirname, "auth", "jwtRS256.key"))
return sign(payload, PRIVATE_KEY, { algorithm: "RS256" })
}
export function verifyToken(token: string) {
const PUBLIC_KEY = readFileSync(join(__dirname, "auth", "jwtRS256.key.pub"))
return jwtVerify(token, PUBLIC_KEY)
}
export const customAuthChecker: AuthChecker<MyContext> = ({ context }) => {
const authHeader = context.req.headers["authorization"]
if (!authHeader) {
throw new Error("authorization header is missing")
}
const token = authHeader.split(" ")[1]
if (!token) {
throw new Error("token not present in authorization header")
}
const payload = verifyToken(token)
if (!payload) {
throw new Error("payload not present in the token")
}
context.payload = payload as any
return true
}

@ -1,105 +0,0 @@
import faker = require("faker")
import { createConnection, getConnection } from "typeorm"
import { callSchema } from "../../utils/callSchema"
import * as jwt from "../../utils/jwt"
import { User } from "../User"
beforeAll(async () => {
return await createConnection({
type: "postgres",
host: "localhost",
port: 5432,
database: "testing",
username: "postgres",
password: "postgres",
// dropSchema: true,
entities: [User],
synchronize: true,
logging: false,
})
})
afterAll(async () => {
return await getConnection().close()
})
afterEach(async () => {
return await getConnection().synchronize(true)
})
describe("resolver of user", () => {
describe("createUser mutation should", () => {
it("return email as it creates user with mutation", async () => {
const fakeEmail = faker.internet.email()
const fakePassword = faker.internet.password(8)
const createUserMutation = `mutation {
createUser(email: "${fakeEmail}", password: "${fakePassword}") {
email
}
}`
const response = await callSchema(createUserMutation)
expect(response.errors).toBeUndefined()
expect(response.data).toMatchObject({
createUser: { email: fakeEmail },
})
})
})
describe("users query should", () => {
it("return emails of registered users", async () => {
const usersQuery = `{
users {
email
}
}`
const user = await User.create({
email: faker.internet.email(),
}).save()
const response = await callSchema(usersQuery)
expect(response.errors).toBeUndefined()
expect(response.data).toMatchObject({
users: [{ email: user.email }],
})
})
})
describe("loginToken query should", () => {
it("return a valid login token", async () => {
const fakeEmail = faker.internet.email()
const fakePassword = faker.internet.password(6)
await User.create({
email: fakeEmail,
password: fakePassword,
}).save()
const loginTokenQuery = `{
loginToken(email: "${fakeEmail}", password: "${fakePassword}")
}`
const response = await callSchema(loginTokenQuery)
const token = response.data!.loginToken
expect(jwt.verify(token, jwt.PUBLIC_KEY)).toBeTruthy()
})
})
describe("me query should", () => {
it("return an error and null data without a valid jwt token", async () => {
const meQuery = `{
me {
email
}
}`
const response = await callSchema(meQuery)
expect(response.errors).not.toBeUndefined()
expect(response.data).toBeNull()
})
})
})

@ -1,54 +0,0 @@
import "reflect-metadata"
import { Arg, Authorized, Mutation, Query, Resolver } from "type-graphql"
import * as argon2 from "../../utils/argon2"
import * as jwt from "../../utils/jwt"
import { User } from "../User"
@Resolver(() => User)
export class UserResolver {
@Query(() => [User])
async users() {
return await User.find()
}
@Query(() => String, { nullable: true })
async loginToken(
@Arg("email") email: string,
@Arg("password") password: string
): Promise<string | null> {
const user = await User.findOne({ where: { email } })
if (!user) {
return null
}
const passwordValid = await argon2.verify(user.password, password)
if (!passwordValid) {
return null
}
const token = jwt.signWithRS256({ userId: user.id })
return token
}
@Query(() => User)
@Authorized()
async me() {
const user = User.create({
email: "asddsf@fdsfs.sk",
})
return user
}
@Mutation(() => User)
async createUser(
@Arg("email") email: string,
@Arg("password") password: string
): Promise<User> {
return await User.create({
email,
password,
}).save()
}
}

@ -0,0 +1,24 @@
import { DocumentNode, graphql, GraphQLSchema } from "graphql"
import { buildSchema } from "type-graphql"
import { customAuthChecker } from "./auth"
import { UserResolver } from "./User/UserResolver"
let schema: GraphQLSchema
export const callSchema = async (document: DocumentNode, context?: any) => {
if (!schema) {
schema = await createSchema()
}
return graphql({
schema,
source: document.loc!.source.body || "",
contextValue: context,
})
}
export const createSchema = () =>
buildSchema({
resolvers: [UserResolver],
authChecker: customAuthChecker,
})

@ -1,10 +0,0 @@
import * as agron2 from "argon2"
export * from "argon2"
/**
* Override the defaul agron2i option with agron2id
* @param password Pasword to has using argon2id
*/
export async function hashIncludingOptions(password: string) {
return await agron2.hash(password, { type: agron2.argon2id })
}

@ -1,15 +0,0 @@
import { graphql, GraphQLSchema } from "graphql";
import { createSchema } from "./createSchema";
let schema: GraphQLSchema
export const callSchema = async ( source : string) => {
if (!schema) {
schema = await createSchema()
}
return graphql({
schema,
source,
})
}

@ -1,40 +0,0 @@
import { AuthChecker, buildSchema, MiddlewareFn } from "type-graphql"
// import { User } from "../modules/User"
import { UserResolver } from "../modules/User/UserResolver"
const errorInterceptor: MiddlewareFn<any> = async ({}, next) => {
try {
return await next()
} catch (err) {
console.error(err)
}
}
const customAuthChecker: AuthChecker<any> = () => false
// { root, args, context, info },
// roles,
// ) => {
// console.log(`root: `)
// console.log(root)
// console.log(`args: `)
// console.log(args)
// console.log(`context: `)
// console.log(context)
// console.log(`info: `)
// console.log(info)
// console.log(`roles: `)
// console.log(roles)
// // here we can read the user from context
// // and check his permission in the db against the `roles` argument
// // that comes from the `@Authorized` decorator, eg. ["ADMIN", "MODERATOR"]
// return false; // or false if access is denied
// };
export const createSchema = () =>
buildSchema({
resolvers: [UserResolver],
globalMiddlewares: [errorInterceptor],
authChecker: customAuthChecker,
// authMode: "null"
})

@ -1,12 +0,0 @@
import * as jwt from "jsonwebtoken"
export * from "jsonwebtoken"
import fs = require('fs')
import path = require('path')
const PRIVATE_KEY = fs.readFileSync(path.join(__dirname, 'keys', 'jwtRS256.key'))
export const PUBLIC_KEY = fs.readFileSync(path.join(__dirname, 'keys', 'jwtRS256.key.pub'))
export function signWithRS256(payload: string | object) {
return jwt.sign(payload, PRIVATE_KEY, {algorithm: "RS256"})
}
Loading…
Cancel
Save