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 full-size slide

  2. Auth in GraphQL can be confusing.
    @samjulien

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    @samjulien

    View full-size slide

  7. Sam Julien
    @samjulien

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  11. Some Auth Background
    @samjulien

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  14. Some Auth Background

    View full-size slide

  15. Authentication & Authorization
    @samjulien

    View full-size slide

  16. Authentication
    @samjulien

    View full-size slide

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

    View full-size slide

  18. Authorization
    @samjulien

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  21. Access
    @samjulien

    View full-size slide

  22. Backend + Frontend on the Same Server
    @samjulien

    View full-size slide

  23. GraphQL + NextJS on the Same Server
    @samjulien

    View full-size slide

  24. GraphQL + NextJS on the Same Server
    @samjulien

    View full-size slide

  25. GraphQL + NextJS on the Same Server
    @samjulien

    View full-size slide

  26. GraphQL + NextJS on the Same Server
    @samjulien

    View full-size slide

  27. GraphQL + NextJS on the Same Server
    @samjulien

    View full-size slide

  28. GraphQL + NextJS on the Same Server
    @samjulien

    View full-size slide

  29. GraphQL + NextJS on the Same Server
    @samjulien

    View full-size slide

  30. Access
    @samjulien

    View full-size slide

  31. Delegated Access
    @samjulien

    View full-size slide

  32. GraphQL + NextJS on the Same Server
    @samjulien

    View full-size slide

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

    View full-size slide

  34. Can contain useful information
    @samjulien

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  37. Token
    @samjulien

    View full-size slide

  38. Authorization Server
    @samjulien

    View full-size slide

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

    View full-size slide

  40. Access Token
    @samjulien

    View full-size slide

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

    View full-size slide

  42. Access Token
    @samjulien

    View full-size slide

  43. Authorization: Bearer
    @samjulien

    View full-size slide

  44. Can contain useful information
    @samjulien

    View full-size slide

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

    View full-size slide

  46. JSON Web Tokens (JWTs)

    View full-size slide

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

    View full-size slide

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

    @samjulien

    View full-size slide

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

    @samjulien

    View full-size slide

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

    @samjulien

    View full-size slide

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

    eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
    @samjulien

    View full-size slide

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

    View full-size slide

  53. 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 full-size slide

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

    View full-size slide

  55. Can contain useful information
    @samjulien

    View full-size slide

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

    View full-size slide

  57. {
    "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 full-size slide

  58. {
    "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 full-size slide

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

    View full-size slide

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

    View full-size slide

  61. How will you be sure you…
    @samjulien

    View full-size slide

  62. How will you be sure you…
    @samjulien

    View full-size slide

  63. 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 full-size slide

  64. 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 full-size slide

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

    View full-size slide

  66. Authorization in GraphQL

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  70. So how do we do that?
    @samjulien

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  75. verifyToken(token): Payload
    @samjulien

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  88. verifyToken(token): Payload
    @samjulien

    View full-size slide

  89. Where do we use this?
    @samjulien

    View full-size slide

  90. Inside of resolvers?
    @samjulien

    View full-size slide

  91. Back in Context
    @samjulien

    View full-size 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 full-size 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 full-size 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 full-size 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 full-size 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 full-size 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 full-size 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 full-size slide

  99. 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 full-size slide

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

    View full-size slide

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

    View full-size 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 full-size 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 full-size slide

  104. 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 full-size slide

  105. Wrapping (Composing) Resolvers
    @samjulien

    View full-size 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 full-size 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 full-size 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 full-size slide

  109. 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 full-size 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 full-size 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 full-size slide

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

    View full-size slide

  113. 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 full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  117. graphql-resolvers
    Lucas Constantino
    @samjulien

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  127. Middleware
    @samjulien

    View full-size slide

  128. graphql-middleware
    Prisma
    @samjulien

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  132. Custom Directives
    @samjulien

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  135. Directives
    @samjulien

    View full-size slide

  136. Part of the GraphQL Spec
    Directives
    @samjulien

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  145. 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 full-size slide

  146. 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 full-size slide

  147. 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 full-size slide

  148. 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 full-size slide

  149. 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 full-size slide

  150. 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 full-size slide

  151. 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 full-size slide

  152. 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 full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  157. Downsides
    @samjulien

    View full-size slide

  158. ⚠ Couples logic to schema.
    Downsides
    @samjulien

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  161. Let’s Review

    View full-size slide

  162. GraphQL + NextJS on the Same Server
    @samjulien

    View full-size slide

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

    View full-size slide

  164. Can contain useful information
    @samjulien

    View full-size slide

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

    View full-size slide

  166. 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 full-size slide

  167. 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 full-size slide

  168. graphql-resolvers
    Lucas Constantino
    @samjulien

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  172. graphql-middleware
    Prisma
    @samjulien

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide