How to refresh token in Nestjs - javascript

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.

Related

How to get user data in Angular after login

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.

Error on ordering of methods in controller Nestjs

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.

NestJS Interceptor - Append data to incoming request Header or Body

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

Typescript ExpressJS - injecting services into REST API Controllers

I am attempting an OOP approach to my express rest API with Typescript and having issues composing classes.
The idea is to have a AuthController class that calls a private AuthService class to interact with the database. However, when I attempt to call the AuthService class with the AuthController class, it always returns undefined.
App class which initializes my express app and registers my controllers:
class App {
private app;
private controllers: Controller[];
constructor(controllers: Controller[]) {
this.app = express();
this.controllers = controllers;
this.config();
this.initializeRoutes();
this.initializeErrorHandler();
}
config() {
console.log("running config");
this.app.use(cors());
this.app.use(express.json());
this.app.use(helmet());
}
public listen() {
this.app.listen(3000);
}
initializeRoutes() {
this.controllers.forEach((controller: any) => {
this.app.use("/", controller.router);
});
}
initializeErrorHandler() {
this.app.use(errorMiddleware);
}
}
(async () => {
try {
await connection(); //creates my database connection
} catch (error) {
console.log("Error while connecting to the database", error);
return error;
}
const app = new App([new AuthController()]);
app.listen();
})();
here is my AuthController being initialized in my call to const app = new App([new AuthController()]);
export default class AuthController implements Controller {
public path = "/api/auth";
public router = Router();
private authService: AuthService = new AuthService();
constructor() {
this.initializeRoutes();
}
public initializeRoutes() {
//login route
this.router.post(this.path.concat("/login"), this.login);
this.router.post(
this.path.concat("/register"),
validationMiddleware(CreateUserDto),
this.register
);
this.router.post(this.path.concat("/resetpassword"), this.resetPassword);
this.router.post(this.path.concat("/newpassword"), this.newPassword);
}
public async register(req: Request, res: Response, next: NextFunction) {
const userData: CreateUserDto = req.body;
try {
let {
tokens: { xAuthToken, xRefreshToken },
user,
} = await this.authService.register(userData);
res.setHeader("x-auth-token", xAuthToken);
res.setHeader("x-refresh-token", xRefreshToken);
res.json(user);
} catch (e) {
next(e);
}
}
}
and finally AuthService class
export class AuthService {
private userRepo: Repository<User> = getRepository(User);
constructor() {}
public async register(userData: CreateUserDto) {
let foundUser = await this.userRepo.findOne({
where: { email: userData.email.toLowerCase() },
});
console.log(foundUser);
if (foundUser) {
throw new EmailInUseException();
} else {
console.log(foundUser);
const user = new User();
user.email = userData.email.toLowerCase();
user.password = userData.password;
await this.userRepo.save(user);
const tokens = this.createTokens(user);
return {
tokens,
user,
};
}
}
}
Anytime I call the AuthService from the AuthController, I receive an 'error cannot read property 'authService' of undefined'.
I have tried changing the code to initialize the AuthService directly const app = new App([new AuthController(new AuthService()]); but this doesn't fix the issue.
Any help is appreciated!
Found the issue. The 'this' context was being changed. I switched my 'register' method to an arrow function which fixed the problem
original:
class AuthController{
public async register(req: Request, res: Response, next: NextFunction) {
const userData: CreateUserDto = req.body;
try {
let {
tokens: { xAuthToken, xRefreshToken },
user,
} = await this.authService.register(userData);
res.setHeader("x-auth-token", xAuthToken);
res.setHeader("x-refresh-token", xRefreshToken);
res.json(user);
} catch (e) {
next(e);
}
}
}
new:
class AuthController{
public register = async (req: Request, res: Response, next: NextFunction) => {
const userData: CreateUserDto = req.body;
try {
let {
tokens: { xAuthToken, xRefreshToken },
user,
} = await this.authService.register(userData);
res.setHeader("x-auth-token", xAuthToken);
res.setHeader("x-refresh-token", xRefreshToken);
res.json(user);
} catch (e) {
next(e);
}
};
}

JWT expired token handling with Angular Interceptor keeps refreshing

I have an interceptor in Angular that I am using to refresh a token if it is expired, but the application seems to get caught in an endless call of 401 errors to the API when the token is successfully refreshed. When I step through the code, the token does indeed refresh if expired but then tries to refresh repeatedly.
I should also note that upon clicking the button again and calling the API again, the app picks up the new token and works properly afterward. Would love to get this working without so many console errors in the first place though.
Here is the interceptor (old)
import { Injectable, Injector } from "#angular/core";
import { Router } from "#angular/router";
import {
HttpClient,
HttpHandler, HttpEvent, HttpInterceptor,
HttpRequest, HttpResponse, HttpErrorResponse
} from "#angular/common/http";
import { AuthService } from "./auth.service";
import { Observable } from "rxjs/Observable";
#Injectable()
export class AuthResponseInterceptor implements HttpInterceptor {
currentRequest: HttpRequest<any>;
auth: AuthService;
constructor(
private injector: Injector,
private router: Router
) { }
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
this.auth = this.injector.get(AuthService);
var token = (this.auth.isLoggedIn()) ? this.auth.getAuth()!.token : null;
if (token) {
// save current request
this.currentRequest = request;
return next.handle(request)
.do((event: HttpEvent<any>) => {
if (event instanceof HttpResponse) {
// do nothing
}
})
.catch(error => {
return this.handleError(error)
});
}
else {
return next.handle(request);
}
}
handleError(err: any) {
if (err instanceof HttpErrorResponse) {
if (err.status === 401) {
// JWT token might be expired:
// try to get a new one using refresh token
console.log("Token expired. Attempting refresh...");
this.auth.refreshToken()
.subscribe(res => {
if (res) {
// refresh token successful
console.log("refresh token successful");
// re-submit the failed request
var http = this.injector.get(HttpClient);
http.request(this.currentRequest).subscribe(
(result: any) => {
console.log(this.currentRequest);
}, (error: any) => console.error(error)
);
}
else {
// refresh token failed
console.log("refresh token failed");
// erase current token
this.auth.logout();
// redirect to login page
this.router.navigate(["login"]);
}
}, error => console.log(error));
}
}
return Observable.throw(err);
}
}
EDIT:
Updated code to working solution
import { Injectable, Injector } from "#angular/core";
import { Router } from "#angular/router";
import {
HttpClient,
HttpHandler, HttpEvent, HttpInterceptor,
HttpRequest, HttpResponse, HttpErrorResponse, HttpHeaders
} from "#angular/common/http";
import { AuthService } from "./auth.service";
import { Observable, Subject } from "rxjs";
#Injectable()
export class AuthResponseInterceptor implements HttpInterceptor {
auth: AuthService;
currentRequest: HttpRequest<any>;
constructor(
private injector: Injector,
private router: Router
) { }
logout() {
this.auth.logout();
this.router.navigate(["login"]);
}
intercept(
request: HttpRequest<any>,
next: HttpHandler): Observable<HttpEvent<any>> {
this.auth = this.injector.get(AuthService);
let token = (this.auth.isLoggedIn()) ? this.auth.getAuth()!.token : null;
this.currentRequest = request;
return next.handle(request).
catch((error) => {
if (error instanceof HttpErrorResponse && error.status === 401) {
return this.auth.refreshToken()
.switchMap(() => {
let token = (Response) ? this.auth.getAuth() : null;
console.log(token);
if (token) {
this.currentRequest = request.clone({
setHeaders: {
Authorization: `Bearer ${token.token}`
}
});
}
return next.handle(this.currentRequest);
}).
catch((e) => {
this.logout();
console.error(e);
return Observable.empty();
});
}
return Observable.throw(error);
});
}
}
Auth.service
constructor(private http: HttpClient,
#Inject(PLATFORM_ID) private platformId: any) {
}
// performs the login
login(username: string, password: string): Observable<boolean> {
var url = "api/token/auth";
var data = {
username: username,
password: password,
client_id: this.clientId,
// required when signing up with username/password
grant_type: "password",
// space-separated list of scopes for which the token is issued
scope: "offline_access profile email"
};
return this.getAuthFromServer(url, data);
}
// try to refresh token
refreshToken(): Observable<boolean> {
var url = "api/token/auth";
var data = {
client_id: this.clientId,
// required when signing up with username/password
grant_type: "refresh_token",
refresh_token: this.getAuth()!.refresh_token,
// space-separated list of scopes for which the token is issued
scope: "offline_access profile email"
};
return this.getAuthFromServer(url, data);
}
// retrieve the access & refresh tokens from the server
getAuthFromServer(url: string, data: any): Observable<boolean> {
return this.http.post<TokenResponse>(url, data)
.map((res) => {
let token = res && res.token;
// if the token is there, login has been successful
if (token) {
// store username and jwt token
this.setAuth(res);
// successful login
return true;
}
// failed login
return Observable.throw('Unauthorized');
})
.catch(error => {
return new Observable<any>(error);
});
}
// performs the logout
logout(): boolean {
this.setAuth(null);
return true;
}
// Persist auth into localStorage or removes it if a NULL argument is given
setAuth(auth: TokenResponse | null): boolean {
if (isPlatformBrowser(this.platformId)) {
if (auth) {
localStorage.setItem(
this.authKey,
JSON.stringify(auth));
}
else {
localStorage.removeItem(this.authKey);
}
}
return true;
}
// Retrieves the auth JSON object (or NULL if none)
getAuth(): TokenResponse | null {
if (isPlatformBrowser(this.platformId)) {
var i = localStorage.getItem(this.authKey);
if (i) {
return JSON.parse(i);
}
}
return null;
}
// Returns TRUE if the user is logged in, FALSE otherwise.
isLoggedIn(): boolean {
if (isPlatformBrowser(this.platformId)) {
return localStorage.getItem(this.authKey) != null;
}
return false;
}
return this.auth.refreshToken(response:any)
//response can be true or null
let token=(response)?this.auth.getAuth():null;
//In token we have an object of type TokenResponse
console.log(token)
.switchMap(() => {
if (token) {
this.currentRequest = request.clone({
setHeaders: { //I think it's toke.token
Authorization: `Bearer ${token.token}`
}
});
....
NOTE: Try to change "var" for "let"
NOTE2: At first you have
var token = (this.auth.isLoggedIn()) ? this.auth.getAuth()!.token : null;
// May be remove "!"?
let token = (this.auth.isLoggedIn()) ? this.auth.getAuth().token : null;
If you want separate the error handle you can do some like
handleError(err: any) {
if (err instanceof HttpErrorResponse) {
if (err.status === 401) {
this.auth.refreshToken()
.switchMap(res=>{ //<--switchMap, not susbcribe
if (res) {
console.log("refresh token successful");
// re-submit the failed request
var http = this.injector.get(HttpClient);
//Modify there this.currentRequest if was neccesary
return next.handle(this.currentRequest).catch(error:any=>
{
console.error(error);
return Observable.throw(error);
});
}
else {
console.log("refresh token failed");
this.auth.logout();
this.router.navigate(["login"]);
}
})
}
}
return Observable.throw(err);
}

Categories

Resources