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

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

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

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

Sam Julien

March 01, 2021
Tweet

More Decks by Sam Julien

Other Decks in Programming

Transcript

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

    Started in Developer Relations & Guide to Tiny Experiments Developer Microskills Newsletter
  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 withAuth from 'graphql-auth'; const resolvers = { Query: {

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

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

    users: withAuth(['users:view'], (root, args, context) => { ... }), ... } } @samjulien
  41. import { GraphQLModule } from "@graphql-modules/core"; const MyModule = new

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

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

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

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

    const server = new GraphQLServer({ typeDefs, resolvers, [authMiddleware], }) @samjulien
  46. 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
  47. 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
  48. 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
  49. 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
  50. 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
  51. 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
  52. 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
  53. const createContext = async req => { /* previous code

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

    hidden but unchanged */ return { db, token, currentUser, models: { Dog: generateDogModel({ currentUser }), }, }; }; @samjulien
  55. type Event { id: ID description: String! eventLocation: String! location:

    String! @deprecated(reason: "Field `eventLocation` replaces `location`.") } @samjulien
  56. 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
  57. 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
  58. 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
  59. 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
  60. 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
  61. 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
  62. 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
  63. 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
  64. ⚠ Couples logic to schema. 😅 Can be quite difficult.

    🧪 Requires exhaustive testing. Downsides @samjulien
  65. 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
  66. 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
  67. import withAuth from 'graphql-auth'; const resolvers = { Query: {

    users: withAuth(['users:view'], (root, args, context) => { ... }), ... } } @samjulien
  68. import { GraphQLModule } from "@graphql-modules/core"; const MyModule = new

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

    const server = new GraphQLServer({ typeDefs, resolvers, [authMiddleware], }) @samjulien
  70. 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
  71. 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
  72. 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
  73. ⚠ Couples logic to schema. 😅 Can be quite difficult.

    🧪 Requires exhaustive testing. Downsides @samjulien