Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Becoming a “Secret” Agent: Securing Your GraphQL Backend with JWTs

Sam Julien
February 22, 2020

Becoming a “Secret” Agent: Securing Your GraphQL Backend with JWTs

Are you confused about how authentication and authorization relate to your GraphQL API? You’re not alone! It’s no secret that learning auth is hard enough on its own, let alone while trying to understand how it fits with GraphQL. Let’s demystify authentication with some practical teaching on how to use JSON Web Tokens (JWTs) to add authentication to your GraphQL APIs! We’ll first shed light on some auth terminology and talk about the problem of delegated access. We’ll also discuss options to consider when choosing an authorization strategy and whether any considerations need to be made for using your GraphQL backend with a frontend on the same server versus on an external server along with multiple APIs. We’ll then step through how to handle authorization in your GraphQL server, including several options for handling access to protected data. This talk will use JavaScript examples, but the principles will apply to other backend technologies. By the end, you’ll feel a whole lot better about tackling auth in GraphQL!

Sam Julien

February 22, 2020
Tweet

More Decks by Sam Julien

Other Decks in Technology

Transcript

  1. Becoming a “Secret” Agent
    Securing Your GraphQL Backend with JWTs

    View Slide

  2. @samjulien

    View Slide

  3. Auth in GraphQL can be confusing.
    @samjulien

    View Slide

  4. app.get("/api/super-secret", checkAuth, function(req, res) {
    res.json({
    message: "Shhh! Very secret!"
    });
    });
    @samjulien

    View Slide

  5. app.get("/api/super-secret", checkAuth, function(req, res) {
    res.json({
    message: "Shhh! Very secret!"
    });
    });
    @samjulien

    View Slide

  6. app.use(
    "/graphql",
    graphqlHTTP({
    schema: MyGraphQLSchema,
    graphiql: true
    })
    );
    @samjulien

    View Slide

  7. app.use(
    "/graphql",
    graphqlHTTP({
    schema: MyGraphQLSchema,
    graphiql: true
    })
    );

    @samjulien

    View Slide

  8. Sam Julien
    @samjulien

    View Slide

  9. Sam Julien
    @samjulien
    Senior Developer Advocate Engineer at Auth0

    View Slide

  10. Sam Julien
    @samjulien
    Senior Developer Advocate Engineer at Auth0
    GDE & Angular Collaborator

    View Slide

  11. Sam Julien
    @samjulien
    Senior Developer Advocate Engineer at Auth0
    GDE & Angular Collaborator
    UpgradingAngularJS.com, Thinkster, egghead

    View Slide

  12. @samjulien

    View Slide

  13. @samjulien

    View Slide

  14. Some Auth Background
    @samjulien

    View Slide

  15. Some Auth Background
    The What & Why of JWTs
    @samjulien

    View Slide

  16. Some Auth Background
    The What & Why of JWTs
    Authorization in GraphQL
    @samjulien

    View Slide

  17. Some Auth Background

    View Slide

  18. Authentication & Authorization
    @samjulien

    View Slide

  19. Authentication
    @samjulien

    View Slide

  20. Authentication
    Are you who you say you are?
    @samjulien

    View Slide

  21. Authorization
    @samjulien

    View Slide

  22. Authorization
    Do you have permission to access resources?
    @samjulien

    View Slide

  23. @samjulien

    View Slide

  24. @samjulien

    View Slide

  25. @samjulien

    View Slide

  26. The GraphQL server doesn’t necessarily
    care about users proving who they are
    — it cares who has access to what.
    @samjulien

    View Slide

  27. Access
    @samjulien

    View Slide

  28. Backend + Frontend on the Same Server
    @samjulien

    View Slide

  29. GraphQL + NextJS on the Same Server
    @samjulien

    View Slide

  30. GraphQL + NextJS on the Same Server
    @samjulien

    View Slide

  31. GraphQL + NextJS on the Same Server
    @samjulien

    View Slide

  32. GraphQL + NextJS on the Same Server
    @samjulien

    View Slide

  33. GraphQL + NextJS on the Same Server
    @samjulien

    View Slide

  34. GraphQL + NextJS on the Same Server
    @samjulien

    View Slide

  35. GraphQL + NextJS on the Same Server
    @samjulien

    View Slide

  36. @samjulien

    View Slide

  37. @samjulien

    View Slide

  38. Access
    @samjulien

    View Slide

  39. Delegated Access
    @samjulien

    View Slide

  40. GraphQL + NextJS on the Same Server
    @samjulien

    View Slide

  41. @samjulien

    View Slide

  42. @samjulien

    View Slide

  43. We need something other
    than a cookie for this…
    @samjulien

    View Slide

  44. @samjulien

    View Slide

  45. Can contain useful information
    @samjulien

    View Slide

  46. Can contain useful information
    ✅ Can be signed and verified
    @samjulien

    View Slide

  47. …but what is that thing and
    how do we (safely) create it?
    @samjulien

    View Slide

  48. Token
    @samjulien

    View Slide

  49. Authorization Server
    @samjulien

    View Slide

  50. Helps make access control decisions in your app or API.
    Authorization Server
    @samjulien

    View Slide

  51. Access Token
    @samjulien

    View Slide

  52. Access Token
    Informs the API the bearer has been authorized.
    @samjulien

    View Slide

  53. @samjulien

    View Slide

  54. @samjulien

    View Slide

  55. @samjulien

    View Slide

  56. @samjulien

    View Slide

  57. @samjulien

    View Slide

  58. Access Token
    @samjulien

    View Slide

  59. Authorization: Bearer
    @samjulien

    View Slide

  60. @samjulien

    View Slide

  61. Can contain useful information
    @samjulien

    View Slide

  62. Can contain useful information
    ✅ Can be signed and verified
    @samjulien

    View Slide

  63. JSON Web Tokens (JWTs)

    View Slide

  64. eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxM
    jM0NTY3ODkwIiwibmFtZSI6IkhlbGxvIEdyYXBoUUwiLCJlbWF
    pbCI6ImhlbGxvQGdyYXBocWwuY29tIiwiaWF0IjoxNTE2MjM5M
    DIyfQ.cJutaCScQJXsGTL6ynH7BRMnQj_P2yl0a58jUhnLDq8
    @samjulien

    View Slide

  65. {
    "sub": "1234567890",
    "name": "Hello GraphQL",
    "email": "[email protected]",
    "iat": 1516239022
    }

    @samjulien

    View Slide

  66. {
    "sub": "1234567890",
    "name": "Hello GraphQL",
    "email": "[email protected]",
    "iat": 1516239022
    }

    @samjulien

    View Slide

  67. {
    "sub": "1234567890",
    "name": "Hello GraphQL",
    "email": "[email protected]",
    "iat": 1516239022
    }

    @samjulien

    View Slide

  68. {
    "sub": "1234567890",
    "name": "Hello GraphQL",
    "email": "[email protected]",
    "iat": 1516239022
    }

    eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
    @samjulien

    View Slide

  69. eyJhbGciOiJIUzI1NiIsInR5c
    CI6IkpXVCJ9.eyJzdWIiOiIxM
    jM0NTY3ODkwIiwibmFtZSI6Ik
    hlbGxvIEdyYXBoUUwiLCJlbWF
    pbCI6ImhlbGxvQGdyYXBocWwu
    Y29tIiwiaWF0IjoxNTE2MjM5M
    DIyfQ.cJutaCScQJXsGTL6ynH
    7BRMnQj_P2yl0a58jUhnLDq8
    Header (Algorithm and Token Type)
    {
    "alg": "HS256",
    "typ": "JWT"
    }
    @samjulien

    View Slide

  70. eyJhbGciOiJIUzI1NiIsInR5c
    CI6IkpXVCJ9.eyJzdWIiOiIxM
    jM0NTY3ODkwIiwibmFtZSI6Ik
    hlbGxvIEdyYXBoUUwiLCJlbWF
    pbCI6ImhlbGxvQGdyYXBocWwu
    Y29tIiwiaWF0IjoxNTE2MjM5M
    DIyfQ.cJutaCScQJXsGTL6ynH
    7BRMnQj_P2yl0a58jUhnLDq8
    Payload (Data and Claims)
    {
    "sub": "1234567890",
    "name": "Hello GraphQL",
    "email": "[email protected]",
    "iat": 1516239022
    }
    @samjulien

    View Slide

  71. eyJhbGciOiJIUzI1NiIsInR5c
    CI6IkpXVCJ9.eyJzdWIiOiIxM
    jM0NTY3ODkwIiwibmFtZSI6Ik
    hlbGxvIEdyYXBoUUwiLCJlbWF
    pbCI6ImhlbGxvQGdyYXBocWwu
    Y29tIiwiaWF0IjoxNTE2MjM5M
    DIyfQ.cJutaCScQJXsGTL6ynH
    7BRMnQj_P2yl0a58jUhnLDq8
    Verify Signature
    x Signature ✍
    @samjulien

    View Slide

  72. @samjulien

    View Slide

  73. Can contain useful information
    @samjulien

    View Slide

  74. Can contain useful information
    ✅ Can be signed and verified
    @samjulien

    View Slide

  75. {
    "sub": "1234567890",
    "name": “Hello GraphQL”,
    "iat": 1516239022,
    "https://hasura.io/jwt/claims": {
    "x-hasura-allowed-roles": ["editor","user", "mod"],
    "x-hasura-default-role": "user",
    "x-hasura-user-id": "1234567890",
    "x-hasura-org-id": "123",
    "x-hasura-custom": "custom-value"
    }
    }
    @samjulien

    View Slide

  76. {
    "sub": "1234567890",
    "name": “Hello GraphQL”,
    "iat": 1516239022,
    "https://hasura.io/jwt/claims": {
    "x-hasura-allowed-roles": ["editor","user", "mod"],
    "x-hasura-default-role": "user",
    "x-hasura-user-id": "1234567890",
    "x-hasura-org-id": "123",
    "x-hasura-custom": "custom-value"
    }
    }
    @samjulien

    View Slide

  77. @samjulien

    View Slide

  78. @samjulien

    View Slide

  79. @samjulien

    View Slide

  80. @samjulien

    View Slide

  81. Your user management will (likely)
    live outside of your GraphQL server.
    @samjulien

    View Slide

  82. Should you just build your
    own Authorization Server?
    @samjulien

    View Slide

  83. How will you be sure you…
    @samjulien

    View Slide

  84. How will you be sure you…
    @samjulien

    View Slide

  85. How will you be sure you…
    Implement proper password controls?
    Implement secure password recovery mechanisms?
    Transmit passwords securely?
    Correctly implement authentication and error messages?
    Prevent brute force attacks?
    @samjulien

    View Slide

  86. How will you be sure you…
    Implement proper password controls?
    Implement secure password recovery mechanisms?
    Transmit passwords securely?
    Correctly implement authentication and error messages?
    Prevent brute force attacks?

    @samjulien

    View Slide

  87. Consider outsourcing this
    piece of your app.
    @samjulien

    View Slide

  88. @samjulien

    View Slide

  89. @samjulien

    View Slide

  90. Authorization in GraphQL

    View Slide

  91. Authorization: Bearer xxx.yyy.zzz
    @samjulien

    View Slide

  92. Your GraphQL server needs to verify
    the access token with a public key.
    @samjulien

    View Slide

  93. You can then parse claims for
    authorization.
    @samjulien

    View Slide

  94. So how do we do that?
    @samjulien

    View Slide

  95. First, grab the token from the
    request and add it to context.
    @samjulien

    View Slide

  96. const server = new ApolloServer({
    typeDefs: schema,
    resolvers,
    context: async ({ req }) => {
    const token = req.headers["Authorization"];
    return { token };
    }
    });
    @samjulien

    View Slide

  97. const server = new ApolloServer({
    typeDefs: schema,
    resolvers,
    context: async ({ req }) => {
    const token = req.headers["Authorization"];
    return { token };
    }
    });
    @samjulien

    View Slide

  98. We still need to verify the token.
    @samjulien

    View Slide

  99. verifyToken(token): Payload
    @samjulien

    View Slide

  100. Authorization: Bearer xxx.yyy.zzz
    @samjulien

    View Slide

  101. const bearerToken = token.split(" ")[1];
    @samjulien

    View Slide

  102. eyJhbGciOiJIUzI1NiIsInR5c
    CI6IkpXVCJ9.eyJzdWIiOiIxM
    jM0NTY3ODkwIiwibmFtZSI6Ik
    hlbGxvIEdyYXBoUUwiLCJlbWF
    pbCI6ImhlbGxvQGdyYXBocWwu
    Y29tIiwiaWF0IjoxNTE2MjM5M
    DIyfQ.cJutaCScQJXsGTL6ynH
    7BRMnQj_P2yl0a58jUhnLDq8
    Verify Signature
    x Signature ✍
    @samjulien

    View Slide

  103. const client = jwksClient({
    jwksUri: `https://${process.env.AUTH_DOMAIN}/.well-known/jwks.json`
    });
    @samjulien

    View Slide

  104. const client = jwksClient({
    jwksUri: `https://${process.env.AUTH_DOMAIN}/.well-known/jwks.json`
    });
    @samjulien

    View Slide

  105. function getJwksClientKey(header, callback) {
    client.getSigningKey(header.kid, function(error, key) {
    const signingKey = key.publicKey || key.rsaPublicKey;
    callback(null, signingKey);
    });
    }
    @samjulien

    View Slide

  106. function getJwksClientKey(header, callback) {
    client.getSigningKey(header.kid, function(error, key) {
    const signingKey = key.publicKey || key.rsaPublicKey;
    callback(null, signingKey);
    });
    }
    @samjulien

    View Slide

  107. jwt.verify(bearerToken, getJwksClientKey, {
    audience: process.env.API_IDENTIFIER,
    issuer: `https://${process.env.AUTH_DOMAIN}/`,
    algorithms: ["RS256"]
    });
    @samjulien

    View Slide

  108. jwt.verify(bearerToken, getJwksClientKey, {
    audience: process.env.API_IDENTIFIER,
    issuer: `https://${process.env.AUTH_DOMAIN}/`,
    algorithms: ["RS256"]
    });
    @samjulien

    View Slide

  109. jwt.verify(bearerToken, getJwksClientKey, {
    audience: process.env.API_IDENTIFIER,
    issuer: `https://${process.env.AUTH_DOMAIN}/`,
    algorithms: ["RS256"]
    });
    @samjulien

    View Slide

  110. try {
    const payload = await jwt.verify(...);
    return payload;
    } catch (error) {
    throw new Error("Invalid token!");
    }
    @samjulien

    View Slide

  111. try {
    const payload = await jwt.verify(...);
    return payload;
    } catch (error) {
    throw new Error("Invalid token!");
    }
    @samjulien

    View Slide

  112. verifyToken(token): Payload
    @samjulien

    View Slide

  113. Where do we use this?
    @samjulien

    View Slide

  114. Inside of resolvers?
    @samjulien

    View Slide

  115. Back in Context
    @samjulien

    View Slide

  116. const createContext = async req => {
    const db = await startDatabase();
    let token = null;
    let currentUser = null;
    try {
    token = req.headers["Authorization"];
    if (token) {
    const payload = await verifyToken(token);
    currentUser = await db.query.users.where({ id: payload.sub });
    }
    } catch (error) {
    throw new Error("Unable to authenticate.");
    }
    return { db, token, currentUser };
    }; @samjulien

    View Slide

  117. const createContext = async req => {
    const db = await startDatabase();
    let token = null;
    let currentUser = null;
    try {
    token = req.headers["Authorization"];
    if (token) {
    const payload = await verifyToken(token);
    currentUser = await db.query.users.where({ id: payload.sub });
    }
    } catch (error) {
    throw new Error("Unable to authenticate.");
    }
    return { db, token, currentUser };
    }; @samjulien

    View Slide

  118. const createContext = async req => {
    const db = await startDatabase();
    let token = null;
    let currentUser = null;
    try {
    token = req.headers["Authorization"];
    if (token) {
    const payload = await verifyToken(token);
    currentUser = await db.query.users.where({ id: payload.sub });
    }
    } catch (error) {
    throw new Error("Unable to authenticate.");
    }
    return { db, token, currentUser };
    }; @samjulien

    View Slide

  119. const createContext = async req => {
    const db = await startDatabase();
    let token = null;
    let currentUser = null;
    try {
    token = req.headers["Authorization"];
    if (token) {
    const payload = await verifyToken(token);
    currentUser = await db.query.users.where({ id: payload.sub });
    }
    } catch (error) {
    throw new Error("Unable to authenticate.");
    }
    return { db, token, currentUser };
    }; @samjulien

    View Slide

  120. const createContext = async req => {
    const db = await startDatabase();
    let token = null;
    let currentUser = null;
    try {
    token = req.headers["Authorization"];
    if (token) {
    const payload = await verifyToken(token);
    currentUser = await db.query.users.where({ id: payload.sub });
    }
    } catch (error) {
    throw new Error("Unable to authenticate.");
    }
    return { db, token, currentUser };
    }; @samjulien

    View Slide

  121. const createContext = async req => {
    const db = await startDatabase();
    let token = null;
    let currentUser = null;
    try {
    token = req.headers["Authorization"];
    if (token) {
    const payload = await verifyToken(token);
    currentUser = await db.query.users.where({ id: payload.sub });
    }
    } catch (error) {
    throw new Error("Unable to authenticate.");
    }
    return { db, token, currentUser };
    }; @samjulien

    View Slide

  122. const createContext = async req => {
    const db = await startDatabase();
    let token = null;
    let currentUser = null;
    try {
    token = req.headers["Authorization"];
    if (token) {
    const payload = await verifyToken(token);
    currentUser = await db.query.users.where({ id: payload.sub });
    }
    } catch (error) {
    throw new Error("Unable to authenticate.");
    }
    return { db, token, currentUser };
    }; @samjulien

    View Slide

  123. const createContext = async req => {
    const db = await startDatabase();
    let token = null;
    let currentUser = null;
    try {
    token = req.headers["Authorization"];
    if (token) {
    const payload = await verifyToken(token);
    currentUser = await db.query.users.where({ id: payload.sub });
    }
    } catch (error) {
    throw new Error("Unable to authenticate.");
    }
    return { db, token, currentUser };
    }; @samjulien

    View Slide

  124. const server = new ApolloServer({
    typeDefs: schema,
    resolvers,
    context: async ({ req }) => createContext(req)
    });
    @samjulien

    View Slide

  125. const server = new ApolloServer({
    typeDefs: schema,
    resolvers,
    context: async ({ req }) => createContext(req)
    });
    @samjulien

    View Slide

  126. createEvent: async (parent, args, context, info) => {
    if (!context.currentUser) {
    throw new Error("Must be logged in for this!");
    }
    const { currentUser } = context;
    if (hasPermissions(currentUser.permissions, PERMISSIONS.CREATE)) {
    return await createEventInDb({
    ...args,
    ...context
    });
    }
    };
    @samjulien

    View Slide

  127. createEvent: async (parent, args, context, info) => {
    if (!context.currentUser) {
    throw new Error("Must be logged in for this!");
    }
    const { currentUser } = context;
    if (hasPermissions(currentUser.permissions, PERMISSIONS.CREATE)) {
    return await createEventInDb({
    ...args,
    ...context
    });
    }
    };
    @samjulien

    View Slide

  128. createEvent: async (parent, args, context, info) => {
    if (!context.currentUser) {
    throw new Error("Must be logged in for this!");
    }
    const { currentUser } = context;
    if (hasPermissions(currentUser.permissions, PERMISSIONS.CREATE)) {
    return await createEventInDb({
    ...args,
    ...context
    });
    }
    };
    @samjulien

    View Slide

  129. Wrapping (Composing) Resolvers
    @samjulien

    View Slide

  130. export const isAuthenticated = next => (parent, args, context, info) => {
    if (!context.currentUser) {
    throw new Error("You must be authenticated!");
    }
    return next(parent, args, context, info);
    };
    @samjulien

    View Slide

  131. export const isAuthenticated = next => (parent, args, context, info) => {
    if (!context.currentUser) {
    throw new Error("You must be authenticated!");
    }
    return next(parent, args, context, info);
    };
    @samjulien

    View Slide

  132. export const isAuthenticated = next => (parent, args, context, info) => {
    if (!context.currentUser) {
    throw new Error("You must be authenticated!");
    }
    return next(parent, args, context, info);
    };
    @samjulien

    View Slide

  133. export const isAuthenticated = next => (parent, args, context, info) => {
    if (!context.currentUser) {
    throw new Error("You must be authenticated!");
    }
    return next(parent, args, context, info);
    };
    @samjulien

    View Slide

  134. createEvent: isAuthenticated(async (parent, args, context, info) => {
    const { currentUser } = context;
    if (hasPermissions(currentUser.permissions, PERMISSIONS.CREATE)) {
    return await createEventInDb({ ...args, ...context });
    }
    });
    @samjulien

    View Slide

  135. createEvent: isAuthenticated(async (parent, args, context, info) => {
    const { currentUser } = context;
    if (hasPermissions(currentUser.permissions, PERMISSIONS.CREATE)) {
    return await createEventInDb({ ...args, ...context });
    }
    });
    @samjulien

    View Slide

  136. createEvent: isAuthenticated(async (parent, args, context, info) => {
    const { currentUser } = context;
    if (hasPermissions(currentUser.permissions, PERMISSIONS.CREATE)) {
    return await createEventInDb({ ...args, ...context });
    }
    });
    @samjulien

    View Slide

  137. export const checkPermission = permission => next => (
    parent,
    args,
    context,
    info
    ) => {
    if (!hasPermissions(context.currentUser.permissions, permission)) {
    throw new Error("You don't have permission!");
    }
    return next(parent, args, context, info);
    };
    @samjulien

    View Slide

  138. createEvent: isAuthenticated(
    checkPermissions(PERMISSIONS.CREATE)(
    async (parent, args, context, info) => {
    return await createEventInDb({ ...args, ...context });
    }
    )
    );
    @samjulien

    View Slide

  139. createEvent: isAuthenticated(
    checkPermissions(PERMISSIONS.CREATE)(
    async (parent, args, context, info) => {
    return await createEventInDb({ ...args, ...context });
    }
    )
    );
    @samjulien

    View Slide

  140. createEvent: isAuthenticated(
    checkPermissions(PERMISSIONS.CREATE)(
    async (parent, args, context, info) => {
    return await createEventInDb({ ...args, ...context });
    }
    )
    );
    @samjulien

    View Slide

  141. graphql-resolvers
    Lucas Constantino
    @samjulien

    View Slide

  142. export const isAuthenticated = (parent, args, context, info) => {
    return context.currentUser ? skip :
    new Error("Not authenticated!");
    };
    @samjulien

    View Slide

  143. export const isAuthenticated = (parent, args, context, info) => {
    return context.currentUser ? skip :
    new Error("Not authenticated!");
    };
    @samjulien

    View Slide

  144. export const isAuthenticated = (parent, args, context, info) => {
    return context.currentUser ? skip :
    new Error("Not authenticated!");
    };
    @samjulien

    View Slide

  145. createEvent: combineResolvers(
    isAuthenticated,
    checkPermissions(PERMISSIONS.CREATE),
    async (parent, args, context, info) => {
    return await createEventInDb({ ...args, ...context });
    }
    );
    @samjulien

    View Slide

  146. createEvent: combineResolvers(
    isAuthenticated,
    checkPermissions(PERMISSIONS.CREATE),
    async (parent, args, context, info) => {
    return await createEventInDb({ ...args, ...context });
    }
    );
    @samjulien

    View Slide

  147. createEvent: combineResolvers(
    isAuthenticated,
    checkPermissions(PERMISSIONS.CREATE),
    async (parent, args, context, info) => {
    return await createEventInDb({ ...args, ...context });
    }
    );
    @samjulien

    View Slide

  148. graphql-modules
    Uri Goldshtein (& Many Others)
    @samjulien

    View Slide

  149. import { GraphQLModule } from "@graphql-modules/core";
    const MyModule = new GraphQLModule({
    /*...*/
    resolversComposition: {
    "Mutation.createEvent": [
    isAuthenticated(),
    checkPermissions(PERMISSIONS.CREATE)
    ]
    }
    });
    @samjulien

    View Slide

  150. import { GraphQLModule } from "@graphql-modules/core";
    const MyModule = new GraphQLModule({
    /*...*/
    resolversComposition: {
    "Mutation.createEvent": [
    isAuthenticated(),
    checkPermissions(PERMISSIONS.CREATE)
    ]
    }
    });
    @samjulien

    View Slide

  151. Middleware
    @samjulien

    View Slide

  152. graphql-middleware
    Prisma
    @samjulien

    View Slide

  153. const authMiddleware = {
    Mutation: {
    createEvent: isAuthenticated
    },
    }
    const server = new GraphQLServer({
    typeDefs,
    resolvers,
    [authMiddleware],
    })
    @samjulien

    View Slide

  154. const authMiddleware = {
    Mutation: {
    createEvent: isAuthenticated
    },
    }
    const server = new GraphQLServer({
    typeDefs,
    resolvers,
    [authMiddleware],
    })
    @samjulien

    View Slide

  155. const authMiddleware = {
    Mutation: {
    createEvent: isAuthenticated
    },
    }
    const server = new GraphQLServer({
    typeDefs,
    resolvers,
    [authMiddleware],
    })
    @samjulien

    View Slide

  156. View Slide

  157. Custom Directives
    @samjulien

    View Slide

  158. @deprecated(reason: “Field `eventLocation` replaces `location`.")
    @samjulien

    View Slide

  159. type Event {
    id: ID
    description: String!
    eventLocation: String!
    location: String! @deprecated(reason: "Field
    `eventLocation` replaces `location`.")
    }
    @samjulien

    View Slide

  160. Directives
    @samjulien

    View Slide

  161. Part of the GraphQL Spec
    Directives
    @samjulien

    View Slide

  162. Part of the GraphQL Spec
    Change behavior at runtime
    Directives
    @samjulien

    View Slide

  163. Part of the GraphQL Spec
    Change behavior at runtime
    Many use cases
    Directives
    @samjulien

    View Slide

  164. We can use custom directives to
    control access down to the field level.
    @samjulien

    View Slide

  165. directive @hasPermission(permission: String) on FIELD_DEFINITION
    Anatomy of a Directive
    @samjulien

    View Slide

  166. directive @hasPermission(permission: String) on FIELD_DEFINITION
    Name of the Directive
    @samjulien

    View Slide

  167. directive @hasPermission(permission: String) on FIELD_DEFINITION
    Argument
    @samjulien

    View Slide

  168. directive @hasPermission(permission: String) on FIELD_DEFINITION
    Where it Works
    @samjulien

    View Slide

  169. directive @hasPermission(permission: String) on FIELD_DEFINITION
    @samjulien

    View Slide

  170. class HasPermissionDirective extends SchemaDirectiveVisitor {
    visitFieldDefinition(field) {
    const { permission } = this.args;
    const { resolve = defaultFieldResolver } = field;
    field.resolve = ({ ...args }) => {
    const context = args[2];
    if (!context.currentUser) {
    throw new Error("Must be logged in!");
    }
    if (hasPermission(context.currentUser, permission)) {
    return resolve.apply(this, args);
    } else {
    throw new Error("Not authorized!");
    }
    };
    }
    }
    @samjulien

    View Slide

  171. class HasPermissionDirective extends SchemaDirectiveVisitor {
    visitFieldDefinition(field) {
    const { permission } = this.args;
    const { resolve = defaultFieldResolver } = field;
    field.resolve = ({ ...args }) => {
    const context = args[2];
    if (!context.currentUser) {
    throw new Error("Must be logged in!");
    }
    if (hasPermission(context.currentUser, permission)) {
    return resolve.apply(this, args);
    } else {
    throw new Error("Not authorized!");
    }
    };
    }
    }
    @samjulien

    View Slide

  172. class HasPermissionDirective extends SchemaDirectiveVisitor {
    visitFieldDefinition(field) {
    const { permission } = this.args;
    const { resolve = defaultFieldResolver } = field;
    field.resolve = ({ ...args }) => {
    const context = args[2];
    if (!context.currentUser) {
    throw new Error("Must be logged in!");
    }
    if (hasPermission(context.currentUser, permission)) {
    return resolve.apply(this, args);
    } else {
    throw new Error("Not authorized!");
    }
    };
    }
    }
    @samjulien

    View Slide

  173. class HasPermissionDirective extends SchemaDirectiveVisitor {
    visitFieldDefinition(field) {
    const { permission } = this.args;
    const { resolve = defaultFieldResolver } = field;
    field.resolve = ({ ...args }) => {
    const context = args[2];
    if (!context.currentUser) {
    throw new Error("Must be logged in!");
    }
    if (hasPermission(context.currentUser, permission)) {
    return resolve.apply(this, args);
    } else {
    throw new Error("Not authorized!");
    }
    };
    }
    }
    @samjulien

    View Slide

  174. class HasPermissionDirective extends SchemaDirectiveVisitor {
    visitFieldDefinition(field) {
    const { permission } = this.args;
    const { resolve = defaultFieldResolver } = field;
    field.resolve = ({ ...args }) => {
    const context = args[2];
    if (!context.currentUser) {
    throw new Error("Must be logged in!");
    }
    if (hasPermission(context.currentUser, permission)) {
    return resolve.apply(this, args);
    } else {
    throw new Error("Not authorized!");
    }
    };
    }
    }
    @samjulien

    View Slide

  175. class HasPermissionDirective extends SchemaDirectiveVisitor {
    visitFieldDefinition(field) {
    const { permission } = this.args;
    const { resolve = defaultFieldResolver } = field;
    field.resolve = ({ ...args }) => {
    const context = args[2];
    if (!context.currentUser) {
    throw new Error("Must be logged in!");
    }
    if (hasPermission(context.currentUser, permission)) {
    return resolve.apply(this, args);
    } else {
    throw new Error("Not authorized!");
    }
    };
    }
    }
    @samjulien

    View Slide

  176. class HasPermissionDirective extends SchemaDirectiveVisitor {
    visitFieldDefinition(field) {
    const { permission } = this.args;
    const { resolve = defaultFieldResolver } = field;
    field.resolve = ({ ...args }) => {
    const context = args[2];
    if (!context.currentUser) {
    throw new Error("Must be logged in!");
    }
    if (hasPermission(context.currentUser, permission)) {
    return resolve.apply(this, args);
    } else {
    throw new Error("Not authorized!");
    }
    };
    }
    }
    @samjulien

    View Slide

  177. class HasPermissionDirective extends SchemaDirectiveVisitor {
    visitFieldDefinition(field) {
    const { permission } = this.args;
    const { resolve = defaultFieldResolver } = field;
    field.resolve = ({ ...args }) => {
    const context = args[2];
    if (!context.currentUser) {
    throw new Error("Must be logged in!");
    }
    if (hasPermission(context.currentUser, permission)) {
    return resolve.apply(this, args);
    } else {
    throw new Error("Not authorized!");
    }
    };
    }
    }
    @samjulien

    View Slide

  178. const schema = makeExecutableSchema({
    typeDefs,
    schemaDirectives: {
    hasPermission: HasPermissionDirective
    }
    });
    @samjulien

    View Slide

  179. const schema = makeExecutableSchema({
    typeDefs,
    schemaDirectives: {
    hasPermission: HasPermissionDirective
    }
    });
    @samjulien

    View Slide

  180. type Mutation: {
    createEvent: Event
    @hasPermission(permission: PERMISSIONS.CREATE)
    }
    @samjulien

    View Slide

  181. type Mutation: {
    createEvent: Event
    @hasPermission(permission: PERMISSIONS.CREATE)
    }
    @samjulien

    View Slide

  182. Downsides
    @samjulien

    View Slide

  183. ⚠ Couples logic to schema.
    Downsides
    @samjulien

    View Slide

  184. ⚠ Couples logic to schema.
    Can be quite difficult.
    Downsides
    @samjulien

    View Slide

  185. ⚠ Couples logic to schema.
    Can be quite difficult.
    Requires exhaustive testing.
    Downsides
    @samjulien

    View Slide

  186. View Slide

  187. View Slide

  188. Let’s Review

    View Slide

  189. @samjulien

    View Slide

  190. GraphQL + NextJS on the Same Server
    @samjulien

    View Slide

  191. @samjulien

    View Slide

  192. @samjulien

    View Slide

  193. @samjulien

    View Slide

  194. @samjulien

    View Slide

  195. eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxM
    jM0NTY3ODkwIiwibmFtZSI6IkhlbGxvIEdyYXBoUUwiLCJlbWF
    pbCI6ImhlbGxvQGdyYXBocWwuY29tIiwiaWF0IjoxNTE2MjM5M
    DIyfQ.cJutaCScQJXsGTL6ynH7BRMnQj_P2yl0a58jUhnLDq8
    @samjulien

    View Slide

  196. @samjulien

    View Slide

  197. Can contain useful information
    @samjulien

    View Slide

  198. Can contain useful information
    ✅ Can be signed and verified
    @samjulien

    View Slide

  199. const createContext = async req => {
    const db = await startDatabase();
    let token = null;
    let currentUser = null;
    try {
    token = req.headers["Authorization"];
    if (token) {
    const payload = await verifyToken(token);
    currentUser = await db.query.users.where({ id: payload.sub });
    }
    } catch (error) {
    throw new Error("Unable to authenticate.");
    }
    return { db, token, currentUser };
    }; @samjulien

    View Slide

  200. createEvent: async (parent, args, context, info) => {
    if (!context.currentUser) {
    throw new Error("Must be logged in for this!");
    }
    const { currentUser } = context;
    if (hasPermissions(currentUser.permissions, PERMISSIONS.CREATE)) {
    return await createEventInDb({
    ...args,
    ...context
    });
    }
    };
    @samjulien

    View Slide

  201. graphql-resolvers
    Lucas Constantino
    @samjulien

    View Slide

  202. createEvent: combineResolvers(
    isAuthenticated,
    checkPermissions(PERMISSIONS.CREATE),
    async (parent, args, context, info) => {
    return await createEventInDb({ ...args, ...context });
    }
    );
    @samjulien

    View Slide

  203. graphql-modules
    Uri Goldshtein (& Many Others)
    @samjulien

    View Slide

  204. import { GraphQLModule } from "@graphql-modules/core";
    const MyModule = new GraphQLModule({
    /*...*/
    resolversComposition: {
    "Mutation.createEvent": [
    isAuthenticated(),
    checkPermissions(PERMISSIONS.CREATE)
    ]
    }
    });
    @samjulien

    View Slide

  205. graphql-middleware
    Prisma
    @samjulien

    View Slide

  206. const authMiddleware = {
    Mutation: {
    createEvent: isAuthenticated
    },
    }
    const server = new GraphQLServer({
    typeDefs,
    resolvers,
    [authMiddleware],
    })
    @samjulien

    View Slide

  207. directive @hasPermission(permission: String) on FIELD_DEFINITION
    @samjulien

    View Slide

  208. type Mutation: {
    createEvent: Event
    @hasPermission(permission: PERMISSIONS.CREATE)
    }
    @samjulien

    View Slide

  209. View Slide

  210. Good schema design is the
    most important goal.
    @samjulien

    View Slide

  211. Design the schema to require
    only as much auth as needed.
    @samjulien

    View Slide

  212. Think through whether auth is needed
    at the object level or the field level.
    @samjulien

    View Slide

  213. Don’t design mutations that
    can change arbitrary objects.
    @samjulien

    View Slide

  214. Experiment with sending null
    values instead of throwing errors.
    @samjulien

    View Slide

  215. http://samj.im/graphql-auth
    @samjulien

    View Slide

  216. http://samj.im/graphql-auth
    Thank you!
    @samjulien

    View Slide