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. Sam Julien @samjulien Senior Developer Advocate Engineer at Auth0 GDE

    & Angular Collaborator UpgradingAngularJS.com, Thinkster, egghead
  2. The GraphQL server doesn’t necessarily care about users proving who

    they are — it cares who has access to what. @samjulien
  3. { "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
  4. { "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
  5. 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
  6. 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
  7. const server = new ApolloServer({ typeDefs: schema, resolvers, context: async

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

    ({ req }) => { const token = req.headers["Authorization"]; return { token }; } }); @samjulien
  9. function getJwksClientKey(header, callback) { client.getSigningKey(header.kid, function(error, key) { const signingKey

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

    = key.publicKey || key.rsaPublicKey; callback(null, signingKey); }); } @samjulien
  11. try { const payload = await jwt.verify(...); return payload; }

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

    catch (error) { throw new Error("Invalid token!"); } @samjulien
  13. 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
  14. 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
  15. 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
  16. 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
  17. 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
  18. 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
  19. 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
  20. 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
  21. 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
  22. 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
  23. 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
  24. 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
  25. 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
  26. 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
  27. 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
  28. createEvent: isAuthenticated(async (parent, args, context, info) => { const {

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

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

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

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

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

    return await createEventInDb({ ...args, ...context }); } ) ); @samjulien
  35. export const isAuthenticated = (parent, args, context, info) => {

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

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

    return context.currentUser ? skip : new Error("Not authenticated!"); }; @samjulien
  38. import { GraphQLModule } from "@graphql-modules/core"; const MyModule = new

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

    GraphQLModule({ /*...*/ resolversComposition: { "Mutation.createEvent": [ isAuthenticated(), checkPermissions(PERMISSIONS.CREATE) ] } }); @samjulien
  40. const authMiddleware = { Mutation: { createEvent: isAuthenticated }, }

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

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

    const server = new GraphQLServer({ typeDefs, resolvers, [authMiddleware], }) @samjulien
  43. type Event { id: ID description: String! eventLocation: String! location:

    String! @deprecated(reason: "Field `eventLocation` replaces `location`.") } @samjulien
  44. 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
  45. 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
  46. 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
  47. 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
  48. 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
  49. 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
  50. 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
  51. 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
  52. ⚠ Couples logic to schema. Can be quite difficult. Requires

    exhaustive testing. Downsides @samjulien
  53. 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
  54. 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
  55. import { GraphQLModule } from "@graphql-modules/core"; const MyModule = new

    GraphQLModule({ /*...*/ resolversComposition: { "Mutation.createEvent": [ isAuthenticated(), checkPermissions(PERMISSIONS.CREATE) ] } }); @samjulien
  56. const authMiddleware = { Mutation: { createEvent: isAuthenticated }, }

    const server = new GraphQLServer({ typeDefs, resolvers, [authMiddleware], }) @samjulien