Nestjs with PassportModule custom AuthGuard inject ConfigService? - nestjs

i want know how to inject ConfigService in PassportModule AuthGuard with custom params?
here is my code
auth.module.ts
#Module({
imports: [
PassportModule.registerAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: async (configService: ConfigService): Promise<IAuthModuleOptions> => {
return {}
}
}),
],
// ...
})
export class AuthModule {}
github-auth.guard.ts
#Controller('auth')
export class AuthController {
// ...
#Get('github-oauth')
#UseGuards(new GithubAuthGuard({}))
async githubOAuth(#Req() req: Request, #Res() res: Response) {
const user = req.user
return await this.handleOauth(user, req, res)
}
}
github-auth.guard.ts
import { ConfigService } from '#nestjs/config'
import { AuthGuard } from '#nestjs/passport'
#Injectable()
export class GithubAuthGuard extends AuthGuard('github') {
constructor(private readonly configService: ConfigService) {
super()
}
//...
}
i call the UseGuards with new GithubAuthGuard({}) because i want pass custom params.
Update:
AuthGuard('github') return a wraped class can accept options the then pass down to my custom strategy's authenticate function as the second argument.
here is my github.strategy.ts
import { Strategy } from 'passport'
class StrategyFoo extends Strategy {
constructor(options, verify) {
//...
}
// options from AuthGuard('github')
authenticate(req, options) {
const self = this
const redirect_uri = options.callbackURL || this._options.callbackURL
// ...
}
}
#Injectable()
export class GithubBarStrategy extends PassportStrategy(StrategyFoo, 'github') {
//...
}
export const GithubStrategy = GithubBarStrategy

after some research i figure it out
Nest will not inject anything on a manually instantiated class
so just call #UseGuards(GithubAuthGuard) and then inject ConfigService in github-auth.guard.ts or github.strategy.ts like:
inject in github-auth.guard.ts
#Injectable()
export class GithubAuthGuard extends AuthGuard('github') {
// #Inject(ConfigService)
// private configService: ConfigService
constructor(private readonly configService: ConfigService) {
//
const options = {
callbackURL: configService.get('OAuth.github.callbackURL'),
//...
}
super(options)
}
getAuthenticateOptions(context) {
// this also works
return {
callbackURL: this.configService.get('OAuth.github.callbackURL'),
//...
}
}
// ...
}
or
inject in github.strategy.ts
class StrategyFoo extends Strategy {
private _options: any
constructor(options, verify) {
//...
this._options = options
// ...
}
authenticate(req, options) {
// ...
let _options = {
...options,
...this._options
}
// ...
}
}
#Injectable()
export class GithubBarStrategy extends PassportStrategy(StrategyFoo, 'github') {
constructor(private readonly configService: ConfigService) {
super({
passReqToCallback: true,
callbackURL: this.configService.get('OAuth.github.callbackURL'),
// ...
})
}
// ...
}

Related

Inject Service to Guard - Nestjs

