Here is a similar question on the GitHub Nest repository
but I have multiple strategies.
export class AdminStrategy extends PassportStrategy(Strategy, 'admin')
I would like to not have to pass a token in dev.
Add an AdminAuthGuard guard that extends AuthGuard('admin')
export class AdminAuthGuard extends AuthGuard('admin') {
canActivate(context: ExecutionContext) {
// add your custom authentication logic here.
return super.canActivate(context);
}
handleRequest(err, user, info) {
if (err || !user) {
throw err || new UnauthorizedException();
}
return user;
}
}
Related
I have developed api-key strategy following https://www.stewright.me/2021/03/add-header-api-key-to-nestjs-rest-api/
and it works, I pass api-key in header and it authorize it.
Now for some cases I need to pass api-key as query params to url instead of header. I wasn't able to figure it out.
example mysite.com/api/book/5?api-key=myapikey
my current code is
api-key-strategy.ts
#Injectable()
export class ApiKeyStrategy extends PassportStrategy(Strategy, 'api-key') {
constructor(private configService: ConfigService) {
super({ header: 'api-key', prefix: '' }, true, async (apiKey, done) =>
this.validate(apiKey, done)
);
}
private validate(apiKey: string, done: (error: Error, data) => any) {
if (
this.configService.get(AuthEnvironmentVariables.API_KEY) === apiKey
) {
done(null, true);
}
done(new UnauthorizedException(), null);
}
}
api-key-auth-gurad.ts
import { Injectable } from '#nestjs/common';
import { AuthGuard } from '#nestjs/passport';
#Injectable()
export class ApiKeyAuthGuard extends AuthGuard('api-key') {}
app.controller
...
#UseGuards(ApiKeyAuthGuard)
#Get('/test-api-key')
testApiKey() {
return {
date: new Date().toISOString()
};
}
...
I found a solution in case someone else has same problem.
I added canActivate method to my guard, then read the api key from request.query, and add it to header. Then the rest of code is working as before and checking header
#Injectable()
export class ApiKeyAuthGuard extends AuthGuard('api-key') {
canActivate(context: ExecutionContext) {
const request: Request = context.switchToHttp().getRequest();
if (request && request.query['api-key'] && !request.header('api-key')) {
(request.headers['api-key'] as any) = request.query['api-key'];
}
return super.canActivate(context);
}
}
I am using nestjs, graphql, & prisma. I am trying to figure out how to pass my jwt token for each database request to the prisma service iv created. Iv tried an object to the constructor but then wont compile saying I am missing a dependency injection for whatever I reference in the constructor paramter.
#Injectable()
export class PrismaService
extends PrismaClient
implements OnModuleDestroy {
constructor() {
super();
//TODO how do I pass my jwt token to this for each request?
this.$use(async (params, next) => {
if (params.action === 'create') {
params.args.data['createdBy'] = 'jwt username goes here';
}
if (params.action === 'update') {
params.args.data['updatedBy'] = 'jwt username goes here';
}
const result = await next(params);
return result;
});
}
async onModuleDestroy() {
await this.$disconnect();
}
}
Are you using a nest middleware?
JWT is normally passed to a Controller, not a service.
Example:
#Injectable()
export class MyMiddleware implements NestMiddleware {
private backend: any // This is your backend
constructor() {
this.backend = null // initialize your backend
}
use(req: Request, res: Response, next: any) {
const token = <string>req.headers.authorization
if (token != null && token != '') {
this.backend
.auth()
.verifyIdToken(<string>token.replace('Bearer ', ''))
.then(async (decodedToken) => {
const user = {
email: decodedToken.email,
uid: decodedToken.uid,
tenantId: decodedToken.tenantId,
}
req['user'] = user
next()
})
.catch((error) => {
log.info('Token validation failed', error)
this.accessDenied(req.url, res)
})
} else {
log.info('No valid token provided', token)
return this.accessDenied(req.url, res)
}
}
private accessDenied(url: string, res: Response) {
res.status(403).json({
statusCode: 403,
timestamp: new Date().toISOString(),
path: url,
message: 'Access Denied',
})
}
}
So every time I get an API call with a valid token, the token is added to the user[] in the request.
In my Controller Class I can then go ahead and use the data:
#Post()
postHello(#Req() request: Request): string {
return 'Hello ' + request['user']?.tenantId + '!'
}
I just learned about an update in Nest.js which allows you to easily inject the header also in a Service. Maybe that is exactly what you need.
So in your service.ts:
import { Global, INestApplication, Inject, Injectable, OnModuleInit, Scope } from '#nestjs/common'
import { PrismaClient } from '#prisma/client'
import { REQUEST } from '#nestjs/core'
#Global()
#Injectable({ scope: Scope.REQUEST })
export class PrismaService extends PrismaClient implements OnModuleInit {
constructor(#Inject(REQUEST) private readonly request: any) {
super()
console.log('request:', request?.user)
}
async onModuleInit() {
// Multi Tenancy Middleware
this.$use(async (params, next) => {
// Check incoming query type
console.log('params:', params)
console.log('request:', this.request)
return next(params)
})
await this.$connect()
}
async enableShutdownHooks(app: INestApplication) {
this.$on('beforeExit', async () => {
await app.close()
})
}
}
As you can see in the log output, you have access to the entire request object.
I'm trying to implement a passport strategy (passport-headerapikey), I was able to make it work and I can secure my routes.
But the request is empty and cannot access the logged in user ?
import { HeaderAPIKeyStrategy } from "passport-headerapikey";
import { PassportStrategy } from "#nestjs/passport";
import { Injectable, NotFoundException } from "#nestjs/common";
import { CompanyService } from "../../companies/companies.service";
#Injectable()
export class ApiKeyStrategy extends PassportStrategy(HeaderAPIKeyStrategy, "api-key") {
constructor(private readonly companyService: CompanyService) {
super(
{
header: "Authorization",
prefix: "Api-Key "
},
true,
async (apiKey, done) => {
return this.validate(apiKey, done);
}
);
}
public async validate(apiKey: string, done: (error: Error, data) => {}) {
const company = await this.companyService.findByApiKey(apiKey);
if (company === null) {
throw new NotFoundException("Company not found");
}
return company;
}
}
#UseGuards(AuthGuard("api-key"))
export class CompaniesController {
constructor(private companyService: CompanyService) {}
#Get()
#ApiOperation({ title: "Get company information" })
public getCompany(#Request() req) {
// here request is empty, so i cannot access the user..
console.log("request", req);
return [];
}
}
Thanks for your help !
To access the logged user, you can inject the object in the request. To do that, in your ApiKeyStrategy constructor, change the third parameter to something like this:
async (apiKey, verified, req) => {
const user = await this.findUser(apiKey);
// inject the user in the request
req.user = user || null;
return verified(null, user || false);
}
Now, you can access the logged user:
getCompany(#Request() req) {
console.log(req.user);
}
I hope that could help you.
As show in the documentation you should do some works to get the current user : here the documetation
First of all in the app.module make sure that the context is set :
context: ({ req }) => ({ req })
Then you can add this in the controller/resolver, this example use the Gql (GraphQL):
export const CurrentUser = createParamDecorator(
(data: unknown, context: ExecutionContext) => {
const ctx = GqlExecutionContext.create(context);
return ctx.getContext().req.user;
},
);
if this one doesnt work for you try this one instead :
export const CurrentUser = createParamDecorator(
(data: unknown, context: ExecutionContext) => {
const ctx = GqlExecutionContext.create(context);
const request = ctx.getContext();
request.body = ctx.getArgs();
return request.user;
},
);
Modify your validate method like so:
public async validate(apiKey: string, done: (error: Error, data) => {}) {
const company = await this.companyService.findByApiKey(apiKey);
if (company === null) {
return done(new NotFoundException("Company not found"), null);
}
return done(null, company);
}
I am trying to set up a very simple login system using jwt-passport with nestjs. I followed this tutorial: https://docs.nestjs.com/techniques/authentication but I just can't get it to work. I am really new to this stuff and would appreciate if anyone can show me the way.
The way I send the login to the server:
this.clientAuthService.login(this.userName, this.password).then(response => {
this.clientAuthService.setToken(response.access_token);
this.router.navigate(['/backend']);
});
My ClientAuthService:
export class ClientAuthService {
constructor(private http: HttpClient, #Inject(PLATFORM_ID) private platformId) {
}
getToken(): string {
if (isPlatformBrowser(this.platformId)) {
return localStorage.getItem(TOKEN_NAME);
} else {
return '';
}
}
setToken(token: string): void {
if (isPlatformBrowser(this.platformId)) {
localStorage.setItem(TOKEN_NAME, token);
}
}
removeToken() {
if (isPlatformBrowser(this.platformId)) {
localStorage.removeItem(TOKEN_NAME);
}
}
getTokenExpirationDate(token: string): Date {
const decoded = jwt_decode(token);
if (decoded.exp === undefined) {
return null;
}
const date = new Date(0);
date.setUTCSeconds(decoded.exp);
return date;
}
isTokenExpired(token?: string): boolean {
if (!token) {
token = this.getToken();
}
if (!token) {
return true;
}
const date = this.getTokenExpirationDate(token);
if (date === undefined) {
return false;
}
return !(date.valueOf() > new Date().valueOf());
}
login(userName: string, password: string): Promise<any> {
const loginData = {username: userName, password};
return this.http
.post(Constants.hdaApiUrl + 'user/login', loginData, {headers: new HttpHeaders({'Content-Type': 'application/json'})})
.toPromise();
}
}
My user.controller.ts
#Controller('user')
export class UserController {
constructor(private readonly authService: AuthService) {
}
#UseGuards(AuthGuard('local'))
#Post('login')
authenticate(#Request() req) {
return this.authService.login(req);
}
}
My user.service.ts
export class UsersService {
private readonly users: User[];
constructor() {
this.users = [
{
userId: 1,
username: 'test',
password: '12345',
}
];
}
async findOne(username: string): Promise<User | undefined> {
return this.users.find(user => user.username === username);
}
}
Then I have the jwt.strategy.ts
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor() {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: Constants.jwtSecret,
});
}
async validate(payload: any) {
return { userId: payload.sub, username: payload.username };
}
}
and the local.strategy.ts
export class LocalStrategy extends PassportStrategy(Strategy) {
constructor(private readonly authService: AuthService) {
super();
}
async validate(username: string, password: string): Promise<any> {
const user = await this.authService.validateUser(username, password);
if (!user) {
throw new UnauthorizedException();
}
return user;
}
}
Mostly I just followed the tutorial and added some stuff for the client-side by myself.
I missed the part with the UseGuard('local') for the login route but after I added it I am getting 401 error always.
When I don't use UseGuard('local') it doesn't matter what I type in the login form. After I submit the details, I get access to the backend even tho it was not correct.
Also, it might be worth to mention that the validate methods in jwt.strategy.ts and local.strategy.ts are marked as not used in WebStorm.
I know its a lot of code here but I need help because I cannot find any other sources for NestJS auth configuration which is up to date. It feels like the tutorial I followed missed a lot of steps for beginners.
Make sure your post body (payload) is identical to the signature of the validate method (it actually has to be username & password).
In order to add a bit more context, in the above picture you can find detail where the implementation of the specified signature is mentioned. So it's mandatory to send exact "username" and "password" properties in the body.
Note: if you want to customize the signature of your local strategy service you simply have to pass a new object within the super() for specifying the new properties:
constructor() {
super({ usernameField: 'email' });
}
Ref: https://docs.nestjs.com/techniques/authentication
For me, in multiple projects,
Using
export class JwtStrategy extends PassportStrategy(Strategy, 'theJwt')
with #UseGuards(AuthGuard('theJwt'))
rather than using:
export class JwtStrategy extends PassportStrategy(Strategy)
with
#UseGuards(AuthGuard(JwtStrategy )), which did not work.
My purpose is make a role guard to verify the permission of user. I trying to extract authorization header to get the role information which is include on JWT. I implemented canActivate interface to check role of use but i don't know how to get the role info from JWT to verify it.
export class RolesGuard implements CanActivate {
constructor(private readonly _reflector: Reflector) {
}
canActivate(context: ExecutionContext): boolean {
const roles = this._reflector.get<UserRole[]>(
'roles',
context.getHandler(),
);
if (!roles || roles.length === 0) {
return true;
}
const request = context.switchToHttp().getRequest();
const user: InstanceType<User> = request.headers.role;
// i want to get the role from JWT in here
const hasRole = () => roles.indexOf(user.role) >= 0;
if (user && user.role && hasRole()) {
return true;
}
throw new HttpException(
'You do not have permission (Roles)',
HttpStatus.UNAUTHORIZED,
);
}
}
I tried extends PassportStrategy, but it can't work together with CanActive
One option is to use the JwtService from the JwtModule and use jwtService.decode(myJwt) to get the decoded JWT and get the role from there. The other is to use the built Passport Guard (AuthGuard), extend the functionality, and call super.canActivate(context) before your custom logic. Store he result and immediately check if the user has passport access before continuing with your custom logic.
// the mention of jwt in the AuthGuard is only needed if not working with defaultStrategy
export class RolesGuard extends AuthGuard('jwt') {
constructor(private readonly _reflector: Reflector) {
super()
}
canActivate(context: ExecutionContext): boolean {
const passportActive = super.canActivate(context);
if (!passportActivate) {
throw new HttpException(
'You do not have permission (Roles)',
HttpStatus.UNAUTHORIZED,
);
}
const roles = this._reflector.get<UserRole[]>(
'roles',
context.getHandler(),
);
if (!roles || roles.length === 0) {
return true;
}
const request = context.switchToHttp().getRequest();
// this should come from passport
const user: InstanceType<User> = request.user;
// i want to get the role from JWT in here
const hasRole = () => roles.indexOf(user.role) >= 0;
if (user && user.role && hasRole()) {
return true;
}
throw new HttpException(
'You do not have permission (Roles)',
HttpStatus.UNAUTHORIZED,
);
}
}