I have an existing authentication for users which is already working fine. The token for user authentication expires within an hour.
I want to implement another separate authentication strategy a third API that is consuming my Nestjs API. There are separate endpoints for the third-party API, the token should expire with 24 hours. The API has to stay connected to my app for 24 hours.
I don't mind using additional package to achieve this.
I also need to create a guard called thirdParty Guard so that the 3rd part API alone will have access to that endpoint.
This is my jwt.strategy.ts
#Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(private authService: AuthService) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: process.env.SECRETKEY
});
}
async validate(payload: any, done: VerifiedCallback) {
const user = await this.authService.validateUser(payload);
if (!user) {
return done(
new HttpException('Unauthorised access', HttpStatus.UNAUTHORIZED),
false,
);
}
//return user;
return done(null, user, payload.iat)
}
}
ApiKey.strategy.ts
#Injectable()
export class ApiKeyStrategy extends PassportStrategy(HeaderAPIKeyStrategy) {
constructor(private authService: AuthService) {
super({
header: 'api_key',
prefix: ''
}, true,
(apikey: string, done: any, req: any, next: () => void) => {
const checkKey = this.authService.validateApiKey(apikey);
if (!checkKey) {
return done(
new HttpException('Unauthorized access, verify the token is correct', HttpStatus.UNAUTHORIZED),
false,
);
}
return done(null, true, next);
});
}
}
and this is the auth.service.ts
#Injectable()
export class AuthService {
constructor(private userService: UserService) { }
async signPayLoad(payload: any) {
return sign(payload, process.env.SECRETKEY, { expiresIn: '1h' });
}
async validateUser(payload: any) {
const returnuser = await this.userService.findByPayLoad(payload);
return returnuser;
}
validateApiKey(apiKey: string) {
const keys = process.env.API_KEYS;
const apiKeys = keys.split(',');
return apiKeys.find(key => apiKey === key);
}
}
With the above setup, If you are using Passport-HeaderAPIKey then try adding headerapikey in the Guard. The below code worked for me.
Ref: NestJS extending guard
import { ExecutionContext, Injectable } from '#nestjs/common';
import { Reflector } from '#nestjs/core';
import { AuthGuard as NestAuthGuard } from '#nestjs/passport';
#Injectable()
export class AuthGuard extends NestAuthGuard(['jwt', 'headerapikey']) {
constructor(private readonly reflector: Reflector) {
super();
}
canActivate(context: ExecutionContext) {
const isPublic = this.reflector.getAllAndOverride<boolean>('isPublic', [
context.getHandler(),
context.getClass(),
]);
if (isPublic) {
return true;
}
return super.canActivate(context);
}
}
Related
I have a controller which has an authentication guard and a RBAC authorization guard
#Get('get-framework-lists')
#UseGuards(JwtAuthGuard) // authentication guard
#Roles(Role.SO) // RBAC authorization guard
getFrameworkListsByCompany() {
return this.dashboardService.getFrameworkListsByCompany();
}
JwtAuthGuard look like this -
#Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(#InjectModel(User.name) private userModel: Model<UserDocument>) {
super({
ignoreExpiration: false,
secretOrKey: 'SECRET',
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
});
}
async validate(payload: any) {
const user = await this.userModel.findById(payload.sub);
return {
_id: payload.sub,
name: payload.name,
...user,
};
}
}
I have created a custom Roles.guard.ts for #Roles decorator
#Injectable()
export class RolesGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const requiredRole = this.reflector.getAllAndOverride<Role>(ROLES_KEY, [
context.getHandler(),
context.getClass(),
]);
if (!requiredRole) {
return true;
}
console.log({ requiredRole });
const { user } = context.switchToHttp().getRequest();
return requiredRole === user.role;
}
}
In the controller, I can access req.user as user is added to the request object.
However, I am not getting the user as undefined in roles.guard.ts.
What am I doing wrong here?
I think that simple add RolesGuard inside the #UseGuards() decorator, so that both guards can run, will solve your problem.
Like this:
#Get('get-framework-lists')
#UseGuards(JwtAuthGuard, RolesGuard) // here is the change
#Roles(Role.SO)
getFrameworkListsByCompany() {
return this.dashboardService.getFrameworkListsByCompany();
}
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 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')}`;
}
With the help of Guards/Decorators I try to check a JWT first and then the roles a user has.
I have read the documentation regarding Authentication, Guards and Decorators and understand the principles behind them.
However, what I cannot do is to somehow make the authenticated user from JWT-Guard available to Roles-Guards.
In every example that I found, exactly this part that is interesting for me is skipped / left out...
Grateful for every tip!
This is my latest try:
jwt.strategy.ts
import { ExtractJwt, Strategy } from 'passport-jwt';
import { PassportStrategy } from '#nestjs/passport';
import { Injectable } from '#nestjs/common';
import { JwtPayload } from './jwt.model';
#Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor() {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
passReqToCallback: true,
ignoreExpiration: false,
secretOrKey: '0000',
expiresIn: '3 days'
});
}
async validate(payload: JwtPayload) {
return {
id: payload.id,
email: payload.email,
username: payload.username
};
}
}
roles.guard.ts
import { CanActivate, ExecutionContext, Injectable } from '#nestjs/common';
import { Reflector } from '#nestjs/core';
#Injectable()
export class RolesGuard implements CanActivate {
constructor(private readonly reflector: Reflector) {
}
canActivate(context: ExecutionContext): boolean {
const roles = this.reflector.get<string[]>('roles', context.getHandler());
if (!roles) {
return false;
}
const request = context.switchToHttp().getRequest();
const user = request.user ??? // THIS is what is missing
return roles.some((role) => {
return role === user.role;
});
}
}
roles.decorator.ts
import { SetMetadata } from '#nestjs/common';
export const Roles = (...roles: string[]) => SetMetadata('roles', roles);
users.controller.ts
#UseGuards(AuthGuard('jwt'))
#Roles('admin', 'member')
#Get('/')
async doSomething(#Req() req): Promise<User> {
return await this.usersService.doSomething(req.user.id);
}
Your decorator and guards look fine, but from the snippet of your users.controller.ts file it is not clear whether the roles guard is actually applied for the GET / route.
I do, however, have an NestJS app with a quite similar setup based on the guards documentation. The following code in users.controller.ts works as intended:
#UseGuards(JwtAuthGuard, RolesGuard)
#Controller('/users')
export class UserController {
constructor(private readonly userService: UserService) {}
#Get()
#Roles(UserRole.ADMIN)
public async index(): Promise<User[]> {
return this.userService.findAll();
}
// ...
}
Note how both the auth and roles guard are activated in the same scope and that JwtAuthGuard is added before RolesGuard. If I were to change the sequence of the guards then the RolesGuard would not be able to retrieve the user of the request.
Also, you might want to have a look at a similar question from some time ago which contains some details on the order of guards in different scopes.
I have a token based authorization. Authorization happens well there are no errors. But now need to somehow display the data of an authorized user. Please tell me how to properly implement the profile on the client (Angular 6)?
I have this server:
controller:
const db = require('../config/db.config.js')
const User = db.user
const errorHandler = require('../utils/errorHandler')
module.exports.getProfile = async function(req, res) {
try {
await User.findOne({ id: req.user.id}, (user) => {
res.json({
success: true,
user: user,
message: "Successful"
})
})
}catch(e) {
errorHandler(req, e)
}
}
routes:
const express = require('express')
const router = express.Router()
const controller = require('../controllers/user')
const passport = require('passport')
router.get('/profile', passport.authenticate('jwt', {session: false}), controller.getProfile)
module.exports = router
passport:
const JwtStrategy = require('passport-jwt').Strategy
const ExtractJwt = require('passport-jwt').ExtractJwt
const db = require('../config/db.config.js')
const User = db.user
const options = {
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: process.env.SECRET_OR_KEY
}
module.exports = passport => {
passport.use(
new JwtStrategy(options, async (payload, done) => {
try {
const user = await User.findById(payload.userId).toJSON('username id')
if (user) {
done(null, user)
} else {
done(null, false)
}
} catch(e) {
console.log(e)
}
})
)
}
And here is a part of the client:
app-components:
import { Component, OnInit } from '#angular/core';
import { AuthService } from './shared/services/auth.service';
#Component({
selector: 'app-root',
template: '<router-outlet></router-outlet>'
})
export class AppComponent implements OnInit {
constructor(private auth: AuthService) {
}
ngOnInit() {
const potentialToken = localStorage.getItem('auth-token')
if (potentialToken !== null) {
this.auth.setToken(potentialToken)
}
}
}
auth.service:
import { Injectable, Optional } from "#angular/core";
import { HttpClient } from "#angular/common/http";
import { Router, ActivatedRoute } from "#angular/router";
import { Observable } from "rxjs";
import { tap } from "rxjs/operators";
import { User } from "../interfaces";
#Injectable({
providedIn: 'root'
})
export class AuthService {
private token = null;
constructor(
private http: HttpClient,
#Optional() private _activatedRoute: ActivatedRoute,
#Optional() private _router: Router
) {}
login(user: User): Observable<{token: string}> {
return this.http.post<{token: string}>('/api/auth/login', user)
.pipe(
tap(
({token}) => {
localStorage.setItem('auth-token', token)
this.setToken(token)
}
)
)
}
setToken(token: string) {
this.token = token
}
getToken(): string {
return this.token
}
isAuthenticated(): boolean {
return !!this.token
}
logout() {
this.setToken(null)
localStorage.clear()
}
}
token.interceptor.ts:
import { Injectable } from "#angular/core";
import { AuthService } from "../services/auth.service";
import { HttpInterceptor, HttpRequest, HttpHandler, HttpEvent, HttpErrorResponse } from "#angular/common/http";
import { Observable, throwError } from "rxjs";
import { catchError } from "rxjs/operators";
import { Router } from "#angular/router";
#Injectable()
export class TokenInterceptor implements HttpInterceptor {
constructor(private auth: AuthService, private router: Router){
}
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
if (this.auth.isAuthenticated()) {
req = req.clone({
setHeaders: {
Authorization: this.auth.getToken()
}
})
}
return next.handle(req).pipe(
catchError(
(error: HttpErrorResponse) => this.handleAuthError(error)
)
)
}
private handleAuthError(error: HttpErrorResponse): Observable<any> {
if (error.status === 401) {
this.router.navigate(['/sign_in']), {
queryParams: {
sessionFailed: true
}
}
}
return throwError(error)
}
}
What need to implement on the client to display user data. Tell me please, I'm new.
You just need to create a Service class called UserService which will keep the information related to logged in user.
Whenever login action is performed successfully, just get the user details and fill it in UserService class.
user.service.ts
export class UserService {
private currentUser$: new BehaviorSubject<User>; //<-- check your user type
constructor(private http: Http) { }
getCurrentUser() {
this.currentUser$;
}
setCurrentUser(user:User){
this.currentUser$.next(user); //update the current user
}
}
This service class will give the Observable and you can use it any component or service class.