Skip to content

Commit efaf784

Browse files
Added feature to store JWT and validate them. (#101)
* Added feature to store JWT and validate them. * moved jwks client initalization to constructor
1 parent 74b9850 commit efaf784

File tree

6 files changed

+157
-4
lines changed

6 files changed

+157
-4
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: 119 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,19 +31,44 @@ 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

3741
@Injectable()
3842
export class ApiService {
43+
private client: any
44+
private getKey: any;
3945
encodedBase64Key;
4046
parsedBase64Key;
4147
constructor(
4248
private configService: ConfigService,
4349
private readonly fusionAuthService: FusionauthService,
4450
private readonly otpService: OtpService,
4551
private readonly configResolverService: ConfigResolverService,
46-
) {}
52+
@InjectRedis() private readonly redis: Redis
53+
) {
54+
this.client = jwksClient({
55+
jwksUri: this.configService.get("JWKS_URI"),
56+
requestHeaders: {}, // Optional
57+
timeout: 30000, // Defaults to 30s
58+
});
59+
60+
this.getKey = (header: jwt.JwtHeader, callback: any) => {
61+
this.client.getSigningKey(header.kid, (err, key: any) => {
62+
if (err) {
63+
console.error(`Error fetching signing key: ${err}`);
64+
callback(err);
65+
} else {
66+
const signingKey = key.publicKey || key.rsaPublicKey;
67+
callback(null, signingKey);
68+
}
69+
});
70+
};
71+
}
4772

4873
login(user: any, authHeader: string): Promise<SignupResponse> {
4974
return this.fusionAuthService
@@ -535,6 +560,7 @@ export class ApiService {
535560
3.2. If new user, register to this application.
536561
4. Send login response with the token
537562
*/
563+
let otp = loginDto.password;
538564
const salt = this.configResolverService.getSalt(loginDto.applicationId);
539565
let verifyOTPResult;
540566
if(
@@ -562,6 +588,7 @@ export class ApiService {
562588
loginDto.password = salt + loginDto.password; // mix OTP with salt
563589

564590
if (verifyOTPResult.status === SMSResponseStatus.success) {
591+
let response;
565592
const {
566593
statusFA,
567594
userId,
@@ -595,19 +622,31 @@ export class ApiService {
595622
id: registrationId,
596623
},
597624
],
625+
data: {
626+
loginId: loginDto.loginId,
627+
fingerprint: loginDto?.fingerprint,
628+
timestamp: loginDto?.timestamp,
629+
otp
630+
}
598631
},
599632
loginDto.applicationId,
600633
authHeader,
601634
);
602-
return this.login(loginDto, authHeader);
635+
response = await this.login(loginDto, authHeader);
603636
} else {
604637
// create a new user
605638
const createUserPayload: UserRegistration = {
606639
user: {
607640
timezone: "Asia/Kolkata",
608641
username: loginDto.loginId,
609642
mobilePhone: loginDto.loginId,
610-
password: loginDto.password
643+
password: loginDto.password,
644+
data: {
645+
loginId: loginDto.loginId,
646+
fingerprint: loginDto?.fingerprint,
647+
timestamp: loginDto?.timestamp,
648+
otp
649+
}
611650
},
612651
registration: {
613652
applicationId: loginDto.applicationId,
@@ -626,8 +665,17 @@ export class ApiService {
626665
if (userId == null || user == null) {
627666
throw new HttpException(err, HttpStatus.BAD_REQUEST);
628667
}
629-
return this.login(loginDto, authHeader);
668+
response = await this.login(loginDto, authHeader);
669+
}
670+
let existingJWTS:any = await this.redis.get(response?.result?.data?.user?.user?.id);
671+
if(existingJWTS) {
672+
existingJWTS = JSON.parse(existingJWTS);
673+
} else {
674+
existingJWTS = []
630675
}
676+
existingJWTS.push(response?.result?.data?.user?.token);
677+
await this.redis.set(response?.result?.data?.user?.user?.id, JSON.stringify(existingJWTS));
678+
return response;
631679
} else {
632680
const response: SignupResponse = new SignupResponse().init(uuidv4());
633681
response.responseCode = ResponseCode.FAILURE;
@@ -706,4 +754,71 @@ export class ApiService {
706754
}
707755
return registration;
708756
}
757+
758+
async verifyFusionAuthJWT(token: string): Promise<any> {
759+
return new Promise<any>((resolve, reject) => {
760+
jwt.verify(token, this.getKey, async (err, decoded) => {
761+
if (err) {
762+
console.error('APP JWT verification error:', err);
763+
resolve({
764+
isValidFusionAuthToken: false,
765+
claims: null
766+
})
767+
} else {
768+
resolve({
769+
isValidFusionAuthToken: true,
770+
claims: decoded
771+
})
772+
}
773+
});
774+
});
775+
}
776+
777+
async verifyJWT(token:string): Promise<any> {
778+
const { isValidFusionAuthToken, claims} = await this.verifyFusionAuthJWT(token);
779+
780+
let existingUserJWTS:any = JSON.parse(await this.redis.get(claims.sub));
781+
782+
if(!isValidFusionAuthToken){
783+
if(existingUserJWTS.indexOf(token)!=-1){
784+
existingUserJWTS.splice(existingUserJWTS.indexOf(token), 1);
785+
await this.redis.set(claims.sub, JSON.stringify(existingUserJWTS));
786+
}
787+
return {
788+
"isValid": false,
789+
"message": "Invalid/Expired token."
790+
}
791+
}
792+
793+
if(existingUserJWTS.indexOf(token)==-1){
794+
return {
795+
"isValid": false,
796+
"message": "Token is not authorized."
797+
}
798+
}
799+
800+
return {
801+
"isValid": true,
802+
"message": "Token is valid."
803+
}
804+
}
805+
806+
async logout(token:string): Promise<any> {
807+
const { isValidFusionAuthToken, claims} = await this.verifyFusionAuthJWT(token);
808+
if(isValidFusionAuthToken){
809+
let existingUserJWTS:any = JSON.parse(await this.redis.get(claims.sub));
810+
if(existingUserJWTS.indexOf(token)!=-1){
811+
existingUserJWTS.splice(existingUserJWTS.indexOf(token), 1);
812+
await this.redis.set(claims.sub, JSON.stringify(existingUserJWTS));
813+
}
814+
return {
815+
"message": "Logout successful. Token invalidated."
816+
}
817+
} else {
818+
return {
819+
"message": "Invalid or expired token."
820+
}
821+
}
822+
}
823+
709824
}

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)