GraphQL with NestJS
Integrating GraphQL into a NestJS application using Apollo Server or other GraphQL libraries.
GraphQL Authentication and Authorization with NestJS
Authentication and Authorization with GraphQL: An Overview
GraphQL, by itself, doesn't dictate a specific authentication or authorization mechanism. These concerns are typically handled within the server-side implementation, allowing you to leverage existing and robust security strategies.
Authentication
Authentication verifies the *identity* of a user. It answers the question: "Who is this?". Common authentication methods used with GraphQL include:
- Username/Password: The classic approach, validating credentials against a user database.
- JSON Web Tokens (JWTs): A standard for securely transmitting information as a JSON object. The server signs the JWT with a secret key, and the client includes the JWT in subsequent requests.
- OAuth 2.0: Delegating authentication to a third-party provider (e.g., Google, Facebook).
- API Keys: Simple tokens that grant access to specific APIs.
Authorization
Authorization determines what a user is *allowed* to do. It answers the question: "Is this user allowed to perform this action?". Authorization checks typically occur *after* successful authentication. Common authorization models include:
- Role-Based Access Control (RBAC): Users are assigned roles, and roles are granted permissions.
- Attribute-Based Access Control (ABAC): Access decisions are based on attributes of the user, resource, and environment.
- Policy-Based Access Control (PBAC): Access is governed by policies that define the rules for access.
Crucially, authentication and authorization are implemented in the *GraphQL server* layer, usually within resolvers or middleware.
Implementing Authentication and Authorization with JWTs in a NestJS GraphQL API
JWT-Based Authentication Flow
- User Login: The user provides credentials (e.g., username and password) to the server.
- Credential Validation: The server validates the credentials against a user database.
- JWT Generation: If the credentials are valid, the server generates a JWT containing user information (e.g., user ID, roles).
- JWT Transmission: The server sends the JWT back to the client (typically in the response body or as an HTTP-only cookie).
- Subsequent Requests: The client includes the JWT in the `Authorization` header (using the `Bearer` scheme) of subsequent requests to the GraphQL API.
- JWT Verification: The GraphQL server intercepts the request, extracts the JWT from the `Authorization` header, and verifies its signature using the secret key.
- User Context Creation: If the JWT is valid, the server extracts the user information from the JWT and makes it available to the resolvers (often through the GraphQL context).
- Authorization Checks: Before executing a resolver, the server checks if the user has the necessary permissions to access the requested data or perform the requested action.
Example Implementation with NestJS
1. Install Dependencies
npm install @nestjs/jwt @nestjs/passport passport passport-jwt bcrypt
2. Create Authentication Module
Create a module (e.g., `auth.module.ts`) to handle authentication-related logic. This module will contain:
- JWT Service: To generate and verify JWTs.
- Local Strategy (for username/password authentication): Extends `PassportStrategy` to handle local authentication.
- JWT Strategy: Extends `PassportStrategy` to verify JWTs.
// auth.module.ts
import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { PassportModule } from '@nestjs/passport';
import { AuthService } from './auth.service';
import { JwtStrategy } from './strategies/jwt.strategy';
import { LocalStrategy } from './strategies/local.strategy';
import { UsersModule } from '../users/users.module'; // Assuming you have a UsersModule
import { AuthResolver } from './auth.resolver';
@Module({
imports: [
UsersModule,
PassportModule,
JwtModule.register({
secret: 'YOUR_SECRET_KEY', // Replace with a strong, environment-specific secret
signOptions: { expiresIn: '60s' }, // Adjust expiration time as needed
}),
],
providers: [AuthService, JwtStrategy, LocalStrategy, AuthResolver],
exports: [AuthService], // Export AuthService if you need it elsewhere
})
export class AuthModule {}
3. Create JWT Strategy
(e.g., `jwt.strategy.ts`) to handle JWT verification.
// jwt.strategy.ts
import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { UsersService } from '../../users/users.service'; // Import UsersService
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(private readonly usersService: UsersService) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: 'YOUR_SECRET_KEY', // Same secret as JwtModule
});
}
async validate(payload: any) {
const user = await this.usersService.findOne(payload.sub); // Find the user by ID
if (!user) {
return null; // or throw an UnauthorizedException
}
return { userId: payload.sub, username: payload.username, roles: payload.roles }; // Add relevant user info
}
}
4. Create Local Strategy
(e.g., `local.strategy.ts`) to handle username/password authentication.
// local.strategy.ts
import { Strategy } from 'passport-local';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { AuthService } from '../auth.service';
@Injectable()
export class LocalStrategy extends PassportStrategy(Strategy) {
constructor(private authService: AuthService) {}
async validate(username: string, password: string): Promise {
const user = await this.authService.validateUser(username, password);
if (!user) {
throw new UnauthorizedException();
}
return user; // Return the user object
}
}
5. Create Authentication Service
(e.g., `auth.service.ts`) to handle login and JWT generation.
// auth.service.ts
import { Injectable } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { UsersService } from '../users/users.service'; // Assuming you have a UsersService
import * as bcrypt from 'bcrypt';
@Injectable()
export class AuthService {
constructor(
private usersService: UsersService,
private jwtService: JwtService,
) {}
async validateUser(username: string, pass: string): Promise {
const user = await this.usersService.findOneByUsername(username);
if (user && await bcrypt.compare(pass, user.password)) {
const { password, ...result } = user;
return result;
}
return null;
}
async login(user: any) {
const payload = { username: user.username, sub: user.userId, roles: user.roles };
return {
access_token: this.jwtService.sign(payload),
};
}
}
6. Create Authentication Resolver
(e.g., `auth.resolver.ts`) to expose the login mutation.
// auth.resolver.ts
import { Resolver, Mutation, Args, Context } from '@nestjs/graphql';
import { AuthService } from './auth.service';
import { LoginInput } from './dto/login.input';
import { UseGuards } from '@nestjs/common';
import { LocalAuthGuard } from './guards/local-auth.guard';
@Resolver()
export class AuthResolver {
constructor(private authService: AuthService) {}
@Mutation(() => String) // Replace String with your desired return type (e.g., a LoginResponse DTO)
@UseGuards(LocalAuthGuard)
async login(@Args('loginInput') loginInput: LoginInput, @Context() context: any): Promise {
// The LocalAuthGuard will populate the 'user' property on the request object
// If authentication is successful.
return this.authService.login(context.req.user);
}
}
7. Login Input DTO
(e.g., `login.input.ts`)
// login.input.ts
import { InputType, Field } from '@nestjs/graphql';
@InputType()
export class LoginInput {
@Field()
username!: string;
@Field()
password!: string;
}
8. Create Auth Guards
Guards (e.g., `jwt-auth.guard.ts`, `roles.guard.ts`)
Example `jwt-auth.guard.ts`
// jwt-auth.guard.ts
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { GqlExecutionContext } from '@nestjs/graphql';
import { AuthGuard } from '@nestjs/passport';
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') implements CanActivate{
constructor() {
super();
}
getRequest(context: ExecutionContext) {
const ctx = GqlExecutionContext.create(context);
return ctx.getContext().req;
}
canActivate(context: ExecutionContext): boolean | Promise | Observable {
return super.canActivate(context);
}
}
Example `local-auth.guard.ts`
// local-auth.guard.ts
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { GqlExecutionContext } from '@nestjs/graphql';
@Injectable()
export class LocalAuthGuard extends AuthGuard('local') implements CanActivate {
constructor() {
super();
}
getRequest(context: ExecutionContext) {
const ctx = GqlExecutionContext.create(context);
const request = ctx.getContext().req;
request.body = ctx.getArgs().loginInput;
return request;
}
}
Example `roles.guard.ts`
// roles.guard.ts
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { GqlExecutionContext } from '@nestjs/graphql';
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const requiredRoles = this.reflector.getAllAndOverride('roles', [
context.getHandler(),
context.getClass(),
]);
if (!requiredRoles) {
return true;
}
const ctx = GqlExecutionContext.create(context);
const { req } = ctx.getContext();
const user = req.user; // Assuming user info is attached to the request by JwtStrategy
if (!user) {
return false; // Or throw an UnauthorizedException
}
return requiredRoles.some((role) => user.roles?.includes(role));
}
}
9. Using the Guards
// Example usage in a resolver
import { Resolver, Query, Mutation, Args } from '@nestjs/graphql';
import { UseGuards, SetMetadata } from '@nestjs/common';
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
import { RolesGuard } from '../auth/guards/roles.guard';
// Define roles metadata for access control
export const Roles = (...roles: string[]) => SetMetadata('roles', roles);
@Resolver()
export class MyResolver {
@Query(() => String)
@UseGuards(JwtAuthGuard) // Ensure user is authenticated
helloAuthenticated(): string {
return 'Hello Authenticated User!';
}
@Query(() => String)
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles('admin') // Only users with the 'admin' role can access this
helloAdmin(): string {
return 'Hello Admin!';
}
}
Integrating NestJS Interceptors for Access Control (Advanced)
Interceptors in NestJS provide a powerful way to intercept and modify requests and responses. They can be used for tasks such as logging, caching, and, crucially, access control.
Unlike guards, which primarily prevent access, interceptors can also *modify* the data being returned based on the user's permissions. For example, an interceptor could filter sensitive fields from a response for users without administrative privileges.
Example: Data Masking Interceptor
This example demonstrates how an interceptor can mask sensitive data based on the user's roles. Assume that the user context is already established by a JWT guard.
// data-masking.interceptor.ts
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { GqlExecutionContext } from '@nestjs/graphql';
interface ResponseData {
sensitiveField?: string;
otherField: string;
}
@Injectable()
export class DataMaskingInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable {
return next.handle().pipe(
map((data: ResponseData) => {
const ctx = GqlExecutionContext.create(context);
const { req } = ctx.getContext();
const user = req.user;
if (user && user.roles && user.roles.includes('admin')) {
// Admins see everything
return data;
} else {
// Non-admins get the sensitive field masked
return { ...data, sensitiveField: '***MASKED***' };
}
}),
);
}
}
Applying the Interceptor
import { UseInterceptors } from '@nestjs/common';
import { DataMaskingInterceptor } from './data-masking.interceptor';
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
@Resolver()
export class MyResolver {
@Query(() => MyResponseType)
@UseGuards(JwtAuthGuard)
@UseInterceptors(DataMaskingInterceptor)
getData(): MyResponseType {
// Your logic to fetch data. Assume it returns { sensitiveField: 'secret', otherField: 'public' }
return { sensitiveField: 'secret', otherField: 'public' };
}
}
This interceptor will examine the `user` object attached to the request by the `JwtAuthGuard`. If the user has the 'admin' role, the data is returned as is. Otherwise, the `sensitiveField` is masked. This allows for fine-grained control over what data is exposed to different users.
Key Considerations
- Secret Key Management: Never hardcode your JWT secret key. Store it as an environment variable.
- Token Expiration: Use reasonable token expiration times to minimize the risk of stolen tokens.
- Refresh Tokens: Implement refresh tokens to allow users to obtain new access tokens without re-authenticating.
- Error Handling: Provide informative error messages to the client in case of authentication or authorization failures.
- Testing: Thoroughly test your authentication and authorization mechanisms to ensure they are working as expected.
- CORS: Configure CORS properly to prevent cross-origin request vulnerabilities.
- Input Validation: Validate all user input to prevent injection attacks.