From 12b87ee7d1eb1d14217356f174aa8dad91854aa8 Mon Sep 17 00:00:00 2001 From: Matchu Date: Wed, 2 Sep 2020 23:00:16 -0700 Subject: [PATCH] set up auth on the server + test utils --- package.json | 2 + scripts/export-users-to-auth0.js | 4 +- src/app/App.js | 2 + src/server/index.js | 70 ++++++++++++++++++++++++++++- src/server/query-tests/User.test.js | 56 ++++++++++++++++++++++- src/server/query-tests/setup.js | 40 +++++++++++++++-- yarn.lock | 2 +- 7 files changed, 166 insertions(+), 10 deletions(-) diff --git a/package.json b/package.json index 71e1090..762f05e 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,8 @@ "honeycomb-beeline": "^2.2.0", "immer": "^6.0.3", "jimp": "^0.14.0", + "jsonwebtoken": "^8.5.1", + "jwks-rsa": "^1.9.0", "mysql2": "^2.1.0", "node-fetch": "^2.6.0", "react": "^16.13.1", diff --git a/scripts/export-users-to-auth0.js b/scripts/export-users-to-auth0.js index 865a4d1..cbafe03 100644 --- a/scripts/export-users-to-auth0.js +++ b/scripts/export-users-to-auth0.js @@ -17,8 +17,8 @@ const { normalizeRow } = require("../src/server/util"); const auth0 = new ManagementClient({ domain: "openneo.us.auth0.com", - clientId: process.env.AUTH0_CLIENT_ID, - clientSecret: process.env.AUTH0_CLIENT_SECRET, + clientId: process.env.AUTH0_SUPPORT_CLIENT_ID, + clientSecret: process.env.AUTH0_SUPPORT_CLIENT_SECRET, scope: "read:users update:users", }); diff --git a/src/app/App.js b/src/app/App.js index 4ea93c9..2d5c4bc 100644 --- a/src/app/App.js +++ b/src/app/App.js @@ -36,6 +36,8 @@ function App() { domain="openneo.us.auth0.com" clientId="8LjFauVox7shDxVufQqnviUIywMuuC4r" redirectUri={window.location.origin} + audience="https://impress-2020.openneo.net/api" + scope="" > diff --git a/src/server/index.js b/src/server/index.js index 20d7474..c0f7253 100644 --- a/src/server/index.js +++ b/src/server/index.js @@ -1,5 +1,9 @@ -const { gql, makeExecutableSchema } = require("apollo-server"); +const util = require("util"); + const { addBeelineToSchema, beelinePlugin } = require("./lib/beeline-graphql"); +const { gql, makeExecutableSchema } = require("apollo-server"); +const jwtVerify = util.promisify(require("jsonwebtoken").verify); +const jwksClient = require("jwks-rsa"); const connectToDb = require("./db"); const buildLoaders = require("./loaders"); @@ -262,6 +266,7 @@ const typeDefs = gql` species(id: ID!): Species user(id: ID!): User + currentUser: User petOnNeopetsDotCom(petName: String!): Outfit } @@ -714,6 +719,23 @@ const resolvers = { return { id }; }, + currentUser: async (_, __, { currentUserId, userLoader }) => { + if (currentUserId == null) { + return null; + } + + try { + const user = await userLoader.load(currentUserId); + } catch (e) { + if (e.message.includes("could not find user")) { + return null; + } else { + throw e; + } + } + + return { id: currentUserId }; + }, petOnNeopetsDotCom: async (_, { petName }) => { const [petMetaData, customPetData] = await Promise.all([ neopets.loadPetMetaData(petName), @@ -1260,9 +1282,49 @@ if (process.env["NODE_ENV"] !== "test") { plugins.push(beelinePlugin); } +const jwks = jwksClient({ + jwksUri: "https://openneo.us.auth0.com/.well-known/jwks.json", +}); + +async function getJwtKey(header, callback) { + jwks.getSigningKey(header.kid, (err, key) => { + if (err) { + return callback(null, signingKey); + } + const signingKey = key.publicKey || key.rsaPublicKey; + callback(null, signingKey); + }); +} + +async function getUserIdFromToken(token) { + if (!token) { + return null; + } + + let payload; + try { + payload = await jwtVerify(token, getJwtKey, { + audience: "https://impress-2020.openneo.net/api", + issuer: "https://openneo.us.auth0.com/", + algorithms: ["RS256"], + }); + } catch (e) { + console.error(`Invalid auth token: ${token}\n${e}`); + return null; + } + + const userId = payload.sub.match(/auth0\|impress-([0-9]+)/)?.[1]; + if (!userId) { + console.log("Unexpected auth token sub format", payload.sub); + return null; + } + + return userId; +} + const config = { schema, - context: async () => { + context: async ({ req }) => { const db = await connectToDb(); const svgLogger = { @@ -1273,9 +1335,13 @@ const config = { }; lastSvgLogger = svgLogger; + const token = req.headers.authorization?.match(/^Bearer (.+)$/)?.[1]; + const currentUserId = await getUserIdFromToken(token); + return { svgLogger, db, + currentUserId, ...buildLoaders(db), }; }, diff --git a/src/server/query-tests/User.test.js b/src/server/query-tests/User.test.js index 87b2948..71753e0 100644 --- a/src/server/query-tests/User.test.js +++ b/src/server/query-tests/User.test.js @@ -1,5 +1,5 @@ const gql = require("graphql-tag"); -const { query, getDbCalls } = require("./setup.js"); +const { query, getDbCalls, logInAsTestUser } = require("./setup.js"); describe("User", () => { it("looks up a user", async () => { @@ -48,7 +48,7 @@ describe("User", () => { }); expect(res).toHaveNoErrors(); - expect(res.data.user).toBe(null); + expect(res.data).toEqual({ user: null }); expect(getDbCalls()).toMatchInlineSnapshot(` Array [ Array [ @@ -60,4 +60,56 @@ describe("User", () => { ] `); }); + + it("gets current user, if logged in", async () => { + await logInAsTestUser(); + + const res = await query({ + query: gql` + query { + currentUser { + id + username + } + } + `, + }); + + expect(res).toHaveNoErrors(); + expect(res.data).toMatchInlineSnapshot(` + Object { + "currentUser": Object { + "id": "44743", + "username": "dti-test", + }, + } + `); + expect(getDbCalls()).toMatchInlineSnapshot(` + Array [ + Array [ + "SELECT * FROM users WHERE id IN (?)", + Array [ + "44743", + ], + ], + ] + `); + }); + + it("gets no user, if logged out", async () => { + const res = await query({ + query: gql` + query { + currentUser { + id + username + } + } + `, + }); + + expect(res).toHaveNoErrors(); + expect(res.data).toEqual({ currentUser: null }); + expect(getDbCalls()).toMatchInlineSnapshot(`Array []`); + }); }); diff --git a/src/server/query-tests/setup.js b/src/server/query-tests/setup.js index 9a2e191..f518bfc 100644 --- a/src/server/query-tests/setup.js +++ b/src/server/query-tests/setup.js @@ -1,11 +1,28 @@ const { ApolloServer } = require("apollo-server"); const { createTestClient } = require("apollo-server-testing"); +const { AuthenticationClient } = require("auth0"); const connectToDb = require("../db"); const actualConnectToDb = jest.requireActual("../db"); const { config } = require("../index"); -const { query } = createTestClient(new ApolloServer(config)); +let accessTokenForQueries = null; + +const { query } = createTestClient( + new ApolloServer({ + ...config, + context: () => + config.context({ + req: { + headers: { + authorization: accessTokenForQueries + ? `Bearer ${accessTokenForQueries}` + : undefined, + }, + }, + }), + }) +); // Spy on db.execute, so we can snapshot the queries we run. This can help us // keep an eye on perf - watch for tests with way too many queries! @@ -19,7 +36,8 @@ beforeAll(() => { return db; }); }); -afterEach(() => { +beforeEach(() => { + accessTokenForQueries = null; if (dbExecuteFn) { dbExecuteFn.mockClear(); } @@ -31,6 +49,22 @@ afterAll(() => { }); const getDbCalls = () => (dbExecuteFn ? dbExecuteFn.mock.calls : []); +async function logInAsTestUser() { + const auth0 = new AuthenticationClient({ + domain: "openneo.us.auth0.com", + clientId: process.env.AUTH0_TEST_CLIENT_ID, + clientSecret: process.env.AUTH0_TEST_CLIENT_SECRET, + }); + + const res = await auth0.passwordGrant({ + username: "dti-test", + password: process.env.DTI_TEST_USER_PASSWORD, + audience: "https://impress-2020.openneo.net/api", + }); + + accessTokenForQueries = res.access_token; +} + // Add a new `expect(res).toHaveNoErrors()` to call after GraphQL calls! expect.extend({ toHaveNoErrors(res) { @@ -49,4 +83,4 @@ expect.extend({ }, }); -module.exports = { query, getDbCalls }; +module.exports = { query, getDbCalls, logInAsTestUser }; diff --git a/yarn.lock b/yarn.lock index 3b4b338..256ec87 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8547,7 +8547,7 @@ jwa@^1.4.1: ecdsa-sig-formatter "1.0.11" safe-buffer "^5.0.1" -jwks-rsa@^1.8.0: +jwks-rsa@^1.8.0, jwks-rsa@^1.9.0: version "1.9.0" resolved "https://registry.yarnpkg.com/jwks-rsa/-/jwks-rsa-1.9.0.tgz#efa0cd550c13b70397e27cd8764bd53c45a1ad91" integrity sha512-UPCfQQg0s2kF2Ju6UFJrQH73f7MaVN/hKBnYBYOp+X9KN4y6TLChhLtaXS5nRKbZqshwVdrZ9OY63m/Q9CLqcg==