set up auth on the server + test utils
This commit is contained in:
parent
f3013c2956
commit
12b87ee7d1
7 changed files with 166 additions and 10 deletions
|
@ -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",
|
||||||
|
|
|
@ -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",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -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}>
|
||||||
|
|
|
@ -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),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
|
@ -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 []`);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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 };
|
||||||
|
|
|
@ -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==
|
||||||
|
|
Loading…
Reference in a new issue