import { Injectable, InternalServerErrorException } from '@nestjs/common'; import { OracleDBService } from 'src/db/db.service'; import { AuthLoginDTO } from './auth.dto'; import * as oracledb from 'oracledb'; import { ConfigService } from '@nestjs/config'; import axios, { AxiosRequestConfig, AxiosResponse } from 'axios'; import { Request } from 'express'; import * as jwkToPem from "jwk-to-pem" import * as jwt from "jsonwebtoken" 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 = {}; private readonly KEYCLOAK_URL: string; private readonly KEYCLOAK_REALM: string; private readonly KEYCLOAK_BASE_URL: string; private readonly JWKS_URL: string; private readonly TOKENS_INTROSPECT_URL: string; private readonly CLIENT_ID: string; private readonly CLIENT_SECRET: string; private readonly TOKEN_URL: string; private readonly USERS_URL: string; constructor( private readonly oracleDBService: OracleDBService, private readonly configService: ConfigService, ) { const KEYCLOAK_URL = this.configService.get('KEYCLOAK_URL'); if (!KEYCLOAK_URL) throw new Error('Environment variable KEYCLOAK_URL is not set'); this.KEYCLOAK_URL = KEYCLOAK_URL; const KEYCLOAK_REALM = this.configService.get('KEYCLOAK_REALM'); if (!KEYCLOAK_REALM) throw new Error('Environment variable KEYCLOAK_REALM is not set'); this.KEYCLOAK_REALM = KEYCLOAK_REALM; const CLIENT_ID = this.configService.get('CLIENT_ID'); if (!CLIENT_ID) throw new Error('Environment variable KEYCLOAK CLIENT_ID is not set'); this.CLIENT_ID = CLIENT_ID; const CLIENT_SECRET = this.configService.get('CLIENT_SECRET'); if (!CLIENT_SECRET) throw new Error('Environment variable KEYCLOAK CLIENT_SECRET is not set'); this.CLIENT_SECRET = CLIENT_SECRET; this.KEYCLOAK_BASE_URL = `${this.KEYCLOAK_URL}/realms/${this.KEYCLOAK_REALM}/protocol/openid-connect`; this.JWKS_URL = `${this.KEYCLOAK_BASE_URL}/certs` this.TOKENS_INTROSPECT_URL = `${this.KEYCLOAK_BASE_URL}/token/introspect` this.TOKEN_URL = `${this.KEYCLOAK_BASE_URL}/token` this.USERS_URL = `${this.KEYCLOAK_URL}/admin/realms/${this.KEYCLOAK_REALM}/users`; } async login(body: AuthLoginDTO) { let connection; let rows = []; try { connection = await this.oracleDBService.getConnection(); if (!connection) { throw new Error('No DB Connected'); } const result = await connection.execute( `BEGIN USERLOGIN_PKG.ValidateUser(:P_EMAILADDR,:P_PASSWORD,:p_login_cursor); END;`, { P_EMAILADDR: { val: body.P_EMAILADDR, type: oracledb.DB_TYPE_NVARCHAR, }, P_PASSWORD: { val: body.P_PASSWORD, type: oracledb.DB_TYPE_NVARCHAR, }, p_login_cursor: { type: oracledb.CURSOR, dir: oracledb.BIND_OUT, }, }, { outFormat: oracledb.OUT_FORMAT_OBJECT, }, ); if (result.outBinds && result.outBinds.p_login_cursor) { const cursor = result.outBinds.p_login_cursor; let rowsBatch; do { rowsBatch = await cursor.getRows(100); rows = rows.concat(rowsBatch); } while (rowsBatch.length > 0); await cursor.close(); } else { throw new BadRequestException('Error executing request try after some time!'); } if (rows[0]['ERRORMESG']) { throw new BadRequestException('Invalid username or password!'); } return { msg: 'Logged in successfully' }; } catch (err) { throw new BadRequestException('Invalid username or password'); } finally { if (connection) { try { await connection.close(); } catch (closeErr) { console.error('Failed to close connection:', closeErr); } } } } private async getPublicKey(kid: string): Promise { 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 introspectTokenX(token: string): Promise { let det: any = await jwt.decode(token); const { email } = det; console.log("email : ", email); let uid = await this.getUserIdByEmail(email); console.log("uid : ", uid); const USERINFO_URL = `${this.KEYCLOAK_URL}/admin/realms/${this.KEYCLOAK_REALM}/users/${uid}/sessions`; const admintoken = await this.getAdminAccessToken(); try { console.log("url : ", USERINFO_URL); const response = await axios.get(USERINFO_URL, { headers: { Authorization: `Bearer ${admintoken}` } }); console.log("active session data : ", response.data); console.log("from AuthService decodeToken .......................... end"); if (response.data.length > 0) return { active: true }; else return { active: false } } catch (error) { console.log("Error in introspectToken : ", error.message); throw new InternalServerErrorException() } } 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 UnauthorizedException("Authentication failed") } const kid: any = decodedHeader.header.kid; const publicKey = await this.getPublicKey(kid); const introspection = await this.introspectToken(token, publicKey); if (!introspection.active) { console.log("Introspect failed for token"); throw new UnauthorizedException("Authentication Failed") } const verified = jwt.verify(token, publicKey, { algorithms: ['RS256'] }); return verified; } async loginUser(username: string, password: string, req: Request): Promise { const access_token = req.cookies?.access_token; const refresh_token = req.cookies?.refresh_token; const params = new URLSearchParams(); params.append('grant_type', 'password'); params.append('client_id', this.CLIENT_ID); params.append('client_secret', this.CLIENT_SECRET); params.append('username', username); params.append('password', password); // 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' }, }); let k = { ...response.data, email: username }; return k; } catch (error) { console.log("error while logging : ", error.message); throw new BadRequestException('Invalid username or password'); } } async getUserIdByEmailX(email: string) { let url = `${this.USERS_URL}?email=${email}`; let adminAccessToken = await this.getAdminAccessToken(); console.log("admin access_token for getting uid : ", adminAccessToken); console.log("url for uid : ", url); const options: AxiosRequestConfig = { method: 'GET', url, headers: { Authorization: `Bearer ${adminAccessToken}`, "Content-Type": "application/json" } }; try { const { data } = await axios.request(options); return data[0]?.id; } catch (error) { // console.error(error); console.log("from getUserIdByEmail : ", error.message); } } 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({ grant_type: 'password', client_id: `${this.CLIENT_ID}`, client_secret: `${this.CLIENT_SECRET}`, username: `${'a@gmail.com'}`, password: `${'A1!bcdef'}`, }); let userID = await this.getUserIdByEmail(""); let resetPasswordURL = `${this.USERS_URL}/${userID}/reset-password`; let adminAccessToken = await this.getAdminAccessToken(); const options1: AxiosRequestConfig = { method: 'PUT', url: resetPasswordURL, headers: { Authorization: `Bearer ${adminAccessToken}` }, data: { type: 'password', value: 'A1!bcdef', temporary: false, } }; const options2: AxiosRequestConfig = { method: 'POST', url: this.TOKEN_URL, headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, data: validatePasswordURLSearchParams }; try { const { data } = await axios.request(options2); return { statusCode: 200, message: 'password reset successfull' } } catch (error) { // console.error(error); console.log(error.message); throw new InternalServerErrorException() } } async getAdminAccessToken() { try { const adminParams = new URLSearchParams({ grant_type: 'client_credentials', client_id: this.CLIENT_ID, client_secret: this.CLIENT_SECRET, }); 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 }); if (!data.access_token) { console.log("failed to retrieve admin access token....."); throw new UnauthorizedException("Authentication failed") } return data.access_token; } catch (error) { if (error instanceof UnauthorizedException) throw error console.log("Error in getAdminAccessToken"); throw new InternalServerErrorException(); } } async registerUser(body: AuthLoginDTO, req: Request) { let det = await req['user']; if (det?.email !== body.P_EMAILADDR) { throw new BadRequestException(); } const userPayload = { username: body.P_EMAILADDR, // firstName:"A", // lastName:"B", email: body.P_EMAILADDR, emailVerified: true, enabled: true, credentials: [ { type: 'password', value: body.P_PASSWORD, temporary: false, }, ], }; console.log(userPayload); const adminAccessToken = await this.getAdminAccessToken(); try { const response = await axios.post(this.USERS_URL, userPayload, { headers: { Authorization: `Bearer ${adminAccessToken}`, 'Content-Type': 'application/json', }, }); if (response.status === 201) { const uid = await this.getUserIdByEmail(body.P_EMAILADDR); const res = await this.assignRoleToUser(uid, "ca"); console.log(res); return { message: 'User created successfully' }; } } catch (error) { // handle errors like user exists, validation errors, etc. console.log(error.message); if (error.message === "Request failed with status code 409") { throw new ConflictException("User already exist"); } throw new InternalServerErrorException('Failed to create user'); } } async logoutUser(refreshToken: string): Promise { const url = `${this.KEYCLOAK_BASE_URL}/logout`; const params = new URLSearchParams(); params.append('client_id', this.CLIENT_ID); params.append('client_secret', this.CLIENT_SECRET); params.append('refresh_token', refreshToken); try { const response = await axios.post(url, params, { headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, }); return { statusCode: 200, message: 'Logged-Out successfully' }; } catch (error) { throw new InternalServerErrorException('Logout failed'); } } async getClientUUID(): Promise { try { const token = await this.getAdminAccessToken(); const realm = this.KEYCLOAK_REALM; const clientId = this.CLIENT_ID; const keycloakUrl = this.KEYCLOAK_URL; const response = await axios.get( `${keycloakUrl}/admin/realms/${realm}/clients?clientId=${encodeURIComponent(clientId)}`, { headers: { Authorization: `Bearer ${token}`, }, validateStatus: () => true, // manually handle status codes } ); if (response.status !== 200) { throw new Error(`Failed to fetch client list. Status: ${response.status}. Message: ${JSON.stringify(response.data)}`); } const clients = response.data; if (!Array.isArray(clients) || clients.length === 0) { throw new Error(`Client with ID '${clientId}' not found in realm '${realm}'.`); } const clientUUID = clients[0].id; if (!clientUUID) { throw new Error(`Client UUID is missing in the response for clientId '${clientId}'.`); } return clientUUID; } catch (error) { console.error('❌ Error in getClientUUID:', error.message || error); throw error; // rethrow for upstream handling } } async getClientRole(roleName: string): Promise { const token = await this.getAdminAccessToken(); const clientUUID = await this.getClientUUID(); try { const response = await axios.get( `${this.KEYCLOAK_URL}/admin/realms/${this.KEYCLOAK_REALM}/clients/${clientUUID}/roles/${roleName}`, { headers: { Authorization: `Bearer ${token}`, }, }, ); return response.data; } catch (error) { console.error(`Error getting client role '${roleName}'`); console.error(error.response?.data || error.message); throw new InternalServerErrorException(`Could not find client role '${roleName}'`); } } async assignRoleToUser(userId: string, roleName: string): Promise { try { const token = await this.getAdminAccessToken(); const clientId = await this.getClientUUID(); const role = await this.getClientRole(roleName); const url = `${this.KEYCLOAK_URL}/admin/realms/${this.KEYCLOAK_REALM}/users/${userId}/role-mappings/clients/${clientId}`; const response = await axios.post( url, [role], { headers: { Authorization: `Bearer ${token}`, }, validateStatus: () => true, // To manually check status if needed } ); if (response.status >= 200 && response.status < 300) { console.log(`✅ Role "${roleName}" successfully assigned to user ${userId}.`); return { message: "Successfully assigned to user" } } else { throw new Error(`❌ Failed to assign role. Status: ${response.status}. Data: ${JSON.stringify(response.data)}`); } } catch (error) { console.error(`🔥 Error assigning role "${roleName}" to user ${userId}:`, error.message || error); throw error; // Rethrow to allow upstream handling } } async getTokenFromRefreshToken(req: Request) { const refreshToken = req.cookies['refresh_token']; if (!refreshToken) { console.log("refesh token not present"); throw new UnauthorizedException('Authentication failed'); } 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`); try { const response = await axios.post(this.TOKEN_URL, params, { headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, }); return response.data; } catch (error) { if (axios.isAxiosError(error) && error.response) { console.error('🔴 Axios error:', { status: error.response.status, data: error.response.data, }); } else { console.error('🔴 Unexpected error:', error.message); } console.log("Error while refreshing tokens : ", error.message); throw new UnauthorizedException('Authentication failed'); } } }