Pro Yearly is on sale from $80 to $50! »

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

7beed3a6fa39e12c9e873b903e4d9244?s=47 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!

7beed3a6fa39e12c9e873b903e4d9244?s=128

Sam Julien

February 22, 2020
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

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

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

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

    & Angular Collaborator UpgradingAngularJS.com, Thinkster, egghead
  12. @samjulien

  13. @samjulien

  14. Some Auth Background @samjulien

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

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

    in GraphQL @samjulien
  17. Some Auth Background

  18. Authentication & Authorization @samjulien

  19. Authentication @samjulien

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

  21. Authorization @samjulien

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

  23. @samjulien

  24. @samjulien

  25. @samjulien

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

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

  28. Backend + Frontend on the Same Server @samjulien

  29. GraphQL + NextJS on the Same Server @samjulien

  30. GraphQL + NextJS on the Same Server @samjulien

  31. GraphQL + NextJS on the Same Server @samjulien

  32. GraphQL + NextJS on the Same Server @samjulien

  33. GraphQL + NextJS on the Same Server @samjulien

  34. GraphQL + NextJS on the Same Server @samjulien

  35. GraphQL + NextJS on the Same Server @samjulien

  36. @samjulien

  37. @samjulien

  38. Access @samjulien

  39. Delegated Access @samjulien

  40. GraphQL + NextJS on the Same Server @samjulien

  41. @samjulien

  42. @samjulien

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

  44. @samjulien

  45. Can contain useful information @samjulien

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

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

    create it? @samjulien
  48. Token @samjulien

  49. Authorization Server @samjulien

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

    Authorization Server @samjulien
  51. Access Token @samjulien

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

    @samjulien
  53. @samjulien

  54. @samjulien

  55. @samjulien

  56. @samjulien

  57. @samjulien

  58. Access Token @samjulien

  59. Authorization: Bearer <token> @samjulien

  60. @samjulien

  61. Can contain useful information @samjulien

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

    @samjulien
  63. JSON Web Tokens (JWTs)

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

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

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

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

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

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

    and Token Type) { "alg": "HS256", "typ": "JWT" } @samjulien
  70. 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
  71. eyJhbGciOiJIUzI1NiIsInR5c CI6IkpXVCJ9.eyJzdWIiOiIxM jM0NTY3ODkwIiwibmFtZSI6Ik hlbGxvIEdyYXBoUUwiLCJlbWF pbCI6ImhlbGxvQGdyYXBocWwu Y29tIiwiaWF0IjoxNTE2MjM5M DIyfQ.cJutaCScQJXsGTL6ynH 7BRMnQj_P2yl0a58jUhnLDq8 Verify Signature

    x Signature ✍ @samjulien
  72. @samjulien

  73. Can contain useful information @samjulien

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

    @samjulien
  75. { "sub": "1234567890", "name": “Hello GraphQL”, "iat": 1516239022, "https://hasura.io/jwt/claims": {

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

    "x-hasura-allowed-roles": ["editor","user", "mod"], "x-hasura-default-role": "user", "x-hasura-user-id": "1234567890", "x-hasura-org-id": "123", "x-hasura-custom": "custom-value" } } @samjulien
  77. @samjulien

  78. @samjulien

  79. @samjulien

  80. @samjulien

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

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

  83. How will you be sure you… @samjulien

  84. How will you be sure you… @samjulien

  85. How will you be sure you… Implement proper password controls?

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

    Implement secure password recovery mechanisms? Transmit passwords securely? Correctly implement authentication and error messages? Prevent brute force attacks? @samjulien
  87. Consider outsourcing this piece of your app. @samjulien

  88. @samjulien

  89. @samjulien

  90. Authorization in GraphQL

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

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

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

  94. So how do we do that? @samjulien

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

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

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

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

  99. verifyToken(token): Payload @samjulien

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

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

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

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

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

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

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

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

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

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

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

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

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

  113. Where do we use this? @samjulien

  114. Inside of resolvers? @samjulien

  115. Back in Context @samjulien

  116. const createContext = async req => { const db =

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

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

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

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

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

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

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

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

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

    ({ req }) => createContext(req) }); @samjulien
  126. createEvent: async (parent, args, context, info) => { if (!context.currentUser)

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

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

    { throw new Error("Must be logged in for this!"); } const { currentUser } = context; if (hasPermissions(currentUser.permissions, PERMISSIONS.CREATE)) { return await createEventInDb({ ...args, ...context }); } }; @samjulien
  129. Wrapping (Composing) Resolvers @samjulien

  130. export const isAuthenticated = next => (parent, args, context, info)

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

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

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

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

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

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

    currentUser } = context; if (hasPermissions(currentUser.permissions, PERMISSIONS.CREATE)) { return await createEventInDb({ ...args, ...context }); } }); @samjulien
  137. export const checkPermission = permission => next => ( parent,

    args, context, info ) => { if (!hasPermissions(context.currentUser.permissions, permission)) { throw new Error("You don't have permission!"); } return next(parent, args, context, info); }; @samjulien
  138. createEvent: isAuthenticated( checkPermissions(PERMISSIONS.CREATE)( async (parent, args, context, info) => {

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

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

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

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

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

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

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

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

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

    { return await createEventInDb({ ...args, ...context }); } ); @samjulien
  148. graphql-modules Uri Goldshtein (& Many Others) @samjulien

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

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

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

  152. graphql-middleware Prisma @samjulien

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

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

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

    const server = new GraphQLServer({ typeDefs, resolvers, [authMiddleware], }) @samjulien
  156. None
  157. Custom Directives @samjulien

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

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

    String! @deprecated(reason: "Field `eventLocation` replaces `location`.") } @samjulien
  160. Directives @samjulien

  161. Part of the GraphQL Spec Directives @samjulien

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

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

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

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

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

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

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

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

  170. class HasPermissionDirective extends SchemaDirectiveVisitor { visitFieldDefinition(field) { const { permission

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

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

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

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

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

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

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

    } = this.args; const { resolve = defaultFieldResolver } = field; field.resolve = ({ ...args }) => { const context = args[2]; if (!context.currentUser) { throw new Error("Must be logged in!"); } if (hasPermission(context.currentUser, permission)) { return resolve.apply(this, args); } else { throw new Error("Not authorized!"); } }; } } @samjulien
  178. const schema = makeExecutableSchema({ typeDefs, schemaDirectives: { hasPermission: HasPermissionDirective }

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

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

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

  182. Downsides @samjulien

  183. ⚠ Couples logic to schema. Downsides @samjulien

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

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

    exhaustive testing. Downsides @samjulien
  186. None
  187. None
  188. Let’s Review

  189. @samjulien

  190. GraphQL + NextJS on the Same Server @samjulien

  191. @samjulien

  192. @samjulien

  193. @samjulien

  194. @samjulien

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

  196. @samjulien

  197. Can contain useful information @samjulien

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

    @samjulien
  199. const createContext = async req => { const db =

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

    { throw new Error("Must be logged in for this!"); } const { currentUser } = context; if (hasPermissions(currentUser.permissions, PERMISSIONS.CREATE)) { return await createEventInDb({ ...args, ...context }); } }; @samjulien
  201. graphql-resolvers Lucas Constantino @samjulien

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

    { return await createEventInDb({ ...args, ...context }); } ); @samjulien
  203. graphql-modules Uri Goldshtein (& Many Others) @samjulien

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

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

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

    const server = new GraphQLServer({ typeDefs, resolvers, [authMiddleware], }) @samjulien
  207. directive @hasPermission(permission: String) on FIELD_DEFINITION @samjulien

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

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

  211. Design the schema to require only as much auth as

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

    or the field level. @samjulien
  213. Don’t design mutations that can change arbitrary objects. @samjulien

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

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

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