I have the following global guard:
authorization.guard.ts
import { ExecutionContext, Injectable } from "#nestjs/common"
import { Reflector } from "#nestjs/core"
import { AuthGuard } from "#nestjs/passport"
#Injectable()
export class AuthorizationGuard extends AuthGuard(["azure-ad"]) {
public constructor(private readonly reflector: Reflector) {
super()
}
async canActivate(context: ExecutionContext) {
const isPublic = this.reflector.get<boolean>(
"isPublic",
context.getHandler(),
)
if (isPublic) {
return true
}
const req = context.switchToHttp().getRequest()
if(req.headers.isbypass){
//help needed
}
const result = (await super.canActivate(context)) as boolean
await super.logIn(req)
return result
}
}
and the following auth module and strategy:
import { Module } from "#nestjs/common";
import { PassportModule } from "#nestjs/passport";
import { UsersModule } from "modules/users/users.module";
import { AzureADStrategy } from "./azureAD.strategy";
import { SessionSerializer } from "./session.serializer";
#Module({
imports: [PassportModule, UsersModule],
providers: [AzureADStrategy, SessionSerializer],
})
export class AuthModule {}
import {
BearerStrategy,
IBearerStrategyOption,
ITokenPayload,
VerifyCallback,
} from "passport-azure-ad";
import {
Inject,
Injectable,
OnModuleInit,
UnauthorizedException,
} from "#nestjs/common";
import passport = require("passport");
import { UsersService } from "modules/users/users.service";
import env from "../../config";
const tenantId = env.TENANT_ID;
const clientID = env.CLIENT_ID || "";
const azureCredentials: IBearerStrategyOption = {
//
};
#Injectable()
export class AzureADStrategy extends BearerStrategy implements OnModuleInit {
onModuleInit() {
passport.use("azure-ad", this);
}
constructor(
#Inject(UsersService) private usersService: UsersService
) {
super(
azureCredentials,
async (token: ITokenPayload, done: VerifyCallback) => {
if (Date.now() / 1000 > token.exp) {
return done(new UnauthorizedException("access token is expired"));
}
const tokenUsername = token?.preferred_username?.slice(0, 9);
const tokenAppId = !tokenUsername && token?.azp;
if (!tokenUsername && !tokenAppId) {
return done(new UnauthorizedException("Missing User"));
}
let user;
if (tokenUsername) {
try {
user = await this.usersService.getUser(
tokenUsername
);
if (!user) {
return done(
new UnauthorizedException("User is not a test user")
);
}
} catch (err) {
return done(err);
}
user.tz = tokenUsername;
} else {
user.appId = tokenAppId;
}
return done(null, user, token);
}
);
}
}
The guard is defined globally using:
const reflector = app.get(Reflector);
app.useGlobalGuards(
new AuthorizationGuard(reflector),
);
And the auth module is imported in app.module.ts:
#Module({
imports: [
AuthModule,
...
]
Now, for the question.
I would like to have a way to "backdoor" the global authorization by checking if req.headers.isbypass exists in the request's headers, and if it does use userService in authorizationGuard, so i can inject the user from the DB to req.user myself and continue the request.
How do I achieve that?
I would change the app.useGlobalGuards() to be a global guard provider, adding
{
provider: APP_GUARD,
useClass: AuthorizationGuard,
}
Into the providers of your AppModule so that Nest handles all of the DI for you. From there, it's just adding the UsersService to the constructor like you already have for the Reflector
#Injectable()
export class AuthorizationGuard extends AuthGuard(["azure-ad"]) {
public constructor(
private readonly reflector: Reflector,
private readonly usersService: UsersServce
) {
super()
}
async canActivate(context: ExecutionContext) {
const isPublic = this.reflector.get<boolean>(
"isPublic",
context.getHandler(),
)
if (isPublic) {
return true
}
const req = context.switchToHttp().getRequest()
if(req.headers.isbypass){
//help needed
}
const result = (await super.canActivate(context)) as boolean
await super.logIn(req)
return result
}
}

nestjs jwt implementation is stuck - Nest can't resolve dependencies of the JWT_MODULE_OPTIONS (?)

