Skip to content

Commit

Permalink
Added feature to store JWT and validate them.
Browse files Browse the repository at this point in the history
  • Loading branch information
Amruth-Vamshi committed Nov 20, 2024
1 parent 74b9850 commit dda9aee
Show file tree
Hide file tree
Showing 6 changed files with 154 additions and 3 deletions.
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"dependencies": {
"@fusionauth/typescript-client": "^1.43.0",
"@golevelup/ts-jest": "^0.3.5",
"@nestjs-modules/ioredis": "^2.0.2",
"@nestjs/axios": "^0.0.7",
"@nestjs/common": "^8.*",
"@nestjs/config": "^1.0.1",
Expand All @@ -47,6 +48,9 @@
"flagsmith-nodejs": "^2.5.2",
"got": "^11.8.2",
"helmet": "^7.0.0",
"ioredis": "^5.4.1",
"jsonwebtoken": "^9.0.2",
"jwks-rsa": "^3.1.0",
"passport": "^0.5.2",
"passport-http": "^0.3.0",
"passport-jwt": "^4.0.1",
Expand Down
17 changes: 17 additions & 0 deletions src/api/api.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import { VerifyOtpDto } from './dto/verify-otp.dto';
import { Throttle, SkipThrottle} from '@nestjs/throttler';
import { ConfigService } from '@nestjs/config';
import { v4 as uuidv4 } from 'uuid';
import { VerifyJWTDto } from './dto/verify-jwt.dto';
// eslint-disable-next-line @typescript-eslint/no-var-requires
const CryptoJS = require('crypto-js');

Expand Down Expand Up @@ -381,4 +382,20 @@ export class ApiController {
}
return await this.apiService.loginWithUniqueId(user, authHeader);
}

@Post('jwt/verify')
@UsePipes(new ValidationPipe({transform: true}))
async jwtVerify(
@Body() body: VerifyJWTDto
): Promise<any> {
return await this.apiService.verifyJWT(body.token);
}

@Post('logout')
@UsePipes(new ValidationPipe({transform: true}))
async logout(
@Body() body: VerifyJWTDto
): Promise<any> {
return await this.apiService.logout(body.token);
}
}
119 changes: 116 additions & 3 deletions src/api/api.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ const CryptoJS = require('crypto-js');
const AES = require('crypto-js/aes');
import Flagsmith from 'flagsmith-nodejs';
import { LoginWithUniqueIdDto } from './dto/login.dto';
import { InjectRedis } from '@nestjs-modules/ioredis';
import Redis from 'ioredis';
const jwksClient = require('jwks-rsa');
import * as jwt from 'jsonwebtoken';

CryptoJS.lib.WordArray.words;

Expand All @@ -43,6 +47,7 @@ export class ApiService {
private readonly fusionAuthService: FusionauthService,
private readonly otpService: OtpService,
private readonly configResolverService: ConfigResolverService,
@InjectRedis() private readonly redis: Redis
) {}

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

if (verifyOTPResult.status === SMSResponseStatus.success) {
let response;
const {
statusFA,
userId,
Expand Down Expand Up @@ -595,19 +602,31 @@ export class ApiService {
id: registrationId,
},
],
data: {
loginId: loginDto.loginId,
fingerprint: loginDto?.fingerprint,
timestamp: loginDto?.timestamp,
otp
}
},
loginDto.applicationId,
authHeader,
);
return this.login(loginDto, authHeader);
response = await this.login(loginDto, authHeader);
} else {
// create a new user
const createUserPayload: UserRegistration = {
user: {
timezone: "Asia/Kolkata",
username: loginDto.loginId,
mobilePhone: loginDto.loginId,
password: loginDto.password
password: loginDto.password,
data: {
loginId: loginDto.loginId,
fingerprint: loginDto?.fingerprint,
timestamp: loginDto?.timestamp,
otp
}
},
registration: {
applicationId: loginDto.applicationId,
Expand All @@ -626,8 +645,17 @@ export class ApiService {
if (userId == null || user == null) {
throw new HttpException(err, HttpStatus.BAD_REQUEST);
}
return this.login(loginDto, authHeader);
response = await this.login(loginDto, authHeader);
}
let existingJWTS:any = await this.redis.get(response?.result?.data?.user?.user?.id);
if(existingJWTS) {
existingJWTS = JSON.parse(existingJWTS);
} else {
existingJWTS = []
}
existingJWTS.push(response?.result?.data?.user?.token);
await this.redis.set(response?.result?.data?.user?.user?.id, JSON.stringify(existingJWTS));
return response;
} else {
const response: SignupResponse = new SignupResponse().init(uuidv4());
response.responseCode = ResponseCode.FAILURE;
Expand Down Expand Up @@ -706,4 +734,89 @@ export class ApiService {
}
return registration;
}

async verifyFusionAuthJWT(token: string): Promise<any> {
let client = jwksClient({
jwksUri: this.configService.get("JWKS_URI"),
requestHeaders: {}, // Optional
timeout: 30000, // Defaults to 30s
});

let getKey = (header: jwt.JwtHeader, callback: any) => {
client.getSigningKey(header.kid, (err, key: any) => {
if (err) {
console.error(`Error fetching signing key: ${err}`);
callback(err);
} else {
const signingKey = key.publicKey || key.rsaPublicKey;
callback(null, signingKey);
}
});
};

return new Promise<any>((resolve, reject) => {
jwt.verify(token, getKey, async (err, decoded) => {
if (err) {
console.error('APP JWT verification error:', err);
resolve({
isValidFusionAuthToken: false,
claims: null
})
} else {
resolve({
isValidFusionAuthToken: true,
claims: decoded
})
}
});
});
}

async verifyJWT(token:string): Promise<any> {
const { isValidFusionAuthToken, claims} = await this.verifyFusionAuthJWT(token);

let existingUserJWTS:any = JSON.parse(await this.redis.get(claims.sub));

if(!isValidFusionAuthToken){
if(existingUserJWTS.indexOf(token)!=-1){
existingUserJWTS.splice(existingUserJWTS.indexOf(token), 1);
await this.redis.set(claims.sub, JSON.stringify(existingUserJWTS));
}
return {
"isValid": false,
"message": "Invalid/Expired token."
}
}

if(existingUserJWTS.indexOf(token)==-1){
return {
"isValid": false,
"message": "Token is not authorized."
}
}

return {
"isValid": true,
"message": "Token is valid."
}
}

async logout(token:string): Promise<any> {
const { isValidFusionAuthToken, claims} = await this.verifyFusionAuthJWT(token);
if(isValidFusionAuthToken){
let existingUserJWTS:any = JSON.parse(await this.redis.get(claims.sub));
if(existingUserJWTS.indexOf(token)!=-1){
existingUserJWTS.splice(existingUserJWTS.indexOf(token), 1);
await this.redis.set(claims.sub, JSON.stringify(existingUserJWTS));
}
return {
"message": "Logout successful. Token invalidated."
}
} else {
return {
"message": "Invalid or expired token."
}
}
}

}
10 changes: 10 additions & 0 deletions src/api/dto/verify-jwt.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import {
IsNotEmpty, IsString,
} from 'class-validator';

export class VerifyJWTDto {
@IsString()
@IsNotEmpty()
token: string;
}

5 changes: 5 additions & 0 deletions src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { AuthModule } from './auth/auth.module';
import { ApiModule } from './api/api.module';
import got from 'got/dist/source';
import { TerminusModule } from '@nestjs/terminus';
import { RedisModule } from '@nestjs-modules/ioredis';

const gupshupFactory = {
provide: 'GupshupService',
Expand Down Expand Up @@ -47,6 +48,10 @@ const otpServiceFactory = {
ttl: parseInt(process.env.RATE_LIMIT_TTL), //Seconds
limit: parseInt(process.env.RATE_LIMIT), //Number of requests per TTL from a single IP
}),
RedisModule.forRoot({
type: 'single',
url: process.env.REDIS_URL,
}),
AdminModule,
DstModule,
AuthModule,
Expand Down
2 changes: 2 additions & 0 deletions src/user/dto/login.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,6 @@ export class LoginDto {
password: string;
applicationId: UUID;
roles?: Array<string>;
fingerprint?: string;
timestamp?: string;
}

0 comments on commit dda9aee

Please sign in to comment.