Aller au contenu

Authorization

Authorization controls who can access what data in your GraphQL API. Type GraphQL Query provides powerful authorization capabilities through the @Authorize() decorator with support for roles, permissions, attributes, and complex authorization expressions.

This guide will help you understand:

  • ✅ How to use the @Authorize() decorator
  • ✅ How to implement role-based, permission-based, and attribute-based authorization
  • ✅ How to combine authorizations with AND, OR, and NOT operators
  • ✅ How to configure authorization at resolver, field, and class levels
  • ✅ How to create custom authorization checkers
  • ✅ How to configure the authorization system

Authorization is implemented using decorators and middleware. Type GraphQL Query provides the @Authorize() decorator which extends TypeGraphQL’s @Authorized() with support for complex authorization expressions.

import { Query, Resolver } from '@lib/type-graphql';
import { Authorize } from '@lib/type-graphql-query';
import { Product } from './product';
@Resolver()
export class ProductResolver {
@Authorize()
@Query(() => [Product])
async products(): Promise<Product[]> {
// Only authenticated users can access
}
}

The @Authorize() decorator is the main way to protect your GraphQL resolvers and fields. It accepts either a string expression or an Authorization object.

import { Authorize } from '@lib/type-graphql-query';
import { Query, Resolver } from '@lib/type-graphql';
@Resolver()
export class ProductResolver {
// Require authentication only
@Authorize()
@Query(() => [Product])
async products(): Promise<Product[]> {
return [];
}
// Require specific role
@Authorize("role('admin')")
@Query(() => [Product])
async adminProducts(): Promise<Product[]> {
return [];
}
// Require specific permission
@Authorize("permission('view_products')")
@Query(() => [Product])
async viewProducts(): Promise<Product[]> {
return [];
}
}

You can use the authorization factory functions for type-safe and readable authorization:

import { Authorize, Role, Permission, AllOf, AnyOf, Not } from '@lib/type-graphql-query';
import { Query, Resolver, Mutation } from '@lib/type-graphql';
@Resolver()
export class ProductResolver {
// Single role
@Authorize(Role('admin'))
@Query(() => [Product])
async adminProducts(): Promise<Product[]> {
return [];
}
// Single permission
@Authorize(Permission('view_products'))
@Query(() => [Product])
async viewProducts(): Promise<Product[]> {
return [];
}
// Multiple roles (OR)
@Authorize(AnyOf(Role('admin'), Role('moderator')))
@Query(() => [Product])
async adminOrModeratorProducts(): Promise<Product[]> {
return [];
}
// Combined requirements (AND)
@Authorize(AllOf(Role('admin'), Permission('view_products')))
@Query(() => [Product])
async adminWithPermission(): Promise<Product[]> {
return [];
}
// Negation (NOT)
@Authorize(Not(Role('guest')))
@Mutation(() => Product)
async createProduct(): Promise<Product> {
// Anyone except guests can create
}
// Complex authorization
@Authorize(
AllOf(
AnyOf(Role('admin'), Role('moderator')),
Permission('manage_products'),
Not(AnyOf(Role('banned'), Attribute('is_suspended'))),
),
)
@Mutation(() => Product)
async updateProduct(): Promise<Product> {
// Must be admin or moderator
// AND have manage_products permission
// AND NOT be banned or suspended
}
}

You can also use string expressions directly. When using string expressions, you can either specify the authorization type explicitly or rely on defaultAuthorizationType:

@Resolver()
export class ProductResolver {
// Simple string expression with explicit function
@Authorize("role('admin')")
@Query(() => [Product])
async adminProducts(): Promise<Product[]> {
return [];
}
// Simple string expression without function (uses defaultAuthorizationType)
// If defaultAuthorizationType is 'permission', this is equivalent to:
// @Authorize("permission('product.view')")
@Authorize('product.view')
@Query(() => [Product])
async viewProducts(): Promise<Product[]> {
return [];
}
// Complex string expression with AND
@Authorize("(role('admin') AND permission('view_products'))")
@Query(() => [Product])
async complexQuery(): Promise<Product[]> {
return [];
}
// Complex string expression with OR
@Authorize("(role('admin') OR role('moderator'))")
@Query(() => [Product])
async adminOrModerator(): Promise<Product[]> {
return [];
}
// Complex string expression with NOT
@Authorize("!(role('guest'))")
@Mutation(() => Product)
async createProduct(): Promise<Product> {
return {};
}
}

