Last active
February 22, 2024 10:57
-
-
Save GavinRay97/d2c64ef82aef1149248aeafd077293b7 to your computer and use it in GitHub Desktop.
Hasura Nest.js JWT Auth utils (REST + GQL)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { Module } from '@nestjs/common' | |
import { AuthService } from './auth.service' | |
@Module({ | |
providers: [AuthService], | |
exports: [AuthService], | |
}) | |
export class AuthModule {} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { | |
BadRequestException, | |
Injectable, | |
UnauthorizedException, | |
} from '@nestjs/common' | |
import * as jwt from 'jsonwebtoken' | |
import { config } from '../config/environment-variables.interface' | |
import { HasuraJwtClaims, UserJwtClaims } from './hasura-jwt-claims.interface' | |
@Injectable() | |
export class AuthService { | |
parseHasuraJwtSecret() { | |
return JSON.parse(config.get('HASURA_GRAPHQL_JWT_SECRET')) as { | |
type: 'HS256' | 'RS512' | |
key: string | |
} | |
} | |
verifyJwt(token: string): any { | |
try { | |
return jwt.verify(token, this.parseHasuraJwtSecret().key) | |
} catch (err) { | |
throw new UnauthorizedException('Token not valid') | |
} | |
} | |
signJwt(payload: any) { | |
const { type, key } = this.parseHasuraJwtSecret() | |
return jwt.sign(payload, key, { | |
algorithm: type, | |
expiresIn: config.get('JWT_EXPIRE_TIME'), | |
}) | |
} | |
checkUserRoles(user: HasuraJwtClaims, ...roles: string[]) { | |
const claims = user['https://hasura.io/jwt/claims'] | |
const allowedRoles = claims['x-hasura-allowed-roles'] | |
return allowedRoles.some(role => roles.includes(role)) | |
} | |
getTokenFromRequestAuthHeader(request: any): string { | |
const authHeader = request?.headers?.authorization as string | |
if (!authHeader) { | |
throw new BadRequestException('Authorization header not found.') | |
} | |
// Bearer ey.xxxxxxx | |
const [scheme, credentials] = authHeader.split(' ') | |
if (scheme !== 'Bearer') { | |
throw new BadRequestException( | |
`Authentication type \'Bearer\' required. Found \'${scheme}\'`, | |
) | |
} | |
return credentials | |
} | |
generateJWT(user: Record<string, any>) { | |
const payload: UserJwtClaims = { | |
'https://hasura.io/jwt/claims': { | |
'x-hasura-allowed-roles': ['user'], | |
'x-hasura-default-role': 'user', | |
'x-hasura-user-id': String(user['id']), | |
}, | |
} | |
return this.signJwt(payload) | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { createParamDecorator, ExecutionContext } from '@nestjs/common' | |
import { GqlContextType, GqlExecutionContext } from '@nestjs/graphql' | |
/** | |
* This decorator is used to inject @CurrentUser into methods | |
*/ | |
export const CurrentUser = createParamDecorator( | |
(data: unknown, ctx: ExecutionContext) => { | |
if (ctx.getType<GqlContextType>() === 'graphql') { | |
const gqlContext = GqlExecutionContext.create(ctx) | |
return gqlContext.getContext().req.user | |
} | |
return ctx.switchToHttp().getRequest().user | |
}, | |
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
export type HasuraJwtClaims< | |
CustomClaims extends Record<string, string | string[]> = {} | |
> = { | |
'https://hasura.io/jwt/claims': { | |
'x-hasura-default-role': string | |
'x-hasura-allowed-roles': string[] | |
} & CustomClaims | |
} | |
export type UserJwtClaims = HasuraJwtClaims<{ | |
'x-hasura-user-id': string | |
}> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common' | |
import { GqlExecutionContext } from '@nestjs/graphql' | |
import { AuthService } from './auth.service' | |
/** | |
* These guards do two things: | |
* | |
* 1. Checks that the JWT token is present in "Authorization" header | |
* 2. Sets "request.user" property to the value of the decoded token | |
* | |
* There are two different implementations (HTTP/GraphQL) | |
* because the request context is slightly different between them. | |
* | |
* As a note, for the GraphQL guard to work, the GraphQLModule registered in AppModule | |
* must pass the HTTP request context through to the GraphQL execution context: | |
* | |
* ```ts | |
* GraphQLModule.forRoot({ | |
* context: ctx => { | |
* // Sets the "request" object for each GQL request to read headers for auth, etc | |
* return { request: ctx.request } | |
* } | |
* }) | |
* ``` | |
* | |
* These guards which set the "request.user" from JWT are combined with the "Roles" guard | |
* to enforce role-based permissions for HTTP endpoints and GraphQL operations. | |
*/ | |
@Injectable() | |
export class HttpJwtGuard implements CanActivate { | |
constructor(private readonly authService: AuthService) {} | |
canActivate(ctx: ExecutionContext): boolean { | |
try { | |
const req = ctx.switchToHttp().getRequest() | |
const token = this.authService.getTokenFromRequestAuthHeader(req) | |
const user = this.authService.verifyJwt(token) | |
req.user = user | |
return true | |
} catch (err) { | |
console.log('[HttpJwtGuard] caught err:', err) | |
return false | |
} | |
} | |
} | |
@Injectable() | |
export class GqlJwtGuard implements CanActivate { | |
constructor(private readonly authService: AuthService) {} | |
canActivate(ctx: ExecutionContext): boolean { | |
try { | |
// This is GraphQL resolver signature | |
// (parent, args, context, info) => {} | |
const [parent, args, gqlReqCtx, info] = ctx.getArgs() | |
const gqlCtx = GqlExecutionContext.create(ctx) | |
const req = gqlCtx.getContext().request | |
const token = this.authService.getTokenFromRequestAuthHeader(req) | |
const user = this.authService.verifyJwt(token) | |
// Set "user" property in GraphQL request context object | |
req.user = user | |
return true | |
} catch (err) { | |
console.log('[GqlJwtGuard] caught err:', err) | |
return false | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { applyDecorators, SetMetadata, UseGuards } from '@nestjs/common' | |
import { HttpJwtGuard, GqlJwtGuard } from './jwt.guard' | |
import { GqlRolesGuard, HttpRolesGuard } from './roles.guard' | |
export function HttpJwtRoleRequired(...roles: string[]) { | |
return applyDecorators( | |
SetMetadata('roles', roles), | |
UseGuards(HttpJwtGuard, HttpRolesGuard), | |
) | |
} | |
export function GraphQLJwtRoleRequired(...roles: string[]) { | |
return applyDecorators( | |
SetMetadata('roles', roles), | |
UseGuards(GqlJwtGuard, GqlRolesGuard), | |
) | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common' | |
import { Reflector } from '@nestjs/core' | |
import { AuthService } from './auth.service' | |
@Injectable() | |
export class HttpRolesGuard implements CanActivate { | |
constructor(private reflector: Reflector, private authService: AuthService) {} | |
canActivate(context: ExecutionContext): boolean { | |
const roles = this.reflector.get<string[]>('roles', context.getHandler()) | |
if (!roles) return true | |
const request = context.switchToHttp().getRequest() | |
return this.authService.checkUserRoles(request.user, ...roles) | |
} | |
} | |
@Injectable() | |
export class GqlRolesGuard implements CanActivate { | |
constructor(private reflector: Reflector, private authService: AuthService) {} | |
canActivate(context: ExecutionContext): boolean { | |
const roles = this.reflector.get<string[]>('roles', context.getHandler()) | |
if (!roles) return true | |
const [parent, args, ctx, info] = context.getArgs() | |
return this.authService.checkUserRoles(ctx.request.user, ...roles) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment