I am trying to modify an NestJS incoming request and append some data either to header or Body. I was able to replace all the body data with my data but i would like to append and not remove the incoming body data.
Here is the code i have
export class MyInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const request = context.switchToHttp().getRequest();
const token = request.headers['authorization'];
if (token) {
const decoded = jwt_decode(token);
request.body['userId'] = decoded['id'];
}
return next.handle();
}
}
Thanks in advance
I have added two examples as after running testing for the interceptor, it passed without any issue. Of course, my example will be very different to your set up, however, hopefully it'll give you enough insight:
The test file:
test('should not mutate entire request body object', () => {
const dto = {
username: 'testuser',
email: 'test#domain.com',
};
const headers = {
authorization: 'Bearer sdkfjdsakfjdkjfdal',
};
return request(app.getHttpServer())
.post('/')
.send(dto)
.set(headers)
.expect(({ body }) => {
expect(body.userId).toBeDefined();
delete body.userId;
expect(body).toStrictEqual(dto);
});
});
I understand your problem as attempting to obtain information about the authenticated user, and return it/use it later on? However, your current implementation seems to completely override the request.body instead of append your property to the original object.
Interceptor:
#Injectable()
export class HttpRequestBodyInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable {
const request = context.switchToHttp().getRequest();
const token = request.headers['authorization'];
if (token) {
// decode token
request.body['userId'] = 'user_123456789';
}
return next.handle();
}
}
Controller:
#Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
#Post()
#UseInterceptors(HttpRequestBodyInterceptor)
getHello(#Req() req): string {
return req.body;
}
}
This returns the correct response and the test will pass. However, you may find a more robust solution would be:
#Injectable()
export class HttpRequestBodyInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable {
const request = context.switchToHttp().getRequest();
const token = request.headers['authorization'];
if (token) {
// decode token
request.userId = 'user_123456789';
}
return next.handle();
}
}
And then access this in your controller by:
#Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
#Post()
#UseInterceptors(HttpRequestBodyInterceptor)
getHello(#Req() req) {
return {
userId: req.userId,
...req.body,
};
}
}
Finally, if your only need for an interceptor is to obtain that userId property, you may find that https://docs.nestjs.com/security/authentication#jwt-functionality is useful.
#Injectable()
export class JwtInterceptor implements NestInterceptor {
constructor(private readonly jwtService: JwtService, private readonly
userService: UserService) { }
async intercept(context: ExecutionContext, next: CallHandler):
Promise<Observable<any>> {
var request: WsArgumentsHost = context.switchToWs();
var { handshake: { headers: { authorization } } } =
request.getClient();
try {
var jwt = authorization.split(" ")[1];
var { phone } = await this.jwtService.verify(jwt, jwtConstraints)
var user: User = await this.userService.findUserByPhoneNumber(phone);
request.getData()["user"]=user;
return next.handle().pipe(map((data) => { return { ...data, 'user': "david" }; }));
i hope this will help someone in future while working with socket.i wanted the user object in the body after they pass authentication .the above trick worked out for me
Related
What I want to do is to get the user data and output it anywhere on my website. For example I would like to get the name for the user and output it on the homepage when the user is logged in.
any ideas ? Thanks
AuthService
import { Injectable } from '#angular/core';
import { HttpClient } from '#angular/common/http';
import { EnvironmentUrlService } from './environment-url.service';
import { UserRegistrationDto } from '../models/user/UserRegistrationDto.model';
import { RegistrationResponseDto } from '../models/user/response/RegistrationResponseDto.model';
import { UserAuthenticationDto } from '../models/user/UserAuthenticationDto.model';
import { AuthResponseDto, user } from '../models/user/response/AuthResponseDto.model';
import { Subject, BehaviorSubject, Observable, map } from 'rxjs';
import { JwtHelperService } from '#auth0/angular-jwt';
#Injectable({
providedIn: 'root'
})
export class AuthenticationService {
private authChangeSub = new Subject<boolean>()
public authChanged = this.authChangeSub.asObservable();
constructor(private http: HttpClient, private envUrl: EnvironmentUrlService, private jwtHelper: JwtHelperService) {}
public registerUser = (route: string, body: UserRegistrationDto) => {
return this.http.post<RegistrationResponseDto> (this.createCompleteRoute(route, this.envUrl.urlAddress), body);
}
public loginUser = (route: string, body: UserAuthenticationDto) => {
return this.http.post<AuthResponseDto>(this.createCompleteRoute(route, this.envUrl.urlAddress), body);
}
public sendAuthStateChangeNotification = (isAuthenticated: boolean) => {
this.authChangeSub.next(isAuthenticated);
}
public logout = () => {
sessionStorage.removeItem("token");
this.sendAuthStateChangeNotification(false);
}
public isUserAuthenticated = (): boolean => {
const token = sessionStorage.getItem("token");
return token && !this.jwtHelper.isTokenExpired(token);
}
private createCompleteRoute = (route: string, envAddress: string) => {
return `${envAddress}/${route}`;
}
}
login.component.ts
loginUser = (loginFormValue: any) => {
this.showError = false;
const formValues = {... loginFormValue };
const userForAuth: UserAuthenticationDto = {
email: formValues.email,
password: formValues.password
}
this.authService.loginUser('api/accounts/login', userForAuth)
.subscribe({
next: (res:AuthResponseDto) => {
sessionStorage.setItem("token", res.token);
this.authService.sendAuthStateChangeNotification(res.isAuthSuccessful);
this.notificationService.showNotification('success','Login successfully')
this.router.navigate([this.returnUrl]);
},
error: (err: HttpErrorResponse) => {
this.errorMessage = err.message;
this.showError = true;
}})
}
**AuthResponse & User **
export interface AuthResponseDto {
isAuthSuccessful: boolean;
errorMessage: string;
token: string;
}
export interface user {
userId: string;
userName: string
firstName: string;
lastName: string;
role: string []
}
`
I can successfully register and log in a user. I can get the user data from the token but can't map it to user interface
You have to subscribe the api or function user login to get the user information. Also the local storage key value pairing is not used properly here. check user login function, it shows this. set Token(Users[0].
If the data is available in the token received, you can extract it after login and retrieve it with a getter method that return user if user is authenticated, null otherwise (or using promises, that would be cleaner).
The getter method retrieves the data stored in the token of the sessionStorage (if exists) and returns it formatted as user interface. Then you can access the user data fron any component, just import the service in the constructor and call the getter to get the data.
I have this basic CRUD methods in Nestjs.
The issue I am facing is that when I am applying the getCurrentUserId() method on top on all methods it works fine but when I am applying in bottom it doesnt work and gives error.
Is there anything wrong with middleware ?
user.controller.ts
#Controller('users')
#Serialize(UserDto)
export class UsersController {
constructor(private usersService: UsersService) {}
#Post('/signup')
create(#Body() createUserDto: CreateUserDto): Promise<User> {
return this.usersService.create(createUserDto);
}
#Get('/#:userName')
async getUserByUsername(#Param('userName') userName: string) {
const user = await this.usersService.findByName(userName);
console.log(userName);
if (!user) {
throw new NotFoundException('User Not Found');
}
return user;
}
//! Testing for current user
#Get('/current')
#UseGuards(JwtAuthGuard)
async getCurrentUserId(#CurrentUser() id: string) {
console.log('running endpoint');
return id;
}
}
current-user.decorator.ts
import { createParamDecorator, ExecutionContext } from '#nestjs/common';
export const CurrentUser = createParamDecorator(
(data : unknown , context : ExecutionContext) => {
const req = context.switchToHttp().getRequest();
console.log("I am running")
return req.id;
}
)
current-user.middleware.ts
#Injectable()
export class CurrentUserMiddleware implements NestMiddleware {
constructor(private usersService: UsersService) {}
async use(req: RequestId, res: Response, next: NextFunction) {
const token = req.headers['authorization'];
console.log(token);
if (!token) {
throw new UnauthorizedException('Unauthorized');
}
try {
const { userId } =
await this.usersService.getUserByToken(token);
req.id = userId;
console.log(req.id)
next();
} catch {
throw new UnauthorizedException();
}
}
}
And I have added the middleware to user.module.ts like this
export class UsersModule {
configure(consumer: MiddlewareConsumer) {
consumer.apply(CurrentUserMiddleware).forRoutes(
'users/current'
);
}
}
The route is matching on #Get('/#:userName') before it makes it to #Get('/current') so its executing the code inside of your getUserByUsername method instead.
Just move getCurrentUserId to the top and you should be fine.
Routes are evaluated in the order they are defined and the first matching one is used to handle the request. In general you should always put the most specific routes (the ones without route params) at the top of your controller to avoid this problem.
I faced an issue with update method. I'm working on a spring boot angular project so update doesnt work on frontend my code looking logic can someone gie me an idea about this issue
user.service
updateProfile(userData: SignUpData, id: string ): Observable<any> {
return this.http.patch( API_URL + 'update/' + id, userData, httpOptions);
}
component.ts
form: any = {};
id: string;
errorMessage = '';
currentUser: any;
constructor(private userservice: UserService, private route: ActivatedRoute, private router: Router, private token: TokenStorageService) { }
ngOnInit() {
this.currentUser = this.token.getUser();
}
onSubmit() {
const {adresse1, ...rest} = this.form;
const userData: SignUpData = {...rest, adresses: [adresse1]};
this.userservice.updateProfile(userData, this.currentUser.id).subscribe(
data => {
console.log(data);
},
err => {
this.errorMessage = err.error.message;
}
);
}
Interceptor.ts
export class AuthInterceptor implements HttpInterceptor {
constructor(private token: TokenStorageService) { }
intercept(req: HttpRequest<any>, next: HttpHandler) {
let authReq = req;
const token = this.token.getToken();
if (token != null) {
authReq = req.clone({ headers: req.headers.set(TOKEN_HEADER_KEY, 'Bearer ' + token) });
}
return next.handle(authReq);
}
}
export const authInterceptorProviders = [
{ provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true }
];
I try to logout & log in and that's working because first I didn't get user id so I add this.currentUser = this.token.getUser(); without refresh authenticate so GET was always returning 401 not found. I hope this answer can help people that have the same issue And thank you guys for your replies
import { ExtractJwt, Strategy } from 'passport-jwt';
import { AuthService } from './auth.service';
import { PassportStrategy } from '#nestjs/passport';
import { Injectable, UnauthorizedException } from '#nestjs/common';
import { JwtPayload } from './model/jwt-payload.model';
#Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(private readonly authService: AuthService) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: 'secretKey',
});
}
async validate(payload: JwtPayload) {
const user = await this.authService.validateUser(payload);
if (!user) {
throw new UnauthorizedException();
}
return true;
}
}
Token is extracted from the request by PassportStrategy. I don't know how to catch the error when the token expires or gets invalid. My purpose is if there is an error because the token expired, I need to refresh the token. Otherwise do something else.
Refresh token implementation could be handled in canActivate method in custom auth guard.
If the access token is expired, the refresh token will be used to obtain a new access token. In that process, refresh token is updated too.
If both tokens aren't valid, cookies will be cleared.
#Injectable()
export class CustomAuthGuard extends AuthGuard('jwt') {
private logger = new Logger(CustomAuthGuard.name);
constructor(
private readonly authService: AuthService,
private readonly userService: UserService,
) {
super();
}
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest();
const response = context.switchToHttp().getResponse();
try {
const accessToken = ExtractJwt.fromExtractors([cookieExtractor])(request);
if (!accessToken)
throw new UnauthorizedException('Access token is not set');
const isValidAccessToken = this.authService.validateToken(accessToken);
if (isValidAccessToken) return this.activate(context);
const refreshToken = request.cookies[REFRESH_TOKEN_COOKIE_NAME];
if (!refreshToken)
throw new UnauthorizedException('Refresh token is not set');
const isValidRefreshToken = this.authService.validateToken(refreshToken);
if (!isValidRefreshToken)
throw new UnauthorizedException('Refresh token is not valid');
const user = await this.userService.getByRefreshToken(refreshToken);
const {
accessToken: newAccessToken,
refreshToken: newRefreshToken,
} = this.authService.createTokens(user.id);
await this.userService.updateRefreshToken(user.id, newRefreshToken);
request.cookies[ACCESS_TOKEN_COOKIE_NAME] = newAccessToken;
request.cookies[REFRESH_TOKEN_COOKIE_NAME] = newRefreshToken;
response.cookie(ACCESS_TOKEN_COOKIE_NAME, newAccessToken, COOKIE_OPTIONS);
response.cookie(
REFRESH_TOKEN_COOKIE_NAME,
newRefreshToken,
COOKIE_OPTIONS,
);
return this.activate(context);
} catch (err) {
this.logger.error(err.message);
response.clearCookie(ACCESS_TOKEN_COOKIE_NAME, COOKIE_OPTIONS);
response.clearCookie(REFRESH_TOKEN_COOKIE_NAME, COOKIE_OPTIONS);
return false;
}
}
async activate(context: ExecutionContext): Promise<boolean> {
return super.canActivate(context) as Promise<boolean>;
}
handleRequest(err, user) {
if (err || !user) {
throw new UnauthorizedException();
}
return user;
}
}
Attaching user to the request is done in validate method in JwtStrategy class, it will be called if the access token is valid
#Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(
readonly configService: ConfigService,
private readonly userService: UserService,
) {
super({
jwtFromRequest: cookieExtractor,
ignoreExpiration: false,
secretOrKey: configService.get('jwt.secret'),
});
}
async validate({ id }): Promise<User> {
const user = await this.userService.get(id);
if (!user) {
throw new UnauthorizedException();
}
return user;
}
}
Example for custom cookie extractor
export const cookieExtractor = (request: Request): string | null => {
let token = null;
if (request && request.signedCookies) {
token = request.signedCookies[ACCESS_TOKEN_COOKIE_NAME];
}
return token;
};
Instead of using the built-in AuthGuard you can create your own one and overwrite the request handler:
#Injectable()
export class MyAuthGuard extends AuthGuard('jwt') {
handleRequest(err, user, info: Error) {
if (info instanceof TokenExpiredError) {
// do stuff when token is expired
console.log('token expired');
}
return user;
}
}
Depending on what you want to do, you can also overwrite the canActivate method where you have access to the request object. Have a look at the AuthGuard sourcecode.
What I want to achieve is to handle somehow every Http request I'm making and on every request change my variable state. So I made my custom Http service that wraps Angular 2 Http service:
import {Injectable} from '#angular/core';
import {Http, Headers, Response} from '#angular/http';
import {Observable} from "rxjs";
import 'rxjs/add/operator/map';
#Injectable()
export class HttpClientService {
public isLoading: boolean = false;
constructor(private http: Http) {}
get(url) {
let headers = new Headers();
this.isLoadingHttp(true);
return this.http.get(url, {
headers: headers
});
}
isLoadingHttp( state: boolean ): void {
this.isLoading = state;
}
}
So I have isLoading variable and isLoadingHttp function.
First question - Basically, on GET method started I'm setting variable to true, but how do I know when request has made and response is ready?
Second question: Do I need to make isLoading and Observable? I want to access it from my AppComponent and manipulate when to display loader whenever it has changed.
#Injectable()
export class HttpClientService {
private _isLoading: number = 0;
public get isLoading () {
return this._isLoading;
}
constructor(private http: Http) {}
get(url) {
let headers = new Headers();
this._isLoading++;
return this.http.get(url, {
headers: headers
})
.finally(_ => this._isLoading--);
}
}
There can be more than one active request at a time.
The finally operator needs to be imported like any other operator.
#Injectable()
export class HttpClientService {
private requestCounter: number = 0;
private isLoading: Subject<number> = new BehaviorSubject<number>(requestCounter);
public readonly isLoading$:Observable<number> = this._isLoading.asObservable().share();
constructor(private http: Http) {}
get(url) {
let headers = new Headers();
this.isLoading.next(++this.requestCounter);
return this.http.get(url, {
headers: headers
})
.finally(_ => this.isLoading.next(--this.requestCounter));
}
}
of if you don't care how many outstanding request there are, but just if there are any
#Injectable()
export class HttpClientService {
private requestCounter: number = 0;
private isLoading: Subject<boolean> = new BehaviorSubject<boolean>(false);
public readonly isLoading$:Observable<boolean> = this._isLoading.asObservable().share();
constructor(private http: Http) {}
get(url) {
let headers = new Headers();
this.requestCounter++;
if(this.requestCounter == 1) {
this.isLoading.next(true);
}
return this.http.get(url, {
headers: headers
})
.finally(_ => {
this.requestCounter--;
if(this.requestCounter == 0) {
this.isLoading.next(false));
}
})
}
}