Skip to content

Instantly share code, notes, and snippets.

@GavinRay97
Last active February 22, 2024 10:57
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save GavinRay97/d2c64ef82aef1149248aeafd077293b7 to your computer and use it in GitHub Desktop.
Save GavinRay97/d2c64ef82aef1149248aeafd077293b7 to your computer and use it in GitHub Desktop.
Hasura Nest.js JWT Auth utils (REST + GQL)
import { Module } from '@nestjs/common'
import { AuthService } from './auth.service'
@Module({
providers: [AuthService],
exports: [AuthService],
})
export class AuthModule {}
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)
}
}
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
},
)
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
}>
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
}
}
}
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),
)
}
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