|
|
|
// TODO: convert to import
|
|
|
|
import cookie = require("cookie")
|
|
|
|
import { gql } from "apollo-server-express"
|
|
|
|
import { GraphQLClient, rawRequest } from "graphql-request"
|
|
|
|
import fetch from "node-fetch"
|
|
|
|
import { createConnection } from "typeorm"
|
|
|
|
import { createServer } from "./server"
|
|
|
|
import { gqlToStr } from "./server/schema"
|
|
|
|
import { testingConnectionOptions } from "./server/testing"
|
|
|
|
import {
|
|
|
|
rtCookieOptions,
|
|
|
|
signRefreshToken,
|
|
|
|
verifiedRefreshTokenPayload,
|
|
|
|
} from "./server/userResolver/auth"
|
|
|
|
import { User } from "./server/userResolver/User"
|
|
|
|
|
|
|
|
let user: User
|
|
|
|
|
|
|
|
describe("server should", () => {
|
|
|
|
it("reject refresh token without valid cookie", async () => {
|
|
|
|
const response = await fetch(refreshTokenUri, {
|
|
|
|
method: "POST",
|
|
|
|
headers: { cookie: "INVALID-COOKIE" },
|
|
|
|
})
|
|
|
|
const jsonResponse = await response.json()
|
|
|
|
|
|
|
|
expect(jsonResponse.data).toBeNull()
|
|
|
|
expect(jsonResponse.errors).not.toBeUndefined()
|
|
|
|
})
|
|
|
|
|
|
|
|
it("reject refresh token with tokenVersion mismatch", async () => {
|
|
|
|
const oldRefreshToken = signRefreshToken({ uid: user.id, ver: 0 })
|
|
|
|
const cookieHeader = cookie.serialize("rt", oldRefreshToken, rtCookieOptions())
|
|
|
|
|
|
|
|
await user.invalidateTokens()
|
|
|
|
const response = await fetch(refreshTokenUri, {
|
|
|
|
method: "POST",
|
|
|
|
headers: { cookie: cookieHeader },
|
|
|
|
})
|
|
|
|
const jsonResponse = await response.json()
|
|
|
|
|
|
|
|
expect(jsonResponse.data).toBeNull()
|
|
|
|
expect(jsonResponse.errors).not.toBeUndefined()
|
|
|
|
})
|
|
|
|
|
|
|
|
it("provide access token given good crendentials and grant refresh token with it", async () => {
|
|
|
|
const halfADay = (60 * 60 * 24) / 2
|
|
|
|
const fifteenDays = 60 * 60 * 24 * 15
|
|
|
|
|
|
|
|
const accessTokenReponse = await rawRequest(gqlUri, gqlToStr(accessTokenMutation))
|
|
|
|
const accessToken: string = accessTokenReponse.data.accessToken
|
|
|
|
const headers: Headers = accessTokenReponse.headers
|
|
|
|
const varyHeader = headers.get("vary") as string
|
|
|
|
const acacHeader = headers.get("access-control-allow-credentials") as string
|
|
|
|
const acaoHeader = headers.get("access-control-allow-origin") as string
|
|
|
|
|
|
|
|
const cookieHeader = headers.get("set-cookie") as string
|
|
|
|
const parsedCookie = cookie.parse(cookieHeader)
|
|
|
|
const refreshCookieExpires = dateInKiloSeconds(parsedCookie.Expires)
|
|
|
|
const refreshTokenPayload = verifiedRefreshTokenPayload(parsedCookie.rt)
|
|
|
|
const jwtLifetime = refreshTokenPayload.exp! - refreshTokenPayload.iat!
|
|
|
|
const refLifetime = dateInKiloSeconds(new Date().getTime() + jwtLifetime * 1000)
|
|
|
|
|
|
|
|
const client = new GraphQLClient(gqlUri, {
|
|
|
|
headers: {
|
|
|
|
Authorization: "Bearer " + accessToken,
|
|
|
|
},
|
|
|
|
})
|
|
|
|
const meResponse = await client.rawRequest(gqlToStr(meMutation))
|
|
|
|
|
|
|
|
const refreshTokenResponse = await fetch(refreshTokenUri, {
|
|
|
|
method: "POST",
|
|
|
|
headers: { cookie: cookieHeader },
|
|
|
|
})
|
|
|
|
const jsonResponse = await refreshTokenResponse.json()
|
|
|
|
|
|
|
|
expect(varyHeader).toBe("Origin")
|
|
|
|
expect(acacHeader).toBe("true")
|
|
|
|
expect(acaoHeader).toMatch(/http:/)
|
|
|
|
|
|
|
|
expect(cookieHeader).toMatch(/HttpOnly/)
|
|
|
|
expect(parsedCookie.Path).toBe("/refresh_token")
|
|
|
|
expect(refreshTokenPayload.uid).toBe(user.id)
|
|
|
|
expect(refreshTokenPayload.ver).toBe(user.tokenVersion)
|
|
|
|
expect(refreshTokenPayload.msc).toBeLessThan(1000)
|
|
|
|
expect(refreshCookieExpires).toBeCloseTo(refLifetime)
|
|
|
|
expect(jwtLifetime).toBeGreaterThanOrEqual(halfADay)
|
|
|
|
expect(jwtLifetime).not.toBeGreaterThan(fifteenDays)
|
|
|
|
|
|
|
|
expect(meResponse.data.me.email).toBe("auth@server.com")
|
|
|
|
|
|
|
|
expect(jsonResponse.data).toBeDefined()
|
|
|
|
expect(jsonResponse.errors).toBeUndefined()
|
|
|
|
})
|
|
|
|
|
|
|
|
it("provide an empty rt cookie on signOut mutation", async () => {
|
|
|
|
const signOutReponse = await rawRequest(gqlUri, gqlToStr(signOutMutation))
|
|
|
|
const headers: Headers = signOutReponse.headers
|
|
|
|
const cookieHeader = headers.get("set-cookie") as string
|
|
|
|
const parsedCookie = cookie.parse(cookieHeader)
|
|
|
|
|
|
|
|
expect(cookieHeader).toMatch(/HttpOnly/)
|
|
|
|
expect(parsedCookie.Path).toBe("/refresh_token")
|
|
|
|
expect(parsedCookie.rt).toBe("")
|
|
|
|
})
|
|
|
|
})
|
|
|
|
|
|
|
|
beforeAll(async () => {
|
|
|
|
await createConnection(testingConnectionOptions())
|
|
|
|
await createServer(port)
|
|
|
|
|
|
|
|
await User.delete({ email: "auth@server.com" })
|
|
|
|
user = await User.create({ email: "auth@server.com", password: "password" }).save()
|
|
|
|
})
|
|
|
|
|
|
|
|
afterAll(async () => {
|
|
|
|
await User.delete({ email: "auth@server.com" })
|
|
|
|
})
|
|
|
|
|
|
|
|
const port = 4001
|
|
|
|
const gqlUri = `http://localhost:${port}/graphql`
|
|
|
|
const refreshTokenUri = `http://localhost:${port}/refresh_token`
|
|
|
|
|
|
|
|
const accessTokenMutation = gql`
|
|
|
|
mutation {
|
|
|
|
accessToken(email: "auth@server.com", password: "password")
|
|
|
|
}
|
|
|
|
`
|
|
|
|
|
|
|
|
const meMutation = gql`
|
|
|
|
mutation {
|
|
|
|
me {
|
|
|
|
email
|
|
|
|
}
|
|
|
|
}
|
|
|
|
`
|
|
|
|
|
|
|
|
const signOutMutation = gql`
|
|
|
|
mutation {
|
|
|
|
signOut
|
|
|
|
}
|
|
|
|
`
|
|
|
|
|
|
|
|
const dateInKiloSeconds = (date: string | number) => new Date(date).getTime() / 1000000
|