Skip to content

Commit dda9aee

Browse files
committed
Added feature to store JWT and validate them.
1 parent 74b9850 commit dda9aee

File tree

6 files changed

+154
-3
lines changed

6 files changed

+154
-3
lines changed

package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
"dependencies": {
2828
"@fusionauth/typescript-client": "^1.43.0",
2929
"@golevelup/ts-jest": "^0.3.5",
30+
"@nestjs-modules/ioredis": "^2.0.2",
3031
"@nestjs/axios": "^0.0.7",
3132
"@nestjs/common": "^8.*",
3233
"@nestjs/config": "^1.0.1",
@@ -47,6 +48,9 @@
4748
"flagsmith-nodejs": "^2.5.2",
4849
"got": "^11.8.2",
4950
"helmet": "^7.0.0",
51+
"ioredis": "^5.4.1",
52+
"jsonwebtoken": "^9.0.2",
53+
"jwks-rsa": "^3.1.0",
5054
"passport": "^0.5.2",
5155
"passport-http": "^0.3.0",
5256
"passport-jwt": "^4.0.1",

src/api/api.controller.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import { VerifyOtpDto } from './dto/verify-otp.dto';
3535
import { Throttle, SkipThrottle} from '@nestjs/throttler';
3636
import { ConfigService } from '@nestjs/config';
3737
import { v4 as uuidv4 } from 'uuid';
38+
import { VerifyJWTDto } from './dto/verify-jwt.dto';
3839
// eslint-disable-next-line @typescript-eslint/no-var-requires
3940
const CryptoJS = require('crypto-js');
4041

@@ -381,4 +382,20 @@ export class ApiController {
381382
}
382383
return await this.apiService.loginWithUniqueId(user, authHeader);
383384
}
385+
386+
@Post('jwt/verify')
387+
@UsePipes(new ValidationPipe({transform: true}))
388+
async jwtVerify(
389+
@Body() body: VerifyJWTDto
390+
): Promise<any> {
391+
return await this.apiService.verifyJWT(body.token);
392+
}
393+
394+
@Post('logout')
395+
@UsePipes(new ValidationPipe({transform: true}))
396+
async logout(
397+
@Body() body: VerifyJWTDto
398+
): Promise<any> {
399+
return await this.apiService.logout(body.token);
400+
}
384401
}

src/api/api.service.ts

Lines changed: 116 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@ const CryptoJS = require('crypto-js');
3131
const AES = require('crypto-js/aes');
3232
import Flagsmith from 'flagsmith-nodejs';
3333
import { LoginWithUniqueIdDto } from './dto/login.dto';
34+
import { InjectRedis } from '@nestjs-modules/ioredis';
35+
import Redis from 'ioredis';
36+
const jwksClient = require('jwks-rsa');
37+
import * as jwt from 'jsonwebtoken';
3438

3539
CryptoJS.lib.WordArray.words;
3640

@@ -43,6 +47,7 @@ export class ApiService {
4347
private readonly fusionAuthService: FusionauthService,
4448
private readonly otpService: OtpService,
4549
private readonly configResolverService: ConfigResolverService,
50+
@InjectRedis() private readonly redis: Redis
4651
) {}
4752

4853
login(user: any, authHeader: string): Promise<SignupResponse> {
@@ -535,6 +540,7 @@ export class ApiService {
535540
3.2. If new user, register to this application.
536541
4. Send login response with the token
537542
*/
543+
let otp = loginDto.password;
538544
const salt = this.configResolverService.getSalt(loginDto.applicationId);
539545
let verifyOTPResult;
540546
if(
@@ -562,6 +568,7 @@ export class ApiService {
562568
loginDto.password = salt + loginDto.password; // mix OTP with salt
563569

564570
if (verifyOTPResult.status === SMSResponseStatus.success) {
571+
let response;
565572
const {
566573
statusFA,
567574
userId,
@@ -595,19 +602,31 @@ export class ApiService {
595602
id: registrationId,
596603
},
597604
],
605+
data: {
606+
loginId: loginDto.loginId,
607+
fingerprint: loginDto?.fingerprint,
608+
timestamp: loginDto?.timestamp,
609+
otp
610+
}
598611
},
599612
loginDto.applicationId,
600613
authHeader,
601614
);
602-
return this.login(loginDto, authHeader);
615+
response = await this.login(loginDto, authHeader);
603616
} else {
604617
// create a new user
605618
const createUserPayload: UserRegistration = {
606619
user: {
607620
timezone: "Asia/Kolkata",
608621
username: loginDto.loginId,
609622
mobilePhone: loginDto.loginId,
610-
password: loginDto.password
623+
password: loginDto.password,
624+
data: {
625+
loginId: loginDto.loginId,
626+
fingerprint: loginDto?.fingerprint,
627+
timestamp: loginDto?.timestamp,
628+
otp
629+
}
611630
},
612631
registration: {
613632
applicationId: loginDto.applicationId,
@@ -626,8 +645,17 @@ export class ApiService {
626645
if (userId == null || user == null) {
627646
throw new HttpException(err, HttpStatus.BAD_REQUEST);
628647
}
629-
return this.login(loginDto, authHeader);
648+
response = await this.login(loginDto, authHeader);
649+
}
650+
let existingJWTS:any = await this.redis.get(response?.result?.data?.user?.user?.id);
651+
if(existingJWTS) {
652+
existingJWTS = JSON.parse(existingJWTS);
653+
} else {
654+
existingJWTS = []
630655
}
656+
existingJWTS.push(response?.result?.data?.user?.token);
657+
await this.redis.set(response?.result?.data?.user?.user?.id, JSON.stringify(existingJWTS));
658+
return response;
631659
} else {
632660
const response: SignupResponse = new SignupResponse().init(uuidv4());
633661
response.responseCode = ResponseCode.FAILURE;
@@ -706,4 +734,89 @@ export class ApiService {
706734
}
707735
return registration;
708736
}
737+
738+
async verifyFusionAuthJWT(token: string): Promise<any> {
739+
let client = jwksClient({
740+
jwksUri: this.configService.get("JWKS_URI"),
741+
requestHeaders: {}, // Optional
742+
timeout: 30000, // Defaults to 30s
743+
});
744+
745+
let getKey = (header: jwt.JwtHeader, callback: any) => {
746+
client.getSigningKey(header.kid, (err, key: any) => {
747+
if (err) {
748+
console.error(`Error fetching signing key: ${err}`);
749+
callback(err);
750+
} else {
751+
const signingKey = key.publicKey || key.rsaPublicKey;
752+
callback(null, signingKey);
753+
}
754+
});
755+
};
756+
757+
return new Promise<any>((resolve, reject) => {
758+
jwt.verify(token, getKey, async (err, decoded) => {
759+
if (err) {
760+
console.error('APP JWT verification error:', err);
761+
resolve({
762+
isValidFusionAuthToken: false,
763+
claims: null
764+
})
765+
} else {
766+
resolve({
767+
isValidFusionAuthToken: true,
768+
claims: decoded
769+
})
770+
}
771+
});
772+
});
773+
}
774+
775+
async verifyJWT(token:string): Promise<any> {
776+
const { isValidFusionAuthToken, claims} = await this.verifyFusionAuthJWT(token);
777+
778+
let existingUserJWTS:any = JSON.parse(await this.redis.get(claims.sub));
779+
780+
if(!isValidFusionAuthToken){
781+
if(existingUserJWTS.indexOf(token)!=-1){
782+
existingUserJWTS.splice(existingUserJWTS.indexOf(token), 1);
783+
await this.redis.set(claims.sub, JSON.stringify(existingUserJWTS));
784+
}
785+
return {
786+
"isValid": false,
787+
"message": "Invalid/Expired token."
788+
}
789+
}
790+
791+
if(existingUserJWTS.indexOf(token)==-1){
792+
return {
793+
"isValid": false,
794+
"message": "Token is not authorized."
795+
}
796+
}
797+
798+
return {
799+
"isValid": true,
800+
"message": "Token is valid."
801+
}
802+
}
803+
804+
async logout(token:string): Promise<any> {
805+
const { isValidFusionAuthToken, claims} = await this.verifyFusionAuthJWT(token);
806+
if(isValidFusionAuthToken){
807+
let existingUserJWTS:any = JSON.parse(await this.redis.get(claims.sub));
808+
if(existingUserJWTS.indexOf(token)!=-1){
809+
existingUserJWTS.splice(existingUserJWTS.indexOf(token), 1);
810+
await this.redis.set(claims.sub, JSON.stringify(existingUserJWTS));
811+
}
812+
return {
813+
"message": "Logout successful. Token invalidated."
814+
}
815+
} else {
816+
return {
817+
"message": "Invalid or expired token."
818+
}
819+
}
820+
}
821+
709822
}

src/api/dto/verify-jwt.dto.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import {
2+
IsNotEmpty, IsString,
3+
} from 'class-validator';
4+
5+
export class VerifyJWTDto {
6+
@IsString()
7+
@IsNotEmpty()
8+
token: string;
9+
}
10+

src/app.module.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { AuthModule } from './auth/auth.module';
1717
import { ApiModule } from './api/api.module';
1818
import got from 'got/dist/source';
1919
import { TerminusModule } from '@nestjs/terminus';
20+
import { RedisModule } from '@nestjs-modules/ioredis';
2021

2122
const gupshupFactory = {
2223
provide: 'GupshupService',
@@ -47,6 +48,10 @@ const otpServiceFactory = {
4748
ttl: parseInt(process.env.RATE_LIMIT_TTL), //Seconds
4849
limit: parseInt(process.env.RATE_LIMIT), //Number of requests per TTL from a single IP
4950
}),
51+
RedisModule.forRoot({
52+
type: 'single',
53+
url: process.env.REDIS_URL,
54+
}),
5055
AdminModule,
5156
DstModule,
5257
AuthModule,

src/user/dto/login.dto.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,6 @@ export class LoginDto {
55
password: string;
66
applicationId: UUID;
77
roles?: Array<string>;
8+
fingerprint?: string;
9+
timestamp?: string;
810
}

0 commit comments

Comments
 (0)