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'; @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}/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 { 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; } async introspectToken(token: string): Promise { const params = new URLSearchParams(); params.append('token', token); params.append('client_id', this.CLIENT_ID); params.append('client_secret', this.CLIENT_SECRET); const response = await axios.post(this.TOKENS_INTROSPECT_URL, params, { headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, }); return response.data; } async decodeToken(token: string): Promise { const decodedHeader = jwt.decode(token, { complete: true }); if (!decodedHeader || typeof decodedHeader !== 'object') { throw new Error('Invalid token format'); } const kid: any = decodedHeader.header.kid; const publicKey = await this.getPublicKey(kid); const introspection = await this.introspectToken(token); console.log("introspection : ", introspection.active, " : so Unauthorized!"); if (!introspection.active) { throw new Error('Unauthorized'); } const verified = jwt.verify(token, publicKey, { algorithms: ['RS256'] }); 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 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 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); throw new BadRequestException('Invalid username or password'); } // } // else if (accessToken && refreshToken) { // return { access_token: accessToken, refresh_token: refreshToken, email: username } // } } async getUserIdByEmail(email: string) { let url = `${this.USERS_URL}?email=${email}`; let adminAccessToken = await this.getAdminAccessToken(); 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(error.message); } } 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(); adminParams.append('grant_type', 'client_credentials'); adminParams.append('client_id', this.CLIENT_ID); adminParams.append('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 adminAccessToken = adminTokenResponse.data.access_token; return adminAccessToken; } catch (error) { console.log(error.message); throw error } } 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) { 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); try { const response = await axios.post(this.TOKEN_URL, params, { headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, }); return response.data; } catch (error) { console.log(error.message); throw new UnauthorizedException('Authentication failed'); } } }