import { Inject, Injectable, InternalServerErrorException } from '@nestjs/common'; import { OracleDBService } from 'src/db/db.service'; import { AuthLoginDTO, MailTemplateDTO, MailTypeDTO, SendMailDTO } from './auth.dto'; import * as oracledb from 'oracledb'; import { ConfigService } from '@nestjs/config'; import axios, { AxiosError, AxiosRequestConfig } 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'; import { JwtService } from '@nestjs/jwt'; import { UserMaintenanceService } from 'src/oracle/user-maintenance/user-maintenance.service'; import { v4 as uuidv4 } from 'uuid'; import { MailService } from 'src/mail/mail.service'; 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, private readonly mailService: MailService, private readonly userMaintenanceService: UserMaintenanceService, @Inject("REGISTER_SIGN_JWT") readonly REGISTER_SIGN_JWT: JwtService, @Inject("REGISTER_VERIFY_JWT") readonly REGISTER_VERIFY_JWT: JwtService ) { 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)) { throw new UnauthorizedException('Authentication failed'); } // Find the key with the matching kid const key = data.keys.find((k) => k.kid === kid); if (!key) { 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 throw new InternalServerErrorException(); } } async introspectToken(token: string): Promise { try { const decoded = jwt.decode(token) as DecodedToken; const { email } = decoded; if (!email) throw new UnauthorizedException('Authentication failed'); const sessionData = await this.getUserSession(email); return { active: Array.isArray(sessionData) && sessionData.length > 0 }; } catch (error: any) { if (error instanceof UnauthorizedException) { throw error; } throw new InternalServerErrorException('Failed to introspect token'); } } async getUserSession(email: string) { try { const uid = await this.getUserIdByEmail(email); if (!uid) throw new UnauthorizedException('No user ID found for email.'); const userInfoUrl = `${this.USERS_URL}/${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 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); if (!introspection.active) { 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); try { const userSession = await this.getUserSession(username) const emailVerifyResponse: any = await this.userMaintenanceService.ValidateEmail({ P_EMAILADDR: username }) if (Array.isArray(emailVerifyResponse) && emailVerifyResponse[0].ERRORMESG) { throw new BadRequestException(); } let ROLE = ""; if (Array.isArray(emailVerifyResponse) && !emailVerifyResponse[0].ERRORMESG) { switch (emailVerifyResponse[0].SOURCE) { case "USCIB": ROLE = 'ua'; break; case "SP": ROLE = 'sa'; break; case "CLIENT": ROLE = 'ca'; break; } } // if (ROLE === 'ua' && req.headers.origin !== 'https://policy.alphaomegainfosys.com') { // throw new BadRequestException("Invalid username or password") // } // else if (ROLE === 'sa' && req.headers.origin !== 'https://sp.alphaomegainfosys.com') { // throw new BadRequestException("Invalid username or password") // } // else if (ROLE === 'ca' && req.headers.origin !== 'https://client.alphaomegainfosys.com') { // throw new BadRequestException("Invalid username or password") // } if (access_token && refresh_token && userSession.length > 0) { const getSub: any = jwt.decode(refresh_token, { complete: true }) const user: any = await this.getUserDetailsById(getSub?.payload.sub) if (user.email !== username) { throw new UnauthorizedException("Authentication failed") } let tokens = await this.getTokenFromRefreshToken(req); const decoded = jwt.decode(access_token, { complete: true }); const email: any = (decoded && typeof decoded === 'object') ? (decoded as any).payload?.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) { throw new BadRequestException('Invalid username or password'); } } 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)) { throw new UnauthorizedException("Authentication failed") } const user = response.data[0]; if (user?.id) { return user.id } else { throw new UnauthorizedException("Authentication failed") } } catch (error) { if (error instanceof UnauthorizedException) { throw error } throw new InternalServerErrorException(); } } async getUserDetailsById(id: string) { try { const adminAccessToken = await this.getAdminAccessToken(); const userResponse = await axios.get(`${this.USERS_URL}/${id}`, { headers: { Authorization: `Bearer ${adminAccessToken}` }, }); if (userResponse.status !== 200 || !Array.isArray(userResponse.data)) { throw new UnauthorizedException("Authentication failed"); } return userResponse.data; } catch (error) { if (error instanceof UnauthorizedException) { throw error; } else if (error instanceof BadRequestException) { throw error; } throw new InternalServerErrorException(); } } async getUserDetailsByEmail(email: string): Promise { try { const adminAccessToken = await this.getAdminAccessToken(); const userResponse = await axios.get(`${this.USERS_URL}?email=${encodeURIComponent(email)}`, { headers: { Authorization: `Bearer ${adminAccessToken}` }, }); if (userResponse.status !== 200 || !Array.isArray(userResponse.data)) { throw new UnauthorizedException("Authentication failed"); } const user = userResponse.data[0]; if (userResponse.data.length === 0 || !user?.id) { throw new BadRequestException("Invalid User"); } const roleUrl = `${this.USERS_URL}/${user.id}/role-mappings`; const roleResponse = await axios.get(roleUrl, { headers: { Authorization: `Bearer ${adminAccessToken}` }, }); const clientRoles = roleResponse.data; return { id: user.id, email: user.email, forgotPassword: user.attributes?.["forgot-password"][0] || null, role: clientRoles.clientMappings?.[this.CLIENT_ID]?.mappings?.map((x) => x.name) || [] }; } catch (error) { if (error instanceof UnauthorizedException) { throw error; } else if (error instanceof BadRequestException) { throw error; } throw new InternalServerErrorException(); } } async forgotPassword(body: AuthLoginDTO, req: Request) { let det = await req['user']; if (det?.email !== body.P_EMAILADDR) { throw new BadRequestException(); } const userDetails = await this.getUserDetailsByEmail(body.P_EMAILADDR); if (userDetails.forgotPassword !== det.rstID) { throw new UnauthorizedException("Token verification failed"); } let adminAccessToken = await this.getAdminAccessToken(); // console.log(`${this.USERS_URL}/${userDetails.id}/reset-password`); const options2: AxiosRequestConfig = { method: 'PUT', url: `${this.USERS_URL}/${userDetails.id}/reset-password`, headers: { Authorization: `Bearer ${adminAccessToken}`, 'Content-Type': 'application/json', }, data: { type: 'password', value: body.P_PASSWORD, temporary: false, } }; try { const { data } = await axios.request(options2); const forgotPasswordStatus = await this.updateForgotPassword(userDetails) if (!forgotPasswordStatus) { throw new InternalServerErrorException("Update forgot-password failed") } return { statusCode: 200, message: 'password reset successfull' } } catch (error) { // console.error(error); 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) { throw new UnauthorizedException("Authentication failed") } return data.access_token; } catch (error) { if (error instanceof UnauthorizedException) throw error throw new InternalServerErrorException(); } } async registerUser(body: AuthLoginDTO) { // let det = await req['user']; // if (det?.email !== body.P_EMAILADDR) { // throw new BadRequestException(); // } const emailVerifyResponse: any = await this.userMaintenanceService.ValidateEmail({ P_EMAILADDR: body.P_EMAILADDR }) if (Array.isArray(emailVerifyResponse) && emailVerifyResponse[0].ERRORMESG) { throw new BadRequestException(); } const userPayload = { username: body.P_EMAILADDR.toLowerCase(), // firstName:"A", // lastName:"B", email: body.P_EMAILADDR.toLowerCase(), emailVerified: true, enabled: true, attributes: { "forgot-password": ["none"] }, 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) { // console.log("Assigning role : "); // console.log("checking validate email response : ", response); const uid = await this.getUserIdByEmail(body.P_EMAILADDR); let ROLE = ""; if (Array.isArray(emailVerifyResponse) && !emailVerifyResponse[0].ERRORMESG) { switch (emailVerifyResponse[0].SOURCE) { case "USCIB": ROLE = 'ua'; break; case "SP": ROLE = 'sa'; break; case "CLIENT": ROLE = 'ca'; break; } } // console.log("Role to assign : ", ROLE); if (!ROLE) { throw new InternalServerErrorException(); } const res = await this.assignRoleToUser(uid, ROLE); return { statusCode: 201, message: 'User registered successfully' }; } } catch (error) { if (error.message === "Request failed with status code 409") { throw new ConflictException("User already exist"); } else if (error instanceof BadRequestException) { return error; } throw new InternalServerErrorException('Failed to create user'); } } async assignRoleToUser(userId: string, roleName: string): Promise<{ message: string }> { 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}`, 'Content-Type': 'application/json', }, validateStatus: () => true, // Let us handle non-2xx statuses manually }); if (response.status >= 200 && response.status < 300) { return { message: "Successfully assigned to user" }; } else { console.error(`❌ Failed to assign role. Status: ${response.status}, Data:`, response.data); throw new Error(`Failed to assign role "${roleName}" to user ${userId}. Status: ${response.status}`); } } catch (error) { // Handle Axios errors more gracefully if (axios.isAxiosError(error)) { const axiosError = error as AxiosError; console.error(`🔥 Axios error: ${axiosError.message}`); if (axiosError.response) { console.error(`Response status: ${axiosError.response.status}`); console.error(`Response data:`, axiosError.response.data); } } else { console.error(`🔥 Unexpected error:`, error); } throw new Error(`Error assigning role "${roleName}" to user ${userId}. See logs for details.`); } } 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 getTokenFromRefreshToken(req: Request) { const refreshToken = req.cookies['refresh_token']; if (!refreshToken) { 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); } throw new UnauthorizedException('Authentication failed'); } } async generateUUID() { try { const uuid = uuidv4(); if (!uuid || typeof uuid !== 'string') { throw new InternalServerErrorException('Invalid UUID generated'); } return uuid; } catch (error) { console.error('UUID generation failed:', error.message); throw new InternalServerErrorException('Failed to generate unique ID'); } } async generateToken(payload: { P_TO: string, uuid?: string }) { let token: string; try { if (payload.uuid) { token = this.REGISTER_SIGN_JWT.sign({ email: payload.P_TO, rstID: payload.uuid }); } else { token = this.REGISTER_SIGN_JWT.sign({ email: payload.P_TO }); } if (!token) { throw new InternalServerErrorException(); } return token; } catch (error) { throw new InternalServerErrorException("Register Token Generation failed") } } async updateForgotPassword(user: any, rstID?: string) { try { const adminAccessToken = await this.getAdminAccessToken(); const options2: AxiosRequestConfig = { method: 'PUT', url: `${this.USERS_URL}/${user.id}`, headers: { Authorization: `Bearer ${adminAccessToken}`, 'Content-Type': 'application/json', }, data: { "username": `${user.email}`, // firstName:"A", // lastName:"B", "email": `${user.email}`, "emailVerified": true, "enabled": true, "attributes": { "forgot-password": [`${rstID ? rstID : "none"}`] } } }; const userResponse = await axios.request(options2); if (userResponse.status !== 204) { throw new InternalServerErrorException(); } return true; } catch (error) { return false; } } async getMailTemplate(body: SendMailDTO, token: string, source: string): Promise { switch (body.P_MAIL_TYPE) { case MailTypeDTO.REGISTER_CLIENT: return { to: body.P_TO, subject: `🔐 Register Your Account`, templateName: 'a', variables: { to: body.P_TO, url: `https://client.alphaomegainfosys.com/register` } } case MailTypeDTO.FORGOT_PASSWORD: return { to: body.P_TO, subject: `Reset your password`, templateName: 'b', variables: { to: body.P_TO, url: `${source === 'ua' ? `https://policy.alphaomegainfosys.com/forgot-password/${token}` : source === 'sa' ? `https://sp.alphaomegainfosys.com/forgot-password/${token}` : source === 'ca' ? `https://client.alphaomegainfosys.com/forgot-password/${token}` : ''}` } } default: return null; } } async sendMail(body: SendMailDTO) { let token: string; let ROLE: string = ""; let uuid: string | undefined = undefined; let userDetails: any | null = null; if (body.P_MAIL_TYPE === MailTypeDTO.FORGOT_PASSWORD) { try { uuid = await this.generateUUID(); userDetails = await this.getUserDetailsByEmail(body.P_TO); ROLE = userDetails.role[0]; } catch (error) { if (error instanceof BadRequestException) { throw error } throw new InternalServerErrorException(); } } token = await this.generateToken({ P_TO: body.P_TO, uuid: uuid }) const mailTemplate: MailTemplateDTO | null = await this.getMailTemplate(body, token, ROLE); if (!mailTemplate) { throw new BadRequestException(); } try { await this.mailService.sendMail({ ...mailTemplate }); if (body.P_MAIL_TYPE === MailTypeDTO.FORGOT_PASSWORD) { let forgotPasswordStatus = await this.updateForgotPassword(userDetails, uuid); if (forgotPasswordStatus) { return { statusCode: 200, message: 'Email sent successfully' }; } throw new InternalServerErrorException(); } return { statusCode: 200, message: 'Email sent successfully' }; } catch (error) { console.error('Failed to send email:', error.message, error); throw new InternalServerErrorException(); } } }