set up auth on the server + test utils

This commit is contained in:
Emi Matchu 2020-09-02 23:00:16 -07:00
parent f3013c2956
commit 12b87ee7d1
7 changed files with 166 additions and 10 deletions

View file

@ -24,6 +24,8 @@
"honeycomb-beeline": "^2.2.0", "honeycomb-beeline": "^2.2.0",
"immer": "^6.0.3", "immer": "^6.0.3",
"jimp": "^0.14.0", "jimp": "^0.14.0",
"jsonwebtoken": "^8.5.1",
"jwks-rsa": "^1.9.0",
"mysql2": "^2.1.0", "mysql2": "^2.1.0",
"node-fetch": "^2.6.0", "node-fetch": "^2.6.0",
"react": "^16.13.1", "react": "^16.13.1",

View file

@ -17,8 +17,8 @@ const { normalizeRow } = require("../src/server/util");
const auth0 = new ManagementClient({ const auth0 = new ManagementClient({
domain: "openneo.us.auth0.com", domain: "openneo.us.auth0.com",
clientId: process.env.AUTH0_CLIENT_ID, clientId: process.env.AUTH0_SUPPORT_CLIENT_ID,
clientSecret: process.env.AUTH0_CLIENT_SECRET, clientSecret: process.env.AUTH0_SUPPORT_CLIENT_SECRET,
scope: "read:users update:users", scope: "read:users update:users",
}); });

View file

@ -36,6 +36,8 @@ function App() {
domain="openneo.us.auth0.com" domain="openneo.us.auth0.com"
clientId="8LjFauVox7shDxVufQqnviUIywMuuC4r" clientId="8LjFauVox7shDxVufQqnviUIywMuuC4r"
redirectUri={window.location.origin} redirectUri={window.location.origin}
audience="https://impress-2020.openneo.net/api"
scope=""
> >
<ApolloProvider client={apolloClient}> <ApolloProvider client={apolloClient}>
<ChakraProvider theme={theme}> <ChakraProvider theme={theme}>

View file

@ -1,5 +1,9 @@
const { gql, makeExecutableSchema } = require("apollo-server"); const util = require("util");
const { addBeelineToSchema, beelinePlugin } = require("./lib/beeline-graphql"); 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 connectToDb = require("./db");
const buildLoaders = require("./loaders"); const buildLoaders = require("./loaders");
@ -262,6 +266,7 @@ const typeDefs = gql`
species(id: ID!): Species species(id: ID!): Species
user(id: ID!): User user(id: ID!): User
currentUser: User
petOnNeopetsDotCom(petName: String!): Outfit petOnNeopetsDotCom(petName: String!): Outfit
} }
@ -714,6 +719,23 @@ const resolvers = {
return { id }; 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 }) => { petOnNeopetsDotCom: async (_, { petName }) => {
const [petMetaData, customPetData] = await Promise.all([ const [petMetaData, customPetData] = await Promise.all([
neopets.loadPetMetaData(petName), neopets.loadPetMetaData(petName),
@ -1260,9 +1282,49 @@ if (process.env["NODE_ENV"] !== "test") {
plugins.push(beelinePlugin); 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 = { const config = {
schema, schema,
context: async () => { context: async ({ req }) => {
const db = await connectToDb(); const db = await connectToDb();
const svgLogger = { const svgLogger = {
@ -1273,9 +1335,13 @@ const config = {
}; };
lastSvgLogger = svgLogger; lastSvgLogger = svgLogger;
const token = req.headers.authorization?.match(/^Bearer (.+)$/)?.[1];
const currentUserId = await getUserIdFromToken(token);
return { return {
svgLogger, svgLogger,
db, db,
currentUserId,
...buildLoaders(db), ...buildLoaders(db),
}; };
}, },

View file

@ -1,5 +1,5 @@
const gql = require("graphql-tag"); const gql = require("graphql-tag");
const { query, getDbCalls } = require("./setup.js"); const { query, getDbCalls, logInAsTestUser } = require("./setup.js");
describe("User", () => { describe("User", () => {
it("looks up a user", async () => { it("looks up a user", async () => {
@ -48,7 +48,7 @@ describe("User", () => {
}); });
expect(res).toHaveNoErrors(); expect(res).toHaveNoErrors();
expect(res.data.user).toBe(null); expect(res.data).toEqual({ user: null });
expect(getDbCalls()).toMatchInlineSnapshot(` expect(getDbCalls()).toMatchInlineSnapshot(`
Array [ Array [
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 []`);
});
}); });

View file

@ -1,11 +1,28 @@
const { ApolloServer } = require("apollo-server"); const { ApolloServer } = require("apollo-server");
const { createTestClient } = require("apollo-server-testing"); const { createTestClient } = require("apollo-server-testing");
const { AuthenticationClient } = require("auth0");
const connectToDb = require("../db"); const connectToDb = require("../db");
const actualConnectToDb = jest.requireActual("../db"); const actualConnectToDb = jest.requireActual("../db");
const { config } = require("../index"); 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 // 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! // keep an eye on perf - watch for tests with way too many queries!
@ -19,7 +36,8 @@ beforeAll(() => {
return db; return db;
}); });
}); });
afterEach(() => { beforeEach(() => {
accessTokenForQueries = null;
if (dbExecuteFn) { if (dbExecuteFn) {
dbExecuteFn.mockClear(); dbExecuteFn.mockClear();
} }
@ -31,6 +49,22 @@ afterAll(() => {
}); });
const getDbCalls = () => (dbExecuteFn ? dbExecuteFn.mock.calls : []); 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! // Add a new `expect(res).toHaveNoErrors()` to call after GraphQL calls!
expect.extend({ expect.extend({
toHaveNoErrors(res) { toHaveNoErrors(res) {
@ -49,4 +83,4 @@ expect.extend({
}, },
}); });
module.exports = { query, getDbCalls }; module.exports = { query, getDbCalls, logInAsTestUser };

View file

@ -8547,7 +8547,7 @@ jwa@^1.4.1:
ecdsa-sig-formatter "1.0.11" ecdsa-sig-formatter "1.0.11"
safe-buffer "^5.0.1" safe-buffer "^5.0.1"
jwks-rsa@^1.8.0: jwks-rsa@^1.8.0, jwks-rsa@^1.9.0:
version "1.9.0" version "1.9.0"
resolved "https://registry.yarnpkg.com/jwks-rsa/-/jwks-rsa-1.9.0.tgz#efa0cd550c13b70397e27cd8764bd53c45a1ad91" resolved "https://registry.yarnpkg.com/jwks-rsa/-/jwks-rsa-1.9.0.tgz#efa0cd550c13b70397e27cd8764bd53c45a1ad91"
integrity sha512-UPCfQQg0s2kF2Ju6UFJrQH73f7MaVN/hKBnYBYOp+X9KN4y6TLChhLtaXS5nRKbZqshwVdrZ9OY63m/Q9CLqcg== integrity sha512-UPCfQQg0s2kF2Ju6UFJrQH73f7MaVN/hKBnYBYOp+X9KN4y6TLChhLtaXS5nRKbZqshwVdrZ9OY63m/Q9CLqcg==