Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added feature to store JWT and validate them. #101

Merged
merged 2 commits into from
Nov 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);
}
}
123 changes: 119 additions & 4 deletions src/api/api.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,19 +31,44 @@ 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;

@Injectable()
export class ApiService {
private client: any
private getKey: any;
encodedBase64Key;
parsedBase64Key;
constructor(
private configService: ConfigService,
private readonly fusionAuthService: FusionauthService,
private readonly otpService: OtpService,
private readonly configResolverService: ConfigResolverService,
) {}
@InjectRedis() private readonly redis: Redis
) {
this.client = jwksClient({
jwksUri: this.configService.get("JWKS_URI"),
requestHeaders: {}, // Optional
timeout: 30000, // Defaults to 30s
});

this.getKey = (header: jwt.JwtHeader, callback: any) => {
this.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);
}
});
};
}

login(user: any, authHeader: string): Promise<SignupResponse> {
return this.fusionAuthService
Expand Down Expand Up @@ -535,6 +560,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 +588,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 +622,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 +665,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 +754,71 @@ export class ApiService {
}
return registration;
}

async verifyFusionAuthJWT(token: string): Promise<any> {
return new Promise<any>((resolve, reject) => {
jwt.verify(token, this.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;
}
Loading