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.

7beed3a6fa39e12c9e873b903e4d9244?s=128

Sam Julien

March 01, 2021
Tweet

Transcript

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

  2. @samjulien

  3. Auth in GraphQL can be confusing. @samjulien

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

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

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

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

    @samjulien
  8. Sam Julien @samjulien | samjulien.com DevRel Manager at Auth0 Getting

    Started in Developer Relations & Guide to Tiny Experiments Developer Microskills Newsletter
  9. @samjulien

  10. Some Auth Background The What & Why of JWTs Authorization

    in GraphQL @samjulien
  11. Some Auth Background

  12. Authentication & Authorization @samjulien

  13. Authentication @samjulien

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

  15. Authorization @samjulien

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

  17. @samjulien

  18. @samjulien

  19. @samjulien

  20. The GraphQL server doesn’t necessarily care about users proving who

    they are — it cares who has access to what. @samjulien
  21. Access @samjulien

  22. Backend + Frontend on the Same Server @samjulien

  23. GraphQL + NextJS on the Same Server @samjulien

  24. GraphQL + NextJS on the Same Server @samjulien

  25. @samjulien

  26. @samjulien

  27. Access @samjulien

  28. Delegated Access @samjulien

  29. GraphQL + NextJS on the Same Server @samjulien

  30. @samjulien

  31. @samjulien

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

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

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

    create it? @samjulien
  35. Token @samjulien

  36. Authorization Server @samjulien

  37. Helps make access control decisions in your app or API.

    Authorization Server @samjulien
  38. Access Token @samjulien

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

    @samjulien
  40. @samjulien

  41. @samjulien

  42. @samjulien

  43. Access Token @samjulien

  44. Authorization: Bearer <token> @samjulien

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

    @samjulien
  46. JSON Web Tokens (JWTs)

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

  48. { "sub": "1234567890", "name": "Hello GraphQL", "email": "hello@graphql.com", "iat": 1516239022

    } 🤖 eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 @samjulien
  49. eyJhbGciOiJIUzI1NiIsInR5c CI6IkpXVCJ9.eyJzdWIiOiIxM jM0NTY3ODkwIiwibmFtZSI6Ik hlbGxvIEdyYXBoUUwiLCJlbWF pbCI6ImhlbGxvQGdyYXBocWwu Y29tIiwiaWF0IjoxNTE2MjM5M DIyfQ.cJutaCScQJXsGTL6ynH 7BRMnQj_P2yl0a58jUhnLDq8 Header (Algorithm

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

    and Claims) { "sub": "1234567890", "name": "Hello GraphQL", "email": "hello@graphql.com", "iat": 1516239022 } @samjulien
  51. eyJhbGciOiJIUzI1NiIsInR5c CI6IkpXVCJ9.eyJzdWIiOiIxM jM0NTY3ODkwIiwibmFtZSI6Ik hlbGxvIEdyYXBoUUwiLCJlbWF pbCI6ImhlbGxvQGdyYXBocWwu Y29tIiwiaWF0IjoxNTE2MjM5M DIyfQ.cJutaCScQJXsGTL6ynH 7BRMnQj_P2yl0a58jUhnLDq8 Verify Signature

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

    @samjulien
  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
  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
  55. @samjulien

  56. @samjulien

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

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

  59. How will you be sure you… @samjulien

  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
  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
  62. Consider outsourcing this piece of your app. @samjulien

  63. @samjulien

  64. @samjulien

  65. Authorization in GraphQL

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

  67. Your GraphQL server needs to verify the access token with

    a public key. @samjulien
  68. You can then parse claims for authorization. @samjulien

  69. So how do we do that? @samjulien

  70. First, grab the token from the request and add it

    to context. @samjulien
  71. const server = new ApolloServer({ typeDefs: schema, resolvers, context: async

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

    ({ req }) => { const token = req.headers["Authorization"]; return { token }; } }); @samjulien
  73. We still need to verify the token. @samjulien

  74. verifyToken(token): Payload @samjulien

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

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

  77. eyJhbGciOiJIUzI1NiIsInR5c CI6IkpXVCJ9.eyJzdWIiOiIxM jM0NTY3ODkwIiwibmFtZSI6Ik hlbGxvIEdyYXBoUUwiLCJlbWF pbCI6ImhlbGxvQGdyYXBocWwu Y29tIiwiaWF0IjoxNTE2MjM5M DIyfQ.cJutaCScQJXsGTL6ynH 7BRMnQj_P2yl0a58jUhnLDq8 Verify Signature

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

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

  80. function getJwksClientKey(header, callback) { client.getSigningKey(header.kid, function(error, key) { const signingKey

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

    = key.publicKey || key.rsaPublicKey; callback(null, signingKey); }); } @samjulien
  82. jwt.verify(bearerToken, getJwksClientKey, { audience: process.env.API_IDENTIFIER, issuer: `https://${process.env.AUTH_DOMAIN}/`, algorithms: ["RS256"] });

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

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

    @samjulien
  85. try { const payload = await jwt.verify(...); return payload; }

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

    catch (error) { throw new Error("Invalid token!"); } @samjulien
  87. verifyToken(token): Payload @samjulien

  88. Where do we use this? @samjulien

  89. Inside of resolvers? @samjulien

  90. Back in Context @samjulien

  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
  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
  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
  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
  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
  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
  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
  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
  99. const server = new ApolloServer({ typeDefs: schema, resolvers, context: async

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

    ({ req }) => createContext(req) }); @samjulien
  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
  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
  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
  104. Wrapping (Composing) Resolvers @samjulien

  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
  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
  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
  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
  109. createEvent: isAuthenticated(async (parent, args, context, info) => { const {

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

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

    currentUser } = context; if (hasPermissions(currentUser.permissions, PERMISSIONS.CREATE)) { return await createEventInDb({ ...args, ...context }); } }); @samjulien
  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
  113. createEvent: isAuthenticated( checkPermissions(PERMISSIONS.CREATE)( async (parent, args, context, info) => {

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

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

    return await createEventInDb({ ...args, ...context }); } ) ); @samjulien
  116. graphql-resolvers Lucas Constantino @samjulien

  117. export const isAuthenticated = (parent, args, context, info) => {

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

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

    return context.currentUser ? skip : new Error("Not authenticated!"); }; @samjulien
  120. createEvent: combineResolvers( isAuthenticated, checkPermissions(PERMISSIONS.CREATE), async (parent, args, context, info) =>

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

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

    { return await createEventInDb({ ...args, ...context }); } ); @samjulien
  123. graphql-auth Kurt Kemple @samjulien

  124. import withAuth from 'graphql-auth'; const resolvers = { Query: {

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

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

    users: withAuth(['users:view'], (root, args, context) => { ... }), ... } } @samjulien
  127. graphql-modules Uri Goldshtein (& Many Others) @samjulien

  128. import { GraphQLModule } from "@graphql-modules/core"; const MyModule = new

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

    GraphQLModule({ /*...*/ resolversComposition: { "Mutation.createEvent": [ isAuthenticated(), checkPermissions(PERMISSIONS.CREATE) ] } }); @samjulien
  130. Middleware @samjulien

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

  132. const authMiddleware = { Mutation: { createEvent: isAuthenticated }, }

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

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

    const server = new GraphQLServer({ typeDefs, resolvers, [authMiddleware], }) @samjulien
  135. graphql-shield Matic Zavadlal @samjulien

  136. None
  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
  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
  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
  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
  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
  142. Models @samjulien

  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
  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
  145. getAll: () => { if(!currentUser || !currentUser.roles.includes('admin')) return null; return

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

    fetch(`${API_URL}/dog`); } @samjulien
  147. const createContext = async req => { /* previous code

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

    hidden but unchanged */ return { db, token, currentUser, models: { Dog: generateDogModel({ currentUser }), }, }; }; @samjulien
  149. Custom Directives @samjulien

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

  151. type Event { id: ID description: String! eventLocation: String! location:

    String! @deprecated(reason: "Field `eventLocation` replaces `location`.") } @samjulien
  152. Part of the GraphQL Spec Change behavior at runtime Many

    use cases Directives @samjulien
  153. We can use custom directives to control access down to

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

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

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

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

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

  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
  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
  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
  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
  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
  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
  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
  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
  167. const schema = makeExecutableSchema({ typeDefs, schemaDirectives: { hasPermission: HasPermissionDirective }

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

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

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

  171. ⚠ Couples logic to schema. 😅 Can be quite difficult.

    🧪 Requires exhaustive testing. Downsides @samjulien
  172. Let’s Review

  173. @samjulien

  174. GraphQL + NextJS on the Same Server @samjulien

  175. @samjulien

  176. @samjulien

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

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

    @samjulien
  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
  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
  181. graphql-resolvers Lucas Constantino @samjulien

  182. createEvent: combineResolvers( isAuthenticated, checkPermissions(PERMISSIONS.CREATE), async (parent, args, context, info) =>

    { return await createEventInDb({ ...args, ...context }); } ); @samjulien
  183. graphql-auth Kurt Kemple @samjulien

  184. import withAuth from 'graphql-auth'; const resolvers = { Query: {

    users: withAuth(['users:view'], (root, args, context) => { ... }), ... } } @samjulien
  185. graphql-modules Uri Goldshtein (& Many Others) @samjulien

  186. import { GraphQLModule } from "@graphql-modules/core"; const MyModule = new

    GraphQLModule({ /*...*/ resolversComposition: { "Mutation.createEvent": [ isAuthenticated(), checkPermissions(PERMISSIONS.CREATE) ] } }); @samjulien
  187. graphql-middleware Prisma Community (Matic Zavadlal) @samjulien

  188. const authMiddleware = { Mutation: { createEvent: isAuthenticated }, }

    const server = new GraphQLServer({ typeDefs, resolvers, [authMiddleware], }) @samjulien
  189. graphql-shield Matic Zavadlal @samjulien

  190. None
  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
  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
  193. Models @samjulien

  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
  195. getAll: () => { if(!currentUser || !currentUser.roles.includes('admin')) return null; return

    fetch(`${API_URL}/dog`); } @samjulien
  196. Custom Directives @samjulien

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

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

  199. ⚠ Couples logic to schema. 😅 Can be quite difficult.

    🧪 Requires exhaustive testing. Downsides @samjulien
  200. http://samj.im/graphql-auth @samjulien

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