I'm trying to implement JWT with NestJs. In my user.module.ts, I have added following configuration:
import { Module } from '#nestjs/common';
import { ConfigService } from '#nestjs/config';
import { UserService } from './user.service';
import { UserResolver } from './user.resolver';
import { User } from './entities/user.entity';
import { TypeOrmModule } from '#nestjs/typeorm';
import { AuthHelper } from './auth/auth.helper';
import { JwtStrategy } from './auth/auth.strategy';
import { PassportModule } from '#nestjs/passport';
import { JwtModule } from '#nestjs/jwt';
#Module({
imports: [
// PassportModule.register({ defaultStrategy: 'jwt', property: 'user' }),
// JwtModule.registerAsync({
// inject: [ConfigService],
// useFactory: (config: ConfigService) => ({
// secret: 'secret',
// signOptions: { expiresIn: 36000000 },
// }),
// }),
TypeOrmModule.forFeature([User]),
],
providers: [UserResolver, UserService], // AuthHelper, JwtStrategy],
})
export class UserModule {}
Whenever I uncomment these lines, I get some issues.
Here are some relevant files:
auth.strategy.ts
import { Injectable, Inject } from '#nestjs/common';
import { ConfigService } from '#nestjs/config';
import { PassportStrategy } from '#nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { User } from '../entities/user.entity';
import { AuthHelper } from './auth.helper';
#Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
#Inject(AuthHelper)
private readonly helper: AuthHelper;
constructor(#Inject(ConfigService) config: ConfigService) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: 'KhubSecret',
ignoreExpiration: true,
});
}
private validate(payload: string): Promise<User | never> {
return this.helper.validateUser(payload);
}
}
auth.guard.ts
import { Injectable, ExecutionContext } from '#nestjs/common';
import { AuthGuard, IAuthGuard } from '#nestjs/passport';
import { User } from '../entities/user.entity';
#Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') implements IAuthGuard {
public handleRequest(err: unknown, user: User): any {
return user;
}
public async canActivate(context: ExecutionContext): Promise<boolean> {
await super.canActivate(context);
const { user } = context.switchToHttp().getRequest();
return user ? true : false;
}
}
auth.helper.ts:
import {
Injectable,
HttpException,
HttpStatus,
UnauthorizedException,
} from '#nestjs/common';
import { JwtService } from '#nestjs/jwt';
import { InjectRepository } from '#nestjs/typeorm';
import { Repository } from 'typeorm';
import * as bcrypt from 'bcryptjs';
import { User } from '../entities/user.entity';
#Injectable()
export class AuthHelper {
#InjectRepository(User)
private readonly repository: Repository<User>;
private readonly jwt: JwtService;
constructor(jwt: JwtService) {
this.jwt = jwt;
}
public async decode(token: string): Promise<unknown> {
return this.jwt.decode(token, null);
}
public async validateUser(decoded: any): Promise<User> {
return this.repository.findOne(decoded.id);
}
public generateToken(user: User): string {
return this.jwt.sign({
id: user.userId,
username: user.username,
});
}
public isPasswordValid(password: string, userPassword: string): boolean {
return bcrypt.compareSync(password, userPassword);
}
public encodePassword(password: string): string {
const salt: string = bcrypt.genSaltSync(10);
return bcrypt.hashSync(password, salt);
}
private async validate(token: string): Promise<boolean | never> {
const decoded: unknown = this.jwt.verify(token);
if (!decoded) {
throw new HttpException('Forbidden', HttpStatus.FORBIDDEN);
}
const user: User = await this.validateUser(decoded);
if (!user) {
throw new UnauthorizedException();
}
return true;
}
}
I get some error like this:
[Nest] 18360 - 05/10/2022, 18:14:42 ERROR [ExceptionHandler] Nest can't resolve dependencies of the JWT_MODULE_OPTIONS (?). Please make sure that the argument ConfigService at index [0] is available in the JwtModule context.
Potential solutions:
- If ConfigService is a provider, is it part of the current JwtModule?
- If ConfigService is exported from a separate #Module, is that module imported within JwtModule?
#Module({
imports: [ /* the Module containing ConfigService */ ]
})
Error: Nest can't resolve dependencies of the JWT_MODULE_OPTIONS (?). Please make sure that the argument ConfigService at index [0] is available in the JwtModule context.
Potential solutions:
- If ConfigService is a provider, is it part of the current JwtModule?
- If ConfigService is exported from a separate #Module, is that module imported within JwtModule?
#Module({
imports: [ /* the Module containing ConfigService */ ]
})
I have already tested and implemented these solutions:
NestJS can't resolve dependencies of the JWT_MODULE_OPTIONS
Here is the solution. I hope it would help.
In your auth strategy add this:
constructor() {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: 'KhubSecret',
ignoreExpiration: true,
});
}
instead of:
constructor(#Inject(ConfigService) config: ConfigService) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: 'KhubSecret',
ignoreExpiration: true,
});
}
In Short, Remove
#Inject(ConfigService) config: ConfigService
from your constructor
From the immediate error you've provided, I'll assume that your ConfigModule is not global. This means that you'll need to import the ConfigModule in the JwtModule.registerAsync() call (in the imports array). Another option would be to make your ConfigModule global (#nestjs/config has an option for this) so that you already have access to ConfigService.

How to check for roles in NestJS Guard

