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

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

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

Are you confused about how authentication and authorization relate to GraphQL APIs? You’re not alone! It’s no secret that learning auth is hard on its own, let alone on top of GraphQL. Let’s demystify auth while learning how to use JSON Web Tokens (JWTs) with GraphQL APIs! After discovering why controlling access to APIs is so challenging and ways we can solve it, we’ll step through how to handle authorization in your GraphQL server. This talk will use JavaScript examples, but the principles will apply to other tech. By the end, you’ll feel a whole lot better about tackling auth in GraphQL.

Sam Julien

March 01, 2021
Tweet

More Decks by Sam Julien

Other Decks in Programming

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 | samjulien.com
    DevRel Manager at Auth0
    Getting Started in Developer Relations & Guide to Tiny Experiments
    Developer Microskills Newsletter

    View Slide

  9. @samjulien

    View Slide

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

    View Slide

  11. Some Auth Background

    View Slide

  12. Authentication & Authorization
    @samjulien

    View Slide

  13. Authentication
    @samjulien

    View Slide

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

    View Slide

  15. Authorization
    @samjulien

    View Slide

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

    View Slide

  17. @samjulien

    View Slide

  18. @samjulien

    View Slide

  19. @samjulien

    View Slide

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

    View Slide

  21. Access
    @samjulien

    View Slide

  22. Backend + Frontend on the Same Server
    @samjulien

    View Slide

  23. GraphQL + NextJS on the Same Server
    @samjulien

    View Slide

  24. GraphQL + NextJS on the Same Server
    @samjulien

    View Slide

  25. @samjulien

    View Slide

  26. @samjulien

    View Slide

  27. Access
    @samjulien

    View Slide

  28. Delegated Access
    @samjulien

    View Slide

  29. GraphQL + NextJS on the Same Server
    @samjulien

    View Slide

  30. @samjulien

    View Slide

  31. @samjulien

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  35. Token
    @samjulien

    View Slide

  36. Authorization Server
    @samjulien

    View Slide

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

    View Slide

  38. Access Token
    @samjulien

    View Slide

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

    View Slide

  40. @samjulien

    View Slide

  41. @samjulien

    View Slide

  42. @samjulien

    View Slide

  43. Access Token
    @samjulien

    View Slide

  44. Authorization: Bearer
    @samjulien

    View Slide

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

    View Slide

  46. JSON Web Tokens (JWTs)

    View Slide

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

    View Slide

  48. {
    "sub": "1234567890",
    "name": "Hello GraphQL",
    "email": "[email protected]",
    "iat": 1516239022
    }
    🤖
    eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
    @samjulien

    View Slide

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

    View Slide

  50. 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

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

    View Slide

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

    View Slide

  53. {
    "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

  54. {
    "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

  55. @samjulien

    View Slide

  56. @samjulien

    View Slide

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

    View Slide

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

    View Slide

  59. How will you be sure you…
    @samjulien

    View Slide

  60. 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

  61. 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

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

    View Slide

  63. @samjulien

    View Slide

  64. @samjulien

    View Slide

  65. Authorization in GraphQL

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  69. So how do we do that?
    @samjulien

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  74. verifyToken(token): Payload
    @samjulien

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  87. verifyToken(token): Payload
    @samjulien

    View Slide

  88. Where do we use this?
    @samjulien

    View Slide

  89. Inside of resolvers?
    @samjulien

    View Slide

  90. Back in Context
    @samjulien

    View Slide

  91. 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

  92. 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

  93. 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

  94. 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

  95. 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

  96. 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

  97. 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

  98. 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

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

    View Slide

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

    View Slide

  101. 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

  102. 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

  103. 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

  104. Wrapping (Composing) Resolvers
    @samjulien

    View Slide

  105. 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

  106. 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

  107. 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

  108. 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

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

    View Slide

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

    View Slide

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

    View Slide

  112. 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

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

    View Slide

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

    View Slide

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

    View Slide

  116. graphql-resolvers
    Lucas Constantino
    @samjulien

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  123. graphql-auth
    Kurt Kemple
    @samjulien

    View Slide

  124. import withAuth from 'graphql-auth';
    const resolvers = {
    Query: {
    users: withAuth(['users:view'], (root, args, context) => { ... }),
    ...
    }
    }
    @samjulien

    View Slide

  125. import withAuth from 'graphql-auth';
    const resolvers = {
    Query: {
    users: withAuth(['users:view'], (root, args, context) => { ... }),
    ...
    }
    }
    @samjulien

    View Slide

  126. import withAuth from 'graphql-auth';
    const resolvers = {
    Query: {
    users: withAuth(['users:view'], (root, args, context) => { ... }),
    ...
    }
    }
    @samjulien

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  130. Middleware
    @samjulien

    View Slide

  131. graphql-middleware
    Prisma Community (Matic Zavadlal)
    @samjulien

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  135. graphql-shield
    Matic Zavadlal
    @samjulien

    View Slide

  136. View Slide

  137. const isAuthenticated = rule({ cache: "contextual" })(
    async (parent, args, ctx, info) => {
    return ctx.user !== null;
    }
    );
    const isAdmin = rule({ cache: "contextual" })(
    async (parent, args, ctx, info) => {
    return ctx.user.role === "admin";
    }
    );
    const isEditor = rule({ cache: "contextual" })(
    async (parent, args, ctx, info) => {
    return ctx.user.role === "editor";
    }
    );
    @samjulien

    View Slide

  138. const isAuthenticated = rule({ cache: "contextual" })(
    async (parent, args, ctx, info) => {
    return ctx.user !== null;
    }
    );
    const isAdmin = rule({ cache: "contextual" })(
    async (parent, args, ctx, info) => {
    return ctx.user.role === "admin";
    }
    );
    const isEditor = rule({ cache: "contextual" })(
    async (parent, args, ctx, info) => {
    return ctx.user.role === "editor";
    }
    );
    @samjulien

    View Slide

  139. const permissions = shield({
    Query: {
    frontPage: not(isAuthenticated),
    events: and(isAuthenticated, or(isAdmin, isEditor)),
    },
    Mutation: {
    createEvent: isAuthenticated,
    },
    });
    const server = new GraphQLServer({
    typeDefs,
    resolvers,
    middlewares: [permissions],
    context: (req) => ({
    ...req,
    user: getUser(req),
    }),
    });
    @samjulien

    View Slide

  140. const permissions = shield({
    Query: {
    frontPage: not(isAuthenticated),
    events: and(isAuthenticated, or(isAdmin, isEditor)),
    },
    Mutation: {
    createEvent: isAuthenticated,
    },
    });
    const server = new GraphQLServer({
    typeDefs,
    resolvers,
    middlewares: [permissions],
    context: (req) => ({
    ...req,
    user: getUser(req),
    }),
    });
    @samjulien

    View Slide

  141. const permissions = shield({
    Query: {
    frontPage: not(isAuthenticated),
    events: and(isAuthenticated, or(isAdmin, isEditor)),
    },
    Mutation: {
    createEvent: isAuthenticated,
    },
    });
    const server = new GraphQLServer({
    typeDefs,
    resolvers,
    middlewares: [permissions],
    context: (req) => ({
    ...req,
    user: getUser(req),
    }),
    });
    @samjulien

    View Slide

  142. Models
    @samjulien

    View Slide

  143. export const Dog = {
    getAll: () => {
    /* logic to get all dogs */
    },
    getById: (id) => {
    /* logic to get a single dog */
    },
    getByGroupId: (id) => {
    /* logic to get a group of dogs */
    },
    };
    @samjulien

    View Slide

  144. export const generateDogModel = ({ currentUser }) =>
    ({
    getAll: () => {
    /* logic to get all dogs */
    },
    getById: (id) => {
    /* logic to get a single dog */
    },
    getByGroupId: (id) => {
    /* logic to get a group of dogs */ },
    });
    @samjulien

    View Slide

  145. getAll: () => {
    if(!currentUser || !currentUser.roles.includes('admin'))
    return null;
    return fetch(`${API_URL}/dog`);
    }
    @samjulien

    View Slide

  146. getAll: () => {
    if(!currentUser || !currentUser.roles.includes('admin'))
    return null;
    return fetch(`${API_URL}/dog`);
    }
    @samjulien

    View Slide

  147. const createContext = async req => {
    /* previous code hidden but unchanged */
    return {
    db,
    token,
    currentUser,
    models: {
    Dog: generateDogModel({ currentUser }),
    },
    };
    };
    @samjulien

    View Slide

  148. const createContext = async req => {
    /* previous code hidden but unchanged */
    return {
    db,
    token,
    currentUser,
    models: {
    Dog: generateDogModel({ currentUser }),
    },
    };
    };
    @samjulien

    View Slide

  149. Custom Directives
    @samjulien

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  159. 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

  160. 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

  161. 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

  162. 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

  163. 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

  164. 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

  165. 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

  166. 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

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  172. Let’s Review

    View Slide

  173. @samjulien

    View Slide

  174. GraphQL + NextJS on the Same Server
    @samjulien

    View Slide

  175. @samjulien

    View Slide

  176. @samjulien

    View Slide

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

    View Slide

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

    View Slide

  179. 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

  180. 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

  181. graphql-resolvers
    Lucas Constantino
    @samjulien

    View Slide

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

    View Slide

  183. graphql-auth
    Kurt Kemple
    @samjulien

    View Slide

  184. import withAuth from 'graphql-auth';
    const resolvers = {
    Query: {
    users: withAuth(['users:view'], (root, args, context) => { ... }),
    ...
    }
    }
    @samjulien

    View Slide

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

    View Slide

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

    View Slide

  187. graphql-middleware
    Prisma Community (Matic Zavadlal)
    @samjulien

    View Slide

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

    View Slide

  189. graphql-shield
    Matic Zavadlal
    @samjulien

    View Slide

  190. View Slide

  191. const isAuthenticated = rule({ cache: "contextual" })(
    async (parent, args, ctx, info) => {
    return ctx.user !== null;
    }
    );
    const isAdmin = rule({ cache: "contextual" })(
    async (parent, args, ctx, info) => {
    return ctx.user.role === "admin";
    }
    );
    const isEditor = rule({ cache: "contextual" })(
    async (parent, args, ctx, info) => {
    return ctx.user.role === "editor";
    }
    );
    @samjulien

    View Slide

  192. const permissions = shield({
    Query: {
    frontPage: not(isAuthenticated),
    events: and(isAuthenticated, or(isAdmin, isEditor)),
    },
    Mutation: {
    createEvent: isAuthenticated,
    },
    });
    const server = new GraphQLServer({
    typeDefs,
    resolvers,
    middlewares: [permissions],
    context: (req) => ({
    ...req,
    user: getUser(req),
    }),
    });
    @samjulien

    View Slide

  193. Models
    @samjulien

    View Slide

  194. export const generateDogModel = ({ currentUser }) =>
    ({
    getAll: () => {
    /* logic to get all dogs */
    },
    getById: (id) => {
    /* logic to get a single dog */
    },
    getByGroupId: (id) => {
    /* logic to get a group of dogs */ },
    });
    @samjulien

    View Slide

  195. getAll: () => {
    if(!currentUser || !currentUser.roles.includes('admin'))
    return null;
    return fetch(`${API_URL}/dog`);
    }
    @samjulien

    View Slide

  196. Custom Directives
    @samjulien

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide