I'm trying here to implement a token-based authentication using the passport-headerapikey library.
This is what I've tried so far, and for some reason I have a 500 server error popping from somewhere I couldn't find.
This is the structure of my authentication system (I also have a JWT-token based strategy in parallel on my graphQL queries).
app.module
#Module({
imports: [
AuthModule,
],
controllers: [AppController],
providers: [
AppService
],
})
export class AppModule {
configure(consumer: MiddlewareConsumer) {
consumer.apply(AuthMiddleware).forRoutes('/datasource/:id');
}
}
auth.module
#Module({
imports: [
PassportModule,
],
providers: [
AuthService,
DatasourceTokenStrategy,
],
controllers: [],
exports: [AuthService],
})
export class AuthModule {}
datasourceToken.strategy
#Injectable()
export class DatasourceTokenStrategy extends PassportStrategy(
HeaderAPIKeyStrategy,
'datasourceToken',
) {
constructor(private authService: AuthService) {
super(
{ header: 'datasourceToken', prefix: '' },
true,
(apikey, done, req) => {
const checkKey = authService.validateDatasourceToken(apikey);
if (!checkKey) {
return done(false);
}
return done(true);
},
);
}
}
authMiddleware.strategy
import {
Injectable,
NestMiddleware,
UnauthorizedException,
} from '#nestjs/common';
import * as passport from 'passport';
#Injectable()
export class AuthMiddleware implements NestMiddleware {
use(req: any, res: any, next: () => void) {
passport.authenticate(
'datasourceToken',
{ session: false, failureRedirect: '/api/unauthorized' },
(value) => {
if (value) {
next();
} else {
throw new UnauthorizedException();
}
},
)(req, res, next);
}
}
This is the error thrown when testing the endpoint with Jest:
When running my debug mode, I can see that the datasourceToken strategy is ok (I can retrieve the datasourceToken properly and validate it), but I think the problem is happening after my auth middleware..
Thanks guys for your insights
The function "done()" takes 3 arguments.
done(error, user, info)
You need to pass null as the first argument to let passport know that there was no error while authenticating.
done(null, true)
Related
In brief, I'm attempting to develop a GraphQL API that is guarded by an access token generated upon successful login. I'm copying and pasting the token into Postman and sending it with requests to a guarded resolver endpoint as an authorization header. I've verified that the token is indeed reaching the server as expected. The problem is that I seem to be unable to actually verify the token. Note that interpolation of things like environment variables, etc., is working and the application is compiling successfully. It's just that I'm not sure how to validate the access token provided by Auth0. I could really use some help with this and would even be willing to offer a reward to anyone who could help me solve this matter. Thanks!
My setup looks like this:
app.module.ts
src/authorization
- auth.guard.ts
- authorization.module.ts
- jwt.strategy.ts
src/products
- products.resolver.ts
That's it. Pretty simple. As to the files' contents:
// src/app.module.ts
/* istanbul ignore file */
import { Module } from '#nestjs/common';
import { GraphQLModule } from '#nestjs/graphql';
import { ConfigModule } from '#nestjs/config';
import { ProductsModule, ProductsService, ProductsResolver } from './products';
import { PrismaService } from './prisma.service';
import { AuthorizationModule } from './authorization/authorization.module';
#Module({
imports: [
GraphQLModule.forRoot({
debug: false,
playground: true,
typePaths: ['./**/*.graphql'],
definitions: { path: [process.cwd(), 'src/graphql.ts'].join() },
}),
PrismaService,
ConfigModule.forRoot({
isGlobal: false,
envFilePath: ['.env.development.local', '.env.test'],
}),
AuthorizationModule,
],
providers: [ProductsModule, ProductsResolver, ProductsService, PrismaService],
})
export class AppModule {}
// src/authorization/auth.guard.ts
import { ExecutionContext, UnauthorizedException } from '#nestjs/common';
import { GqlExecutionContext } from '#nestjs/graphql';
import { AuthGuard } from '#nestjs/passport';
// I'm not sure what to do here!
export class Auth0Guard extends AuthGuard('jwt') {
canActivate(context: ExecutionContext) {
try {
const graphqlContext = GqlExecutionContext.create(context);
console.log(graphqlContext.getContext().req);
const [_, token] = graphqlContext
.getContext()
.req.headers.authorization.split(' ');
if (token) return true;
} catch (error) {
throw new UnauthorizedException();
}
}
}
// src/authorization/authorization.module.ts
import { Module } from '#nestjs/common';
import { PassportModule } from '#nestjs/passport';
import { ConfigModule } from '#nestjs/config';
import { Auth0Guard } from './auth.guard';
import { JwtStrategy } from './jwt.strategy';
#Module({
imports: [
PassportModule.register({
defaultStrategy: 'jwt'
}),
ConfigModule.forRoot({
envFilePath: ['.env.development.local', '.env.test'],
}),
],
providers: [JwtStrategy, Auth0Guard],
exports: [PassportModule, Auth0Guard],
})
export class AuthorizationModule {}
// src/authorization/jwt.strategy.ts
import { Injectable } from '#nestjs/common';
import { PassportStrategy } from '#nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { ConfigService } from '#nestjs/config';
import { passportJwtSecret } from 'jwks-rsa';
#Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(configService: ConfigService) {
super({
secretOrKeyProvider: passportJwtSecret({
cache: false,
rateLimit: true,
jwksRequestsPerMinute: 100,
jwksUri: configService.get('AUTH0_JWKS'),
}),
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
audience: configService.get('AUTH0_AUDIENCE'),
issuer: configService.get('AUTH0_ISSUER'),
passReqToCallback: false,
algorithms: ['RS256'],
});
}
// This is never envoked!
async validate(req: any, payload: any) {
console.log('Inside validate method', req, payload);
return payload;
}
}
// src/products/products.resolver.ts
import {
Query,
Mutation,
Resolver,
Args,
ArgsType,
Field,
} from '#nestjs/graphql';
import { UseGuards } from '#nestjs/common';
import { Auth0Guard } from '../authorization/auth.guard';
import { ProductsService } from './products.service';
#ArgsType()
#Resolver('Product')
export class ProductsResolver {
constructor(private productsService: ProductsService) {}
#Query('products')
// I use the guard I've created instead of the AuthGuard('jwt') because mine
// extends AuthGuard('jwt'); the problem is: how do I now *verify/validate*
// the JWT itself? That's been the missing piece of the puzzle for some time.
#UseGuards(Auth0Guard)
async getProducts() {
return await this.productsService.getAllProducts();
}
}
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
I've followed the official Nest doc (https://docs.nestjs.com/security/authentication) step by step, but I can't get validate() method called when using #AuthGuard('local') or #AuthGuard(LocalAuthGuard) on login action.
If I don't use that guard decorator, all works as expected (but I need to use it to get my token added to request object).
auth.controller.ts
#UseGuards(AuthGuard('local')) // or AuthGuard(LocalAuthGuard)
#Post('login')
async login(
#Request() req
) {
const { access_token } = await this.authService.login(req.user);
return access_token;
}
}
local.strategy.ts
#Injectable()
export class LocalStrategy extends PassportStrategy(Strategy) {
constructor(private authService: AuthService) {
super({ usernameField: 'email' });
}
async validate(email: string, password: string): Promise<any> { // class is constructed but this method is never called
const user: UserDto = await this.authService.login({
email,
password,
});
if (!user) {
throw new UnauthorizedException();
}
return user;
}
}
auth.module.ts
#Module({
imports: [
UsersModule,
PassportModule,
JwtModule.register({
secret: "bidon",
signOptions: {
expiresIn: '3600',
},
}),
],
providers: [AuthService, LocalStrategy, JwtStrategy],
exports: [AuthService, PassportModule, JwtModule],
controllers: [AuthController],
})
export class AuthModule {}
PS : I've already read all stack overflow related posts (for exemple : NestJS' Passport Local Strategy "validate" method never called) but they didn't help me.
I found that if we don't pass email or password, also the wrong value of both, the guard will response Unauthorized message. The problem is how to ensure that validation of the required field before run guard logic if it not defined, In other words, frontend not pass it to server. If we add #Body() data: loginDto in controller method, it won't validate the body params.
to solve it, I add some validation code in local.guard.ts file. Here is my code in my project:
import { HttpException, HttpStatus, Injectable, UnauthorizedException } from "#nestjs/common";
import { AuthGuard } from "#nestjs/passport";
#Injectable()
export class LocalAuthGuard extends AuthGuard('local') {
handleRequest(err, user, info, context, status) {
const request = context.switchToHttp().getRequest();
const { mobile, password } = request.body;
if (err || !user) {
if (!mobile) {
throw new HttpException({ message: '手机号不能为空' }, HttpStatus.OK);
} else if (!password) {
throw new HttpException({ message: '密码不能为空' }, HttpStatus.OK);
} else {
throw err || new UnauthorizedException();
}
}
return user;
}
}
ValidationPipe doesn't validate your request. Because, Gurads are executed before any interceptor or pipe. But, guards are executed after middleware. So, we can create a validation middleware to solve this issue. Here is my solution. Hope it will help somebody.
login.dto.ts
import { ApiProperty } from '#nestjs/swagger';
import { IsEmail, IsNotEmpty } from 'class-validator';
export class LoginDto {
#ApiProperty({ required: true })
#IsNotEmpty()
#IsEmail()
username: string;
#ApiProperty({ required: true })
#IsNotEmpty()
password: string;
}
authValidationMiddleware.ts
import {
Injectable,
NestMiddleware,
BadRequestException,
} from '#nestjs/common';
import { Response, NextFunction } from 'express';
import { validateOrReject } from 'class-validator';
import { LoginDto } from '../dto/login.dto';
#Injectable()
export class AuthValidationMiddleware implements NestMiddleware {
async use(req: Request, res: Response, next: NextFunction) {
const body = req.body;
const login = new LoginDto();
const errors = [];
Object.keys(body).forEach((key) => {
login[key] = body[key];
});
try {
await validateOrReject(login);
} catch (errs) {
errs.forEach((err) => {
Object.values(err.constraints).forEach((constraint) =>
errors.push(constraint),
);
});
}
if (errors.length) {
throw new BadRequestException(errors);
}
next();
}
}
auth.module.ts
import { MiddlewareConsumer, RequestMethod } from '#nestjs/common';
import { AuthController } from './auth.controller';
import { AuthValidationMiddleware } from './middleware/authValidationMiddleware';
#Module({
imports: ['.. imports'],
controllers: [AuthController],
})
export class AuthModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(AuthValidationMiddleware)
.forRoutes({ path: 'auth/login', method: RequestMethod.POST });
}
}
This local strategy expects your body to have username and password fields, on your code change email to username
When you use NestJs Guard then it executed before Pipe therefore ValidationPipe() doesn't validate your request.
https://docs.nestjs.com/guards
Guards are executed after all middleware, but before any interceptor or pipe.
My use case requires only one parameter.
import { Injectable, UnauthorizedException, BadRequestException } from '#nestjs/common'
import { PassportStrategy } from '#nestjs/passport'
import { Request } from 'express'
import { Strategy } from 'passport-custom'
import { AuthService } from '../auth.service'
#Injectable()
export class CustomStrategy extends PassportStrategy(Strategy) {
constructor(private readonly authService: AuthService) {
super()
}
async validate(req: Request): Promise<any> {
// req.body.xxx can verify any parameter
if (!req.body.code) {
throw new BadRequestException('Missing code parameters!')
// Using the above, this is how the response would look:
// {
// "message": "Missing code parameters!",
// "error": "Bad Request",
// "statusCode": 400,
// }
}
const user = await this.authService.validateUser(req.body.code)
console.log('user', user)
if (!user) {
throw new UnauthorizedException()
}
return user
}
}
I have a controller that uses custom interceptor:
Controller:
#UseInterceptors(SignInterceptor)
#Get('users')
async findOne(#Query() getUserDto: GetUser) {
return await this.userService.findByUsername(getUserDto.username)
}
I have also I SignService, which is wrapper around NestJwt:
SignService module:
#Module({
imports: [
JwtModule.registerAsync({
imports: [ConfigModule],
useFactory: async (configService: ConfigService) => ({
privateKey: configService.get('PRIVATE_KEY'),
publicKey: configService.get('PUBLIC_KEY'),
signOptions: {
expiresIn: configService.get('JWT_EXP_TIME_IN_SECONDS'),
algorithm: 'RS256',
},
}),
inject: [ConfigService],
}),
],
providers: [SignService],
exports: [SignService],
})
export class SignModule {}
And Finally SignInterceptor:
#Injectable()
export class SignInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle().pipe(map(data => this.sign(data)))
}
sign(data) {
const signed = {
...data,
_signed: 'signedContent',
}
return signed
}
}
SignService works properly and I use it. I would like to use this as an interceptor
How can I inject SignService in to SignInterceptor, so I can use the functions it provides?
I assume that SignInterceptor is part of the ApiModule:
#Module({
imports: [SignModule], // Import the SignModule into the ApiModule.
controllers: [UsersController],
providers: [SignInterceptor],
})
export class ApiModule {}
Then inject the SignService into the SignInterceptor:
#Injectable()
export class SignInterceptor implements NestInterceptor {
constructor(private signService: SignService) {}
//...
}
Because you use #UseInterceptors(SignInterceptor) to use the interceptor in your controller Nestjs will instantiate the SignInterceptor for you and handle the injection of dependencies.
I have created an auth middlewere for checking each request, the middlewere is using server (only if data was not found in the req.connection).
I'm trying to inject the service into my middlewere and I keep getting the same error "Nest can't resolve dependencies of the AuthenticationMiddleware (?). Please verify whether [0] argument is available in the current context."
AuthenticationModule:
#Module({
imports: [ServerModule],
controllers: [AuthenticationMiddleware],
})
export class AuthenticationModule {
}
AuthenticationMiddleware:
#Injectable()
export class AuthenticationMiddleware implements NestMiddleware {
constructor(private readonly service : UserService) {}
resolve(): (req, res, next) => void {
return (req, res, next) => {
if (req.connection.user)
next();
this.service.getUsersPermissions()
}
}
ServerModule:
#Module({
components: [ServerService],
controllers: [ServerController],
exports: [ServerService]
})
export class ServerModule {}
ApplicationModule:
#Module({
imports: [
CompanyModule,
ServerModule,
AuthenticationModule
]
})
export class ApplicationModule implements NestModule{
configure(consumer: MiddlewaresConsumer): void {
consumer.apply(AuthenticationMiddleware).forRoutes(
{ path: '/**', method: RequestMethod.ALL }
);
}
}
Your application can't resolve AuthMiddleware dependencies probably because you inject an UserService into it but the ServerModule that you import into your AuthenticationModulejust exports a ServerService. So, what should be made is:
#Injectable()
export class AuthenticationMiddleware implements NestMiddleware {
constructor(private readonly service : ServerService) {}
resolve(): (req, res, next) => void {
return (req, res, next) => {
if (req.connection.user)
next();
this.service.getUsersPermissions()
}
}
You can find more about the NestJS dependency container here.