I have an external service providing a JWT token. In Nestjs i first have JwtGuard class:
#Injectable()
export class JwtGuard extends AuthGuard('JWT_STRATEGY') {
constructor() {
super();
}
getRequest(context: ExecutionContext) {
console.log('JwtGuard');
const ctx = GqlExecutionContext.create(context);
return ctx.getContext().req;
}
}
and then a passport strategy:
#Injectable()
export class JwtStrategy extends PassportStrategy(Strategy, 'JWT_STRATEGY') {
constructor(private configService: ConfigService) {
super({
secretOrKeyProvider: passportJwtSecret({
cache: true,
rateLimit: true,
jwksRequestsPerMinute: 5,
jwksUri: configService.get<string>('ADFS_KEYS_URL'),
}),
ignoreExpiration: false,
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
audience: configService.get<string>('ADFS_AUDIENCE'),
issuer: configService.get<string>('ADFS_ISSUER'),
algorithms: ['RS256'],
});
}
validate(payload: unknown): unknown {
console.log('jwt strategy');
console.log(payload);
return payload;
}
}
It seems that JwtGuard is running first, then the strategy. But if i want to do additional guards and checks, say for roles. Where does one do that? Do i need another guard that runs after the passport strategy? I have two roles "User" and "Admin".
First of all, define a global guard (called RolesGuard) in the AppModule as following:
providers: [
AppService,
{
provide: APP_GUARD,
useClass: JwtAuthGuard,
},
{
provide: APP_GUARD,
useClass: RolesGuard,
},
]
Then within RolesGuard we have the following:
export enum FamilyRole {
Admin = 'Admin',
User = 'User',
}
...
export class FamilyRolesGuard implements CanActivate {
async canActivate(context: ExecutionContext): Promise<boolean> {
const requiredRoles = this.reflector.getAllAndOverride<FamilyRole>(
ROLES_KEY,
[context.getHandler(), context.getClass()],
);
if (!requiredRoles) {
return true;
}
const { user } = context.switchToHttp().getRequest();
// do the rest and return either true or false
}
}
Then create your own decorator and you can decorate your APIs if you need that API to protect your app based on your guard.
import { SetMetadata } from '#nestjs/common';
export const ROLES_KEY = 'FamilyRoles';
export const FamilyRoles = (...roles: FamilyRole[]) =>
SetMetadata(ROLES_KEY, roles);
Then you can use your decorator in your API like this:
#Post('user')
#FamilyRoles(FamilyRole.Admin)
...
So, in your API, if you won't have FamilyRoles, in the guard you won't have requiredRoles, and the if block will return true.
More info: https://docs.nestjs.com/security/authorization

Nest JS - Return undefined when trying to get user context in RolesGuard

I am trying to add role guard in Nest JS API. I used Passport, Jwt authentication for this.
In my RolesGuard class I made the request and get the user from it to check the user role valid or not. I attached the code below.
roles.guard.ts
canActivate(
context: ExecutionContext,
): boolean | Promise<boolean> | Observable<boolean> {
const roles = this.reflector.get<string[]>('roles', context.getHandler());
if (!roles) {
return true;
}
const request = context.switchToHttp().getRequest();
const user: User = request.user;
return this.userService.findOne(user.id).pipe(
map((user: User) => {
const hasRole = () => roles.indexOf(user.role) > -1;
let hasPermission: boolean = false;
if (hasRole()) {
hasPermission = true;
}
return user && hasPermission;
}),
);
}
Problem here is context.switchToHttp().getRequest() returns object, which is undefined. So I could not get user details from it.
After I had some research about this error I found that order of the decorators in controller can be the issue. Then I changed the order, but still problem appears as same. Bellow I added that code also.
user.controller.ts
#UseGuards(JwtAuthGuard, RolesGuard)
#hasRoles(UserRole.USER)
#Get()
findAll(): Observable<User[]> {
return this.userService.findAll();
}
-Thank you-
if you are using graphql you can make the changes to fit the code below
import { Injectable, CanActivate, ExecutionContext } from '#nestjs/common';
import { Reflector } from '#nestjs/core';
import { GqlExecutionContext } from '#nestjs/graphql';
import { Role } from 'src/enums/role.enum';
import { ROLES_KEY } from '../auth';
#Injectable()
export class RolesGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const requiredRoles = this.reflector.getAllAndOverride<Role[]>(ROLES_KEY, [
context.getHandler(),
context.getClass(),
]);
if (!requiredRoles) {
return true;
}
const ctx = GqlExecutionContext.create(context);
const user = ctx.getContext().req.user;
return requiredRoles.some((role) => user.roles?.includes(role));
}
}
if you are not using graphql, make the changes to fit the code below
import { Injectable, CanActivate, ExecutionContext } from '#nestjs/common';
import { Reflector } from '#nestjs/core';
#Injectable()
export class RolesGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const requiredRoles = this.reflector.getAllAndOverride<Role[]>(ROLES_KEY, [
context.getHandler(),
context.getClass(),
]);
if (!requiredRoles) {
return true;
}
const { user } = context.switchToHttp().getRequest();
return requiredRoles.some((role) => user.roles?.includes(role));
}
}
then in you controller you can do
#UseGuards(JwtAuthGuard, RolesGuard)
#hasRoles(UserRole.USER)
#Get()
findAll(): Observable<User[]> {
return this.userService.findAll();
}
finally, for all this to work add the global guard to your app.module.ts
like so
import { Module } from '#nestjs/common';
import { AppController } from './app.controller';
import {MongooseModule} from '#nestjs/mongoose';
import { AppService } from './app.service';
import { ConfigModule } from '#nestjs/config';
import configuration from 'src/config/configuration';
import { RolesGuard } from 'src/auth/auth';
import { UsersModule } from '../users/users.module';
import { AdminsModule } from 'src/admin/admins.module';
import { config } from 'dotenv';
import * as Joi from 'joi';
config();
#Module({
imports: [
// other modules
UsersModule,
// configuration module
ConfigModule.forRoot({
isGlobal: true,
cache: true,
load: [configuration],
expandVariables: true,
// validate stuff with Joi
validationSchema: Joi.object({
NODE_ENV: Joi.string()
.valid('development', 'production', 'test', 'provision')
.default('development'),
PORT: Joi.number().default(5000),
}),
validationOptions: {
// allow unknown keys (change to false to fail on unknown keys)
allowUnknown: true,
abortEarly: true,
},
}),
// connect to mongodb database
MongooseModule.forRoot(process.env.DB_URL, {
useNewUrlParser: true,
useUnifiedTopology: true,
}),
],
controllers: [AppController],
providers: [
AppService,
{
provide: 'APP_GUARD',
useClass: RolesGuard,
}
],
})
export class AppModule {}

request undefined when extracting JWT

I have followed this guide in an attempt to get JWT authentication working.
The only difference I have is that I keep JWT Token in HttpOnly cookie which means a custom extractor is required.
I found an example of how to extract a Token from a cookie. So, the only difference is:
jwtFromRequest: ExtractJwt.fromExtractors([(req: Request) => {
return req?.cookies?.access_token
}])
Unfortunately, req is undefined for no apparent reason.
That's how my auth.module.ts looks like:
#Module({
imports: [
PassportModule,
JwtModule.register({
secret: 'qweqweqweqeqwe',
signOptions: { expiresIn: '20s' }
})
],
providers: [
AuthService,
AuthResolver,
JwtAuthGuard,
JwtStrategy
]
})
export class AuthModule { }
I have also created a strategy file jwt.stragegy.ts:
#Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor() {
super({
jwtFromRequest: ExtractJwt.fromExtractors([(req: Request) => {
return req?.cookies?.access_token
}]),
ignoreExpiration: false,
secretOrKey: 'qweqweqweqeqwe',
})
}
async validate(payload: any) {
return { userId: payload.sub, username: payload.username }
}
}
auth.guard.ts:
#Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {}
Could it be an error on passport.js library's side? Like, #nestjs/passport fails to map arguments or something...
By following the docs, you should be able to get the request.
import { ExecutionContext } from '#nestjs/common';
import { GqlExecutionContext } from '#nestjs/graphql';
#Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
getRequest(context: ExecutionContext) {
const ctx = GqlExecutionContext.create(context);
return ctx.getContext().req;
}
}
In my scenario what was making the payload.sub undefined was the login function on auth.service.
Before (not working):
async login(user: any) {
const payload = { apiKey: user.apiKey, sub: user.userId };
const token = this.jwtService.sign(payload);
return `Access token=${token};\n HttpOnly;\n Path=/;\n Max-
Age=${this.configService.get('jwtConstants.expirationTime')}`;
}
After changes (working):
async login(user: any) {
const payload = { apiKey: user._doc.apiKey, sub: user._doc._id };
const token = this.jwtService.sign(payload);
return `Access token=${token};\n HttpOnly;\n Path=/;\n Max-Ag
e=${this.configService.get('jwtConstants.expirationTime')}`;
}

Resources