|
|
|
@ -1,3 +1,5 @@ |
|
|
|
|
// 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" |
|
|
|
@ -5,25 +7,54 @@ import { createConnection } from "typeorm" |
|
|
|
|
import { createServer } from "./server" |
|
|
|
|
import { gqlToStr } from "./server/schema" |
|
|
|
|
import { testingConnectionOptions } from "./server/testing" |
|
|
|
|
import { verifiedRefreshTokenPayload } from "./server/userResolver/auth" |
|
|
|
|
import { |
|
|
|
|
rtCookieOptions, |
|
|
|
|
signRefreshToken, |
|
|
|
|
verifiedRefreshTokenPayload, |
|
|
|
|
} from "./server/userResolver/auth" |
|
|
|
|
import { User } from "./server/userResolver/User" |
|
|
|
|
import cookie = require("cookie") |
|
|
|
|
|
|
|
|
|
let user: User |
|
|
|
|
|
|
|
|
|
describe("server should", () => { |
|
|
|
|
it("perform the refresh tokens operation flawlessly", async () => { |
|
|
|
|
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 createUserResponse = await rawRequest(gqlUri, gqlToStr(createUserMutation)) |
|
|
|
|
const userId = createUserResponse.data.createUser.id |
|
|
|
|
|
|
|
|
|
const accessTokenReponse = await rawRequest(gqlUri, gqlToStr(accessTokenQuery)) |
|
|
|
|
const accessTokenReponse = await rawRequest(gqlUri, gqlToStr(accessTokenMutation)) |
|
|
|
|
const accessToken: string = accessTokenReponse.data.accessToken |
|
|
|
|
const headers: Headers = accessTokenReponse.headers |
|
|
|
|
const cookieHeader = headers.get("set-cookie") as string |
|
|
|
|
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) |
|
|
|
@ -35,7 +66,7 @@ describe("server should", () => { |
|
|
|
|
Authorization: "Bearer " + accessToken, |
|
|
|
|
}, |
|
|
|
|
}) |
|
|
|
|
const meResponse = await client.rawRequest(gqlToStr(meQuery)) |
|
|
|
|
const meResponse = await client.rawRequest(gqlToStr(meMutation)) |
|
|
|
|
|
|
|
|
|
const refreshTokenResponse = await fetch(refreshTokenUri, { |
|
|
|
|
method: "POST", |
|
|
|
@ -46,63 +77,68 @@ describe("server should", () => { |
|
|
|
|
expect(varyHeader).toBe("Origin") |
|
|
|
|
expect(acacHeader).toBe("true") |
|
|
|
|
expect(acaoHeader).toMatch(/http:/) |
|
|
|
|
|
|
|
|
|
expect(cookieHeader).toMatch(/HttpOnly/) |
|
|
|
|
expect(parsedCookie.Path).toBe("/refresh_token") |
|
|
|
|
expect(refreshTokenPayload.userId).toBe(userId) |
|
|
|
|
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("it doesnt perform refresh tokens without valid cookie", async () => { |
|
|
|
|
const response = await fetch(refreshTokenUri, { |
|
|
|
|
method: "POST", |
|
|
|
|
headers: { cookie: "INVALID-COOKIE" }, |
|
|
|
|
}) |
|
|
|
|
const jsonResponse = await response.json() |
|
|
|
|
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(jsonResponse.data).toBeNull() |
|
|
|
|
expect(jsonResponse.errors).not.toBeUndefined() |
|
|
|
|
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 () => { |
|
|
|
|
User.delete({ email: "auth@server.com" }) |
|
|
|
|
await User.delete({ email: "auth@server.com" }) |
|
|
|
|
}) |
|
|
|
|
|
|
|
|
|
const port = 4001 |
|
|
|
|
const gqlUri = `http://localhost:${port}/graphql` |
|
|
|
|
const refreshTokenUri = `http://localhost:${port}/refresh_token` |
|
|
|
|
|
|
|
|
|
const createUserMutation = gql` |
|
|
|
|
const accessTokenMutation = gql` |
|
|
|
|
mutation { |
|
|
|
|
createUser(email: "auth@server.com", password: "password") { |
|
|
|
|
email |
|
|
|
|
id |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
` |
|
|
|
|
|
|
|
|
|
const accessTokenQuery = gql` |
|
|
|
|
query { |
|
|
|
|
accessToken(email: "auth@server.com", password: "password") |
|
|
|
|
} |
|
|
|
|
` |
|
|
|
|
|
|
|
|
|
const meQuery = gql` |
|
|
|
|
query { |
|
|
|
|
const meMutation = gql` |
|
|
|
|
mutation { |
|
|
|
|
me { |
|
|
|
|
email |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
` |
|
|
|
|
|
|
|
|
|
const signOutMutation = gql` |
|
|
|
|
mutation { |
|
|
|
|
signOut |
|
|
|
|
} |
|
|
|
|
` |
|
|
|
|
|
|
|
|
|
const dateInKiloSeconds = (date: string | number) => new Date(date).getTime() / 1000000 |
|
|
|
|