Skip to content

Latest commit

 

History

History
218 lines (170 loc) · 6.68 KB

authorization.md

File metadata and controls

218 lines (170 loc) · 6.68 KB
title
Authorization

Authorization is a core feature used in almost all APIs. Sometimes we want to restrict data access or actions for a specific group of users.

In express.js (and other Node.js frameworks) we use middleware for this, like passport.js or the custom ones. However, in GraphQL's resolver architecture we don't have middleware so we have to imperatively call the auth checking function and manually pass context data to each resolver, which might be a bit tedious.

That's why authorization is a first-class feature in TypeGraphQL!

Declaration

First, we need to use the @Authorized decorator as a guard on a field, query or mutation. Example object type field guards:

@ObjectType()
class MyObject {
  @Field()
  publicField: string;

  @Authorized()
  @Field()
  authorizedField: string;

  @Authorized("ADMIN")
  @Field()
  adminField: string;

  @Authorized(["ADMIN", "MODERATOR"])
  @Field({ nullable: true })
  hiddenField?: string;
}

We can leave the @Authorized decorator brackets empty or we can specify the role/roles that the user needs to possess in order to get access to the field, query or mutation. By default the roles are of type string but they can easily be changed as the decorator is generic - @Authorized<number>(1, 7, 22).

Thus, authorized users (regardless of their roles) can only read the publicField or the authorizedField from the MyObject object. They will receive null when accessing the hiddenField field and will receive an error (that will propagate through the whole query tree looking for a nullable field) for the adminField when they don't satisfy the role constraints.

Sample query and mutation guards:

@Resolver()
class MyResolver {
  @Query()
  publicQuery(): MyObject {
    return {
      publicField: "Some public data",
      authorizedField: "Data for logged users only",
      adminField: "Top secret info for admin",
    };
  }

  @Authorized()
  @Query()
  authedQuery(): string {
    return "Authorized users only!";
  }

  @Authorized("ADMIN", "MODERATOR")
  @Mutation()
  adminMutation(): string {
    return "You are an admin/moderator, you can safely drop the database ;)";
  }
}

Authorized users (regardless of their roles) will be able to read data from the publicQuery and the authedQuery queries, but will receive an error when trying to perform the adminMutation when their roles don't include ADMIN or MODERATOR.

However, declaring @Authorized() on all the resolver's class methods would be not only a tedious task but also an error-prone one, as it's easy to forget to put it on some newly added method, etc. Hence, TypeGraphQL support declaring @Authorized() or the resolver class level. This way you can declare it once per resolver's class but you can still overwrite the defaults and narrows the authorization rules:

@Authorized()
@Resolver()
class MyResolver {
  // this will inherit the auth guard defined on the class level
  @Query()
  authedQuery(): string {
    return "Authorized users only!";
  }

  // this one overwrites the resolver's one
  // and registers roles required for this mutation
  @Authorized("ADMIN", "MODERATOR")
  @Mutation()
  adminMutation(): string {
    return "You are an admin/moderator, you can safely drop the database ;)";
  }
}

Runtime checks

Having all the metadata for authorization set, we need to create our auth checker function. Its implementation may depend on our business logic:

export const customAuthChecker: AuthChecker<ContextType> = (
  { root, args, context, info },
  roles,
) => {
  // Read user from context
  // and check the user's permission against the `roles` argument
  // that comes from the '@Authorized' decorator, eg. ["ADMIN", "MODERATOR"]

  return true; // or 'false' if access is denied
};

The second argument of the AuthChecker generic type is RoleType - used together with the @Authorized decorator generic type.

Auth checker can be also defined as a class - this way we can leverage the dependency injection mechanism:

export class CustomAuthChecker implements AuthCheckerInterface<ContextType> {
  constructor(
    // Dependency injection
    private readonly userRepository: Repository<User>,
  ) {}

  check({ root, args, context, info }: ResolverData<ContextType>, roles: string[]) {
    const userId = getUserIdFromToken(context.token);
    // Use injected service
    const user = this.userRepository.getById(userId);

    // Custom logic, e.g.:
    return user % 2 === 0;
  }
}

The last step is to register the function or class while building the schema:

import { customAuthChecker } from "../auth/custom-auth-checker.ts";

const schema = await buildSchema({
  resolvers: [MyResolver],
  // Register the auth checking function
  // or defining it inline
  authChecker: customAuthChecker,
});

And it's done! 😉

If we need silent auth guards and don't want to return authorization errors to users, we can set the authMode property of the buildSchema config object to "null":

const schema = await buildSchema({
  resolvers: ["./**/*.resolver.ts"],
  authChecker: customAuthChecker,
  authMode: "null",
});

It will then return null instead of throwing an authorization error.

Recipes

We can also use TypeGraphQL with JWT authentication. Here's an example using @apollo/server:

import { ApolloServer } from "@apollo/server";
import { expressMiddleware } from "@apollo/server/express4";
import express from "express";
import jwt from "express-jwt";
import bodyParser from "body-parser";
import { schema } from "./graphql/schema";
import { User } from "./User.type";

// GraphQL path
const GRAPHQL_PATH = "/graphql";

// GraphQL context
type Context = {
  user?: User;
};

// Express
const app = express();

// Apollo server
const server = new ApolloServer<Context>({ schema });
await server.start();

// Mount a JWT or other authentication middleware that is run before the GraphQL execution
app.use(
  GRAPHQL_PATH,
  jwt({
    secret: "TypeGraphQL",
    credentialsRequired: false,
  }),
);

// Apply GraphQL server middleware
app.use(
  GRAPHQL_PATH,
  bodyParser.json(),
  expressMiddleware(server, {
    // Build context
    // 'req.user' comes from 'express-jwt'
    context: async ({ req }) => ({ user: req.user }),
  }),
);

// Start server
await new Promise<void>(resolve => app.listen({ port: 4000 }, resolve));
console.log(`GraphQL server ready at http://localhost:4000/${GRAPHQL_PATH}`);

Then we can use standard, token based authorization in the HTTP header like in classic REST APIs and take advantage of the TypeGraphQL authorization mechanism.

Example

See how this works in the simple real life example.