iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

Auth

GraphQL has no built-in authentication or authorization — the runtime trusts the resolvers. The patterns that work: authenticate at the HTTP layer (cookie, JWT, mTLS), put the user on the context, and let each resolver decide whether the requester is allowed to see the field. Field-level checks beat route-level checks because clients can compose any field combination they like.

Authentication + per-field authorization with context

EXAMPLE
// schema.graphql
scalar DateTime

type User {
  id: ID!
  email: String!                      # visible to the owner and admins
  publicHandle: String!               # visible to anyone
  emailVerifiedAt: DateTime           # owner + admins
}

type Mutation {
  promoteToAdmin(userId: ID!): User!  # admins only
}

type Query {
  me: User                            # authenticated requesters
  user(id: ID!): User                 # respects per-field rules
}

// --- context: who is making the request? ---
async function context({ req }) {
  const auth = req.headers.authorization?.replace(/^Bearer /i, '');
  if (!auth) return { user: null };
  try {
    const claims = verifyJwt(auth);
    const user = await db.users.findById(claims.sub);
    return { user };
  } catch {
    return { user: null };
  }
}

// --- resolvers express access rules in code ---
const resolvers = {
  Query: {
    me: (_, __, { user }) => user ?? null,
    user: (_, { id }) => db.users.findById(id),
  },
  Mutation: {
    promoteToAdmin: async (_, { userId }, { user }) => {
      requireAdmin(user);
      return db.users.update(userId, { role: 'admin' });
    },
  },

  User: {
    // Field-level authorization. Same field, different visibility per requester.
    email: (parent, _, { user }) => {
      if (!user) return null;
      if (user.id === parent.id || user.role === 'admin') return parent.email;
      throw new ForbiddenError('email not visible to this requester');
    },
    emailVerifiedAt: (parent, _, { user }) => {
      if (!user) return null;
      if (user.id === parent.id || user.role === 'admin') return parent.emailVerifiedAt;
      return null;
    },
    publicHandle: (parent) => parent.publicHandle,
  },
};

// --- guards ---
class AuthError      extends Error { extensions = { code: 'UNAUTHENTICATED' }; }
class ForbiddenError extends Error { extensions = { code: 'FORBIDDEN' }; }

function requireUser(user)  { if (!user) throw new AuthError('login required'); }
function requireAdmin(user) { requireUser(user); if (user.role !== 'admin') throw new ForbiddenError('admin only'); }

// --- on the client, structure errors with extensions.code ---
// {
//   "data": { "user": { "id": "u1", "publicHandle": "alice", "email": null } },
//   "errors": [{ "message": "email not visible", "extensions": { "code": "FORBIDDEN" }, "path": ["user","email"] }]
// }

Why it matters

Field-level checks let you return partial results: the publicHandle resolves while the email field returns null + a structured error. Top-level "throw 403" wipes the entire response, which makes profile pages, search results, and admin panels harder to render correctly.

Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.

Example

Example
// Check ctx.user in resolvers, or use directives like @auth(role: ADMIN).
Try it Yourself »

Discussion

Loading…