You can protect individual fields in your GraphQL types:

import { ObjectType, Field } from '@lib/type-graphql';
import { Authorize, Role, Permission } from '@lib/type-graphql-query';
@ObjectType()
export class Product {
@Field()
id!: string;
@Field()
name!: string;
// Only admins can see the cost
@Authorize(Role('admin'))
@Field()
cost!: number;
// Users with view_prices permission can see the price
@Authorize(Permission('view_prices'))
@Field()
price!: number;
}

You can apply authorization at the resolver class level:

import { Resolver, Query } from '@lib/type-graphql';
import { Authorize, Role } from '@lib/type-graphql-query';
@Resolver()
@Authorize(Role('admin')) // All methods in this resolver require admin role
export class AdminResolver {
@Query(() => String)
async adminOnly(): Promise<string> {
return 'Admin only';
}
// You can override class-level authorization
@Authorize(Role('super_admin'))
@Query(() => String)
async superAdminOnly(): Promise<string> {
return 'Super admin only';
}
}

You can implement custom authorization logic:

@Authorized()
@Query(() => Product)
async product(
@Args("id") id: string,
@Ctx() ctx: Context
): Promise<Product> {
// Custom authorization logic
if (ctx.user.role !== "ADMIN" && product.userId !== ctx.user.id) {
throw new Error("Unauthorized");
}
return product;
}

Type GraphQL Query provides three predefined authorization types:

  • permission - Checks if the user has specific permissions
  • role - Checks if the user has specific roles
  • attribute - Checks if the user has specific attributes

These authorization types correspond to the properties returned by getUserAuthorizationsFor:

  • permission type → permissions property (array of strings)
  • role type → roles property (array of strings)
  • attribute type → attributes property (array of strings)

Type GraphQL Query provides factory functions to create type-safe authorization objects:

Creates a role-based authorization:

import { Role } from "@lib/type-graphql-query";
@Authorize(Role("admin"))
@Query(() => [Product])
async adminProducts(): Promise<Product[]> {
return [];
}

Creates a permission-based authorization:

import { Permission } from "@lib/type-graphql-query";
@Authorize(Permission("view_products"))
@Query(() => [Product])
async viewProducts(): Promise<Product[]> {
return [];
}

Creates an attribute-based authorization:

import { Attribute } from "@lib/type-graphql-query";
@Authorize(Attribute("can_delete"))
@Mutation(() => Product)
async deleteProduct(): Promise<Product> {
return {};
}

Combines multiple authorizations with AND logic:

import { AllOf, Role, Permission } from "@lib/type-graphql-query";
@Authorize(AllOf(
Role("admin"),
Permission("manage_products")
))
@Mutation(() => Product)
async createProduct(): Promise<Product> {
// User must have BOTH admin role AND manage_products permission
return {};
}

Combines multiple authorizations with OR logic:

import { AnyOf, Role } from "@lib/type-graphql-query";
@Authorize(AnyOf(
Role("admin"),
Role("moderator")
))
@Query(() => [Product])
async products(): Promise<Product[]> {
// User must have admin role OR moderator role
return [];
}

Negates an authorization:

import { Not, Role } from "@lib/type-graphql-query";
@Authorize(Not(Role("guest")))
@Mutation(() => Product)
async createProduct(): Promise<Product> {
// User must NOT have guest role
return {};
}

You can combine these functions for complex authorization logic:

import { AllOf, AnyOf, Not, Role, Permission } from "@lib/type-graphql-query";
@Authorize(AllOf(
// Must be admin OR moderator
AnyOf(Role("admin"), Role("moderator")),
// AND have manage_products permission
Permission("manage_products"),
// AND NOT be banned
Not(Role("banned"))
))
@Mutation(() => Product)
async updateProduct(): Promise<Product> {
// Complex authorization: (admin OR moderator) AND manage_products AND NOT banned
return {};
}

The AuthOptions<TContextType> interface allows you to configure the authorization system when building your GraphQL schema.

import { buildSchema } from '@lib/type-graphql-query';
import type { AuthOptions } from '@lib/type-graphql-query';
interface MyContext {
user?: {
id: string;
roles: string[];
permissions: string[];
};
}
const authOptions: AuthOptions<MyContext> = {
defaultAuthorizationType: 'permission',
getUserAuthorizationsFor: async ({ resolverData, context, authorizations }) => {
const user = resolverData.context.user;
if (!user) {
return false; // User not authenticated
}
// Return only the authorizations needed for this check
return {
roles: user.roles,
permissions: user.permissions,
};
},
};
const schema = await buildSchema({
// ... other options
auth: authOptions,
});

