diff --git a/src/auth/auth.controller.ts b/src/auth/auth.controller.ts index 3f7a1ea..1a30934 100644 --- a/src/auth/auth.controller.ts +++ b/src/auth/auth.controller.ts @@ -10,12 +10,6 @@ import { LogoutInterceptor } from 'src/interceptors/logout.interceptor'; export class AuthController { constructor(private readonly authService: AuthService) { } - - // @Post('/login') - // login(@Body() body: AuthLoginDTO) { - // return this.authService.login(body); - // } - @Post('login') @HttpCode(200) async loginClient(@Body() body: AuthLoginDTO, @Res({ passthrough: true }) res: Response, @Req() req: Request) { diff --git a/src/auth/auth.service.ts b/src/auth/auth.service.ts index a7e9161..650a92a 100644 --- a/src/auth/auth.service.ts +++ b/src/auth/auth.service.ts @@ -11,6 +11,15 @@ import { UnauthorizedException } from 'src/exceptions/unauthorized.exception'; import { ConflictException } from 'src/exceptions/conflict.exception'; import { BadRequestException } from 'src/exceptions/badRequest.exception'; +interface DecodedToken { + email: string; + [key: string]: any; +} + +interface IntrospectResult { + active: boolean; +} + @Injectable() export class AuthService { private keyCache: Record = {}; @@ -121,19 +130,44 @@ export class AuthService { } private async getPublicKey(kid: string): Promise { - if (this.keyCache[kid]) return this.keyCache[kid]; - const { data } = await axios.get(this.JWKS_URL); - const key = data.keys.find((k) => k.kid === kid); - if (!key) throw new InternalServerErrorException('Key not found'); - const pem = jwkToPem(key); - this.keyCache[kid] = pem; - return pem; + try { + // Check if the key is already cached + if (this.keyCache[kid]) return this.keyCache[kid]; + + // Fetch the JWKS (JSON Web Key Set) + const { data } = await axios.get(this.JWKS_URL); + + // Validate the response shape + if (!data?.keys || !Array.isArray(data.keys)) { + console.log("Invalid JWKS response"); + throw new UnauthorizedException('Authentication failed'); + } + + // Find the key with the matching kid + const key = data.keys.find((k) => k.kid === kid); + if (!key) { + console.log("Authentication failed: Key not found"); + throw new UnauthorizedException('Authentication failed'); + } + + // Convert JWK to PEM format and cache it + const pem = jwkToPem(key); + this.keyCache[kid] = pem; + + return pem; + } catch (error) { + if (error instanceof UnauthorizedException) { + throw error; // Let UnauthorizedException bubble up as-is + } + // Optionally log error details for debugging + console.error('Failed to retrieve or process JWKS:', error); + // Wrap other errors as InternalServerException + console.log('Failed to retrieve public key'); + throw new InternalServerErrorException(); + } } - async introspectToken(token: string): Promise { - - console.log("from AuthService introspectToken .......................... end"); - + async introspectTokenX(token: string): Promise { let det: any = await jwt.decode(token); const { email } = det; @@ -170,68 +204,98 @@ export class AuthService { } } - async decodeToken(token: string): Promise { - console.log("from AuthService decodeToken .......................... start"); + async introspectToken(token: string, publicKey: string): Promise { + try { + // Securely verify the token + const decoded = jwt.verify(token, publicKey, { algorithms: ['RS256'], }) as DecodedToken; + const { email } = decoded; + + if (!email) throw new UnauthorizedException('Token does not contain an email.'); + + // const uid = await this.getUserIdByEmail(email); + + // if (!uid) throw new UnauthorizedException('No user ID found for email.'); + + // const userInfoUrl = `${this.KEYCLOAK_URL}/admin/realms/${this.KEYCLOAK_REALM}/users/${uid}/sessions`; + + // const adminToken = await this.getAdminAccessToken(); + + // const response = await axios.get(userInfoUrl, { + // headers: { + // Authorization: `Bearer ${adminToken}`, + // }, + // }); + + const sessionData = await this.getUserSession(email); + + return { active: Array.isArray(sessionData) && sessionData.length > 0 }; + + } catch (error: any) { + console.log("Error in introspectToken:", error.message || error); + + if (error instanceof UnauthorizedException || error instanceof jwt.TokenExpiredError || error instanceof jwt.JsonWebTokenError || error instanceof jwt.NotBeforeError) { + throw new UnauthorizedException('Authentication failed'); + } + + throw new InternalServerErrorException('Failed to introspect token'); + } + } + + async getUserSession(email) { + try { + const uid = await this.getUserIdByEmail(email); + + if (!uid) throw new UnauthorizedException('No user ID found for email.'); + + const userInfoUrl = `${this.KEYCLOAK_URL}/admin/realms/${this.KEYCLOAK_REALM}/users/${uid}/sessions`; + + const adminToken = await this.getAdminAccessToken(); + + const response = await axios.get(userInfoUrl, { + headers: { + Authorization: `Bearer ${adminToken}`, + }, + }); + + const sessionData = response.data; + + return sessionData + + } catch (error) { + if (error instanceof UnauthorizedException) throw error + console.log("Error while getting User session....."); + throw new InternalServerErrorException() + } + } + + async decodeToken(token: string): Promise { const decodedHeader = jwt.decode(token, { complete: true }); if (!decodedHeader || typeof decodedHeader !== 'object') { - throw new Error('Invalid token format'); + throw new UnauthorizedException("Authentication failed") } - console.log("✅ token decoded from decodeToken"); - - const kid: any = decodedHeader.header.kid; + const publicKey = await this.getPublicKey(kid); - console.log("✅ publicKey got from decodeToken"); - - const introspection = await this.introspectToken(token); - - console.log("✅ introspection got from decodeToken"); + const introspection = await this.introspectToken(token, publicKey); if (!introspection.active) { - console.log("❌ introspection active from decodeToken"); - console.log("from AuthService decodeToken .......................... end"); + console.log("Introspect failed for token"); throw new UnauthorizedException("Authentication Failed") } - console.log("✅ introspection active from decodeToken"); - const verified = jwt.verify(token, publicKey, { algorithms: ['RS256'] }); - console.log("✅ token verification successufull from decodeToken"); - - console.log("from AuthService decodeToken .......................... end"); - return verified; } - async getAdminAccessTokenx(): Promise { - const data = new URLSearchParams(); - data.append('grant_type', 'client_credentials'); - data.append('client_id', this.CLIENT_ID); - data.append('client_secret', this.CLIENT_SECRET); - try { - const response: AxiosResponse = await axios.post(this.KEYCLOAK_URL, data, { - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - }, - }); - return response.data.access_token; - } catch (error) { - console.error('Error getting Keycloak access token:', error.response ? error.response.data : error.message); - throw new Error('Failed to obtain Keycloak access token'); - } - } - async loginUser(username: string, password: string, req: Request): Promise { - const accessToken = req.cookies['access_token']; - const refreshToken = req.cookies['refresh_token']; - - // if (!accessToken && !refreshToken) { + const access_token = req.cookies?.access_token; + const refresh_token = req.cookies?.refresh_token; const params = new URLSearchParams(); params.append('grant_type', 'password'); @@ -242,27 +306,41 @@ export class AuthService { // params.append('scope', 'openid'); try { + const userSession = await this.getUserSession(username) + + if (access_token && refresh_token && userSession.length > 0) { + let tokens = await this.getTokenFromRefreshToken(refresh_token); + const decodedHeader = jwt.decode(access_token, { complete: true }); + + if (!decodedHeader || typeof decodedHeader !== 'object') { + throw new UnauthorizedException("Authentication failed") + } + + const kid: any = decodedHeader.header.kid; + + const publicKey = await this.getPublicKey(kid); + + const decoded = jwt.verify(access_token, publicKey, { algorithms: ['RS256'] }); + + const email = (decoded && typeof decoded === 'object') ? (decoded as any).email : undefined; + return { ...tokens, email } + } + const response = await axios.post(this.TOKEN_URL, params, { headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, }); - console.log('first time data ...'); let k = { ...response.data, email: username }; return k; } catch (error) { - console.log(error.message); + console.log("error while logging : ", error.message); throw new BadRequestException('Invalid username or password'); } - // } - - // else if (accessToken && refreshToken) { - // return { access_token: accessToken, refresh_token: refreshToken, email: username } - // } } - async getUserIdByEmail(email: string) { + async getUserIdByEmailX(email: string) { let url = `${this.USERS_URL}?email=${email}`; @@ -289,6 +367,43 @@ export class AuthService { } } + async getUserIdByEmail(email: string): Promise { + try { + const adminAccessToken = await this.getAdminAccessToken(); + const url = `${this.USERS_URL}?email=${encodeURIComponent(email)}`; + const options: AxiosRequestConfig = { + method: 'GET', + url, + headers: { + Authorization: `Bearer ${adminAccessToken}`, + 'Content-Type': 'application/json', + }, + }; + + const response = await axios.request(options); + + if (response.status !== 200 || !Array.isArray(response.data)) { + console.log("failed to retrieve user-id...."); + throw new UnauthorizedException("Authentication failed") + } + + const user = response.data[0]; + if (user?.id) { + return user.id + } else { + throw new UnauthorizedException("Authentication failed") + } + + } catch (error) { + console.log("Error in getUserIdByEmail ..........."); + if (error instanceof UnauthorizedException) { + throw error + } + + throw new InternalServerErrorException(); + } + } + async forgotPassword() { const validatePasswordURLSearchParams = new URLSearchParams({ @@ -341,24 +456,28 @@ export class AuthService { async getAdminAccessToken() { try { - const adminParams = new URLSearchParams(); - adminParams.append('grant_type', 'client_credentials'); - adminParams.append('client_id', this.CLIENT_ID); - adminParams.append('client_secret', this.CLIENT_SECRET); + const adminParams = new URLSearchParams({ + grant_type: 'client_credentials', + client_id: this.CLIENT_ID, + client_secret: this.CLIENT_SECRET, + }); - const adminTokenResponse = await axios.post(this.TOKEN_URL, - adminParams, - { - headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, - }); - // console.log(adminTokenResponse.data); + const { data } = await axios.post(this.TOKEN_URL, adminParams, { + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + timeout: 6000, // optional: add a timeout to avoid hanging requests + }); - const adminAccessToken = adminTokenResponse.data.access_token; + if (!data.access_token) { + console.log("failed to retrieve admin access token....."); + throw new UnauthorizedException("Authentication failed") + } + + return data.access_token; - return adminAccessToken; } catch (error) { - console.log(error.message); - throw error + if (error instanceof UnauthorizedException) throw error + console.log("Error in getAdminAccessToken"); + throw new InternalServerErrorException(); } } @@ -546,16 +665,13 @@ export class AuthService { } - console.log("refesh token present : ", refreshToken); - - const params = new URLSearchParams(); params.append('grant_type', 'refresh_token'); params.append('client_id', this.CLIENT_ID); params.append('client_secret', this.CLIENT_SECRET); params.append('refresh_token', refreshToken); - console.log("url for refresh is : ", `${this.KEYCLOAK_URL}/auth/realms/${this.KEYCLOAK_REALM}/protocol/openid-connect/token`); + // console.log("url for refresh is : ", `${this.KEYCLOAK_URL}/auth/realms/${this.KEYCLOAK_REALM}/protocol/openid-connect/token`); try { const response = await axios.post(this.TOKEN_URL, params, { diff --git a/src/guards/jwt-auth.guard.ts b/src/guards/jwt-auth.guard.ts index b47bfbc..794ca52 100644 --- a/src/guards/jwt-auth.guard.ts +++ b/src/guards/jwt-auth.guard.ts @@ -8,7 +8,7 @@ import { import { AuthService } from 'src/auth/auth.service'; import * as jwt from 'jsonwebtoken'; import { InternalServerException } from 'src/exceptions/internalServerError.exception'; -import { UnauthorizedException as UE} from 'src/exceptions/unauthorized.exception'; +import { UnauthorizedException as UE } from 'src/exceptions/unauthorized.exception'; @Global() @Injectable() @@ -20,30 +20,35 @@ export class JwtAuthGuard implements CanActivate { const token = request.cookies?.access_token; if (!token) { - console.log('❌ No access token found'); + console.log("No token found from Request"); throw new UnauthorizedException('Authentication failed'); } - console.log("✅ access_token is present in JwtAuthGuard"); + // Decode the token without verifying the signature + const decoded = jwt.decode(token) as { exp?: number }; + if (!decoded || !decoded.exp) { + throw new UnauthorizedException('Authentication failed'); + } + + const currentTime = Math.floor(Date.now() / 1000); // in seconds + + if (decoded.exp < currentTime) { + console.log('Token expired'); + throw new UnauthorizedException('Invalid Token'); + } + + // Token is valid and not expired - continue with your normal logic try { - const decoded = await this.authService.decodeToken(token); - request.user = decoded; + const verifiedUser = await this.authService.decodeToken(token); + request.user = verifiedUser; return true; } catch (error) { - if ( - error instanceof jwt.TokenExpiredError || - error instanceof jwt.JsonWebTokenError - ) { - console.log('❌ Token is invalid or expired'); - throw new UnauthorizedException('Invalid Token'); - } - else if (error instanceof UnauthorizedException || error instanceof UE) { + if (error instanceof UnauthorizedException || error instanceof UE) { throw error; } - console.log(error instanceof UE); - throw new InternalServerException(); } } + }