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.
To refresh the token, the user needs to call a separate endpoint, called /refresh. This time, the refresh token is taken from the cookies and sent to the API. If it is valid and not expired, the user receives the new access token. Thanks to that, there is no need to provide the username and password again.
The token will be used to validate users that need to access protected routes. To achieve that we will be making use of some packages: npm install --save @nestjs/passport @nestjs/jwt passport-jwt npm install --save-dev @types/passport-jwt.
Refresh Tokens: It is a unique token that is used to obtain additional access tokens. This allows you to have short-lived access tokens without having to collect credentials every time one expires.
Refresh token: The refresh token is used to generate a new access token. Typically, if the access token has an expiration date, once it expires, the user would have to authenticate again to obtain an access token.
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; };
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With