The default authorization type to use when a string expression doesn’t specify a function. This allows you to write shorter expressions without explicitly specifying the authorization type.

When you write @Authorize('product.view'), it’s equivalent to @Authorize("permission('product.view')") or @Authorize(Permission('product.view')) if defaultAuthorizationType is set to 'permission'.

The function name in string expressions (like permission, role, attribute) must match one of the available authorization types. The defaultAuthorizationType is only used when no function is specified in the expression.

Example:

const authOptions: AuthOptions<MyContext> = {
defaultAuthorizationType: 'permission', // Default to 'permission'
};
// These are equivalent:
@Authorize('product.view')
@Authorize("permission('product.view')")
@Authorize(Permission('product.view'))

Changing the default type:

const authOptions: AuthOptions<MyContext> = {
defaultAuthorizationType: 'role', // Use 'role' as default instead of 'permission'
};
// Now these are equivalent:
@Authorize('admin')
@Authorize("role('admin')")
@Authorize(Role('admin'))

Note: The defaultAuthorizationType must be a valid authorization type that exists in your system. The predefined types are 'permission', 'role', and 'attribute'. You can also define custom authorization types.

A function that retrieves the current user’s authorizations for a specific scope. This function is optimized to fetch only the authorizations needed for the current check, not all user authorizations.

Parameters:

  • resolverData - GraphQL resolver data
  • context - Execution context
  • authorizations - Authorizations to check (scope of the verification)

Returns:

  • Authorizations - The user’s authorizations in the requested scope. This object should contain properties matching the authorization types:
    • permissions - Array of permission strings (for permission type)
    • roles - Array of role strings (for role type)
    • attributes - Array of attribute strings (for attribute type)
  • false - If the user is not authenticated

Authorization Type Mapping:

  • Authorization type permission uses the permissions property (plural)
  • Authorization type role uses the roles property (plural)
  • Authorization type attribute uses the attributes property (plural)

Example:

const authOptions: AuthOptions<MyContext> = {
getUserAuthorizationsFor: async ({ resolverData, context, authorizations }) => {
const user = resolverData.context.user;
if (!user) {
return false;
}
// Only fetch the authorizations needed for this specific check
// This optimizes database queries
const userAuthorizations: Authorizations = {};
// Check if roles are needed (for 'role' authorization type)
if (authorizations.roles) {
userAuthorizations.roles = await getUserRoles(user.id, authorizations.roles);
}
// Check if permissions are needed (for 'permission' authorization type)
if (authorizations.permissions) {
userAuthorizations.permissions = await getUserPermissions(
user.id,
authorizations.permissions,
);
}
// Check if attributes are needed (for 'attribute' authorization type)
if (authorizations.attributes) {
userAuthorizations.attributes = await getUserAttributes(user.id, authorizations.attributes);
}
return userAuthorizations;
},
};

Custom authorization checkers for specific authorization types. Allows you to implement custom logic for different authorization types.

The isAuthorized function passed to the checker can be used to check other authorizations (not the one being checked, as that would create an infinite loop). This is particularly useful when implementing complex authorization logic, such as the AllOf operator which calls isAuthorized on each element of the authorizations array.

Critical: Never call isAuthorized(authorization) with the same authorization parameter that the checker is currently processing. This will create an infinite loop because isAuthorized calls the checker, which would call isAuthorized again with the same authorization, and so on.

Use isAuthorized only to check different authorizations (e.g., when checking nested authorizations like AllOf(Role('admin'), Permission('view'))).

Example: Custom Checker for a Single Type

import type { AuthorizationChecker, Authorization } from '@lib/type-graphql-query';
const customChecker: AuthorizationChecker<MyContext> = ({
resolverData,
context,
authorizations,
authorization,
isAuthorized,
}) => {
// Custom logic for checking authorization
// No need to check authorization.type since this checker is only for 'custom' type
const customValue = authorization.value as string;
return checkCustomAuthorization(customValue, context);
};
const authOptions: AuthOptions<MyContext> = {
authorizationCheckers: {
custom: customChecker,
},
};

Example: Custom Checker for Multiple Types

If your checker handles multiple authorization types, you need to check the type:

import type { AuthorizationChecker, Authorization } from '@lib/type-graphql-query';
const multiTypeChecker: AuthorizationChecker<MyContext> = ({
resolverData,
context,
authorizations,
authorization,
isAuthorized,
}) => {
if (authorization.type === 'custom') {
// Handle custom type
const customValue = authorization.value as string;
return checkCustomAuthorization(customValue, context);
}
if (authorization.type === 'attribute') {
// Handle attribute type
const attributeValue = authorization.value as string;
return authorizations.attributes?.includes(attributeValue) ?? false;
}
// For other types, use default behavior (check directly in authorizations)
// DO NOT call isAuthorized(authorization) here as it would create an infinite loop
const authKey = `${authorization.type}s`;
return authorizations[authKey]?.includes(authorization.value as string) ?? false;
};
const authOptions: AuthOptions<MyContext> = {
authorizationCheckers: {
custom: multiTypeChecker,
attribute: multiTypeChecker, // Same checker for multiple types
},
};

Example: Using isAuthorized for Complex Logic

The isAuthorized function is useful when you need to check nested authorizations:

import type { AuthorizationChecker, Authorization } from '@lib/type-graphql-query';
import { AllOf, Role, Permission } from '@lib/type-graphql-query';
const complexChecker: AuthorizationChecker<MyContext> = ({
resolverData,
context,
authorizations,
authorization,
isAuthorized,
}) => {
if (authorization.type === 'custom') {
const customValue = authorization.value as string;
// Use isAuthorized to check other authorizations
// This is safe because we're checking different authorizations
if (customValue === 'admin_with_permission') {
// Check if user has admin role AND specific permission
return isAuthorized(AllOf(Role('admin'), Permission('manage_products')));
}
return checkCustomAuthorization(customValue, context);
}
// For other types, use default behavior
const authKey = `${authorization.type}s`;
return authorizations[authKey]?.includes(authorization.value as string) ?? false;
};
const authOptions: AuthOptions<MyContext> = {
authorizationCheckers: {
custom: complexChecker,
},
};
import { buildSchema } from '@lib/type-graphql-query';
import type { AuthOptions, AuthorizationChecker } from '@lib/type-graphql-query';
interface MyContext {
user?: {
id: string;
roles: string[];
permissions: string[];
attributes: string[];
};
}
const attributeChecker: AuthorizationChecker<MyContext> = ({
authorization,
authorizations,
context,
}) => {
// Check if user has the required attribute
const requiredAttribute = authorization.value as string;
return authorizations.attributes?.includes(requiredAttribute) ?? false;
};
const authOptions: AuthOptions<MyContext> = {
defaultAuthorizationType: 'permission',
authorizationCheckers: {
attribute: attributeChecker,
},
getUserAuthorizationsFor: async ({ resolverData, context, authorizations }) => {
const user = resolverData.context.user;
if (!user) {
return false;
}
// Optimize: only fetch what's needed
// The property names (roles, permissions, attributes) correspond to
// the authorization types (role, permission, attribute)
const result: Authorizations = {};
// For 'role' authorization type → use 'roles' property
if (authorizations.roles?.length) {
result.roles = user.roles.filter((r) => authorizations.roles!.includes(r));
}
// For 'permission' authorization type → use 'permissions' property
if (authorizations.permissions?.length) {
result.permissions = user.permissions.filter((p) => authorizations.permissions!.includes(p));
}
// For 'attribute' authorization type → use 'attributes' property
if (authorizations.attributes?.length) {
result.attributes = user.attributes.filter((a) => authorizations.attributes!.includes(a));
}
return result;
},
};
const schema = await buildSchema({
resolvers: [ProductResolver, UserResolver],
auth: authOptions,
});
Feature Description Example
Role Role-based authorization @Authorize(Role("admin"))
Permission Permission-based authorization @Authorize(Permission("view_products"))
Attribute Attribute-based authorization @Authorize(Attribute("can_delete"))
AllOf AND logic @Authorize(AllOf(Role("admin"), Permission("view")))
AnyOf OR logic @Authorize(AnyOf(Role("admin"), Role("moderator")))
Not NOT logic @Authorize(Not(Role("guest")))

Continue learning:

  1. 🔧 Resolvers - Resolver implementation
  2. 🔍 Queries - Query operations
  3. ✏️ Mutations - Mutation operations
  4. 🎯 ContextFilter - Context-aware filtering