diff --git a/package-lock.json b/package-lock.json index a4c4fef..d471d79 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,6 +21,7 @@ "axios": "^1.10.0", "class-transformer": "^0.5.1", "class-validator": "^0.14.1", + "cookie-parser": "^1.4.7", "jsonwebtoken": "^9.0.2", "jwk-to-pem": "^2.0.7", "mssql": "^10.0.4", @@ -42,6 +43,7 @@ "@nestjs/testing": "^11.0.1", "@swc/cli": "^0.6.0", "@swc/core": "^1.10.7", + "@types/cookie-parser": "^1.4.9", "@types/express": "^5.0.0", "@types/jest": "^29.5.14", "@types/jwk-to-pem": "^2.0.3", @@ -3798,6 +3800,16 @@ "@types/node": "*" } }, + "node_modules/@types/cookie-parser": { + "version": "1.4.9", + "resolved": "https://registry.npmjs.org/@types/cookie-parser/-/cookie-parser-1.4.9.tgz", + "integrity": "sha512-tGZiZ2Gtc4m3wIdLkZ8mkj1T6CEHb35+VApbL2T14Dew8HA7c+04dmKqsKRNC+8RJPm16JEK0tFSwdZqubfc4g==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/express": "*" + } + }, "node_modules/@types/cookiejar": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.5.tgz", @@ -6205,6 +6217,34 @@ "node": ">= 0.6" } }, + "node_modules/cookie-parser": { + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.7.tgz", + "integrity": "sha512-nGUvgXnotP3BsjiLX2ypbQnWoGUPIIfHQNZkkC668ntrzGWEZVW70HDEB1qnNGMicPje6EttlIgzo51YSwNQGw==", + "license": "MIT", + "dependencies": { + "cookie": "0.7.2", + "cookie-signature": "1.0.6" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/cookie-parser/node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-parser/node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "license": "MIT" + }, "node_modules/cookie-signature": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", diff --git a/package.json b/package.json index d755185..6f214e7 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "axios": "^1.10.0", "class-transformer": "^0.5.1", "class-validator": "^0.14.1", + "cookie-parser": "^1.4.7", "jsonwebtoken": "^9.0.2", "jwk-to-pem": "^2.0.7", "mssql": "^10.0.4", @@ -53,6 +54,7 @@ "@nestjs/testing": "^11.0.1", "@swc/cli": "^0.6.0", "@swc/core": "^1.10.7", + "@types/cookie-parser": "^1.4.9", "@types/express": "^5.0.0", "@types/jest": "^29.5.14", "@types/jwk-to-pem": "^2.0.3", diff --git a/src/auth/auth.controller.ts b/src/auth/auth.controller.ts index bdf47ba..7c5d0df 100644 --- a/src/auth/auth.controller.ts +++ b/src/auth/auth.controller.ts @@ -90,4 +90,11 @@ export class AuthController { return { statusCode: 200, message: "Tokens refreshed successfully" } } + + // @Get('checkRoles') + // @UseGuards(JwtAuthGuard, RolesGuard) + // @Roles('ca') + roleChecking() { + return { message: "role checking successfull..." } + } } diff --git a/src/auth/auth.service.ts b/src/auth/auth.service.ts index 7bbb8bc..2d98270 100644 --- a/src/auth/auth.service.ts +++ b/src/auth/auth.service.ts @@ -14,10 +14,46 @@ import { ConflictException } from 'src/exceptions/conflict.exception'; 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; @@ -87,32 +123,23 @@ export class AuthService { private async getPublicKey(kid: string): Promise { if (this.keyCache[kid]) return this.keyCache[kid]; - - const jwksUri = `${this.configService.get('KEYCLOAK_URL')}/realms/${this.configService.get( - 'REALM', - )}/protocol/openid-connect/certs`; - - const { data } = await axios.get(jwksUri); + 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 url = `${this.configService.get('KEYCLOAK_URL')}/realms/${this.configService.get( - 'REALM', - )}/protocol/openid-connect/token/introspect`; const params = new URLSearchParams(); params.append('token', token); - params.append('client_id', this.configService.get('CLIENT_ID') || ''); - params.append('client_secret', this.configService.get('CLIENT_SECRET') || ''); + params.append('client_id', this.CLIENT_ID); + params.append('client_secret', this.CLIENT_SECRET); - const response = await axios.post(url, params, { + const response = await axios.post(this.TOKENS_INTROSPECT_URL, params, { headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, }); @@ -130,8 +157,6 @@ export class AuthService { const introspection = await this.introspectToken(token); - console.log('introspec : ', introspection); - if (!introspection.active) { throw new Error('Unauthorized'); } @@ -143,11 +168,10 @@ export class AuthService { async getAdminAccessTokenx(): Promise { const data = new URLSearchParams(); data.append('grant_type', 'client_credentials'); - data.append('client_id', this.configService.get('CLIENT_ID') || ''); - data.append('client_secret', this.configService.get('CLIENT_SECRET') || ''); - let KEYCLOAK_URL = await this.configService.get('KEYCLOAK_URL'); + data.append('client_id', this.CLIENT_ID); + data.append('client_secret', this.CLIENT_SECRET); try { - const response: AxiosResponse = await axios.post(KEYCLOAK_URL, data, { + const response: AxiosResponse = await axios.post(this.KEYCLOAK_URL, data, { headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, @@ -161,39 +185,27 @@ export class AuthService { async loginUser(username: string, password: string, req: Request): Promise { - console.log(username, password); - - const accessToken = req.cookies['access_token']; const refreshToken = req.cookies['refresh_token']; - console.log("---------cookies--------------"); - console.log(accessToken, refreshToken); - - // return {accessToken, refreshToken, expires_in:2000} - if (!accessToken && !refreshToken) { - console.log("I am here getting token ...."); - - const url = `${this.configService.get('KEYCLOAK_URL')}/realms/${this.configService.get('REALM')}/protocol/openid-connect/token`; - const params = new URLSearchParams(); params.append('grant_type', 'password'); - params.append('client_id', this.configService.get('CLIENT_ID') || ''); - params.append('client_secret', this.configService.get('CLIENT_SECRET') || ''); + 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(url, params, { + 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; // tokens and user info + return k; } catch (error) { console.log(error.message); @@ -206,9 +218,9 @@ export class AuthService { } } - async getUserIdByEmail() { + async getUserIdByEmail(email: string) { - let url = `${this.configService.get('KEYCLOAK_URL')}/admin/realms/${this.configService.get('REALM')}/users?email=a@gmail.com`; + let url = `${this.USERS_URL}?email=${email}`; let adminAccessToken = await this.getAdminAccessToken(); @@ -231,37 +243,21 @@ export class AuthService { } async forgotPassword() { - let urlx = `${this.configService.get('KEYCLOAK_URL')}/admin/realms/${this.configService.get('REALM')}/users`; - let urlxx = `${this.configService.get('KEYCLOAK_URL')}/admin/realms/${this.configService.get('REALM')}/users?email=${encodeURIComponent('a@gmail.com')}`; - let url = `${this.configService.get('KEYCLOAK_URL')}/admin/realms/${this.configService.get('REALM')}/users?email=a@gmail.com`; - - let validatePasswordURL = `${this.configService.get('KEYCLOAK_URL')}/realms/${this.configService.get('REALM')}/protocol/openid-connect/token` const validatePasswordURLSearchParams = new URLSearchParams({ grant_type: 'password', - client_id: `${this.configService.get('CLIENT_ID')}`, - client_secret: `${this.configService.get('CLIENT_SECRET')}`, + client_id: `${this.CLIENT_ID}`, + client_secret: `${this.CLIENT_SECRET}`, username: `${'a@gmail.com'}`, password: `${'A1!bcdef'}`, }); - let userID = await this.getUserIdByEmail(); + let userID = await this.getUserIdByEmail(""); - console.log("userID: ", userID); - - let resetPasswordURL = `${this.configService.get('KEYCLOAK_URL')}/admin/realms/${this.configService.get('REALM')}/users/${userID}/reset-password`; + let resetPasswordURL = `${this.USERS_URL}/${userID}/reset-password`; let adminAccessToken = await this.getAdminAccessToken(); - const options: AxiosRequestConfig = { - method: 'GET', - url, - headers: { - Authorization: `Bearer ${adminAccessToken}`, - "Content-Type": "application/json" - } - }; - const options1: AxiosRequestConfig = { method: 'PUT', url: resetPasswordURL, @@ -277,7 +273,7 @@ export class AuthService { const options2: AxiosRequestConfig = { method: 'POST', - url: validatePasswordURL, + url: this.TOKEN_URL, headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, @@ -298,16 +294,12 @@ export class AuthService { async getAdminAccessToken() { try { - const adminTokenUrl = `${this.configService.get('KEYCLOAK_URL')}/realms/${this.configService.get('REALM')}/protocol/openid-connect/token`; - - console.log(adminTokenUrl); - const adminParams = new URLSearchParams(); adminParams.append('grant_type', 'client_credentials'); - adminParams.append('client_id', this.configService.get('CLIENT_ID') || ''); - adminParams.append('client_secret', this.configService.get('CLIENT_SECRET') || ''); + adminParams.append('client_id', this.CLIENT_ID); + adminParams.append('client_secret', this.CLIENT_SECRET); - const adminTokenResponse = await axios.post(adminTokenUrl, + const adminTokenResponse = await axios.post(this.TOKEN_URL, adminParams, { headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, @@ -324,35 +316,10 @@ export class AuthService { } async registerUser(body: AuthLoginDTO, req: Request) { - // First, get an admin access token using client credentials let det = await req['user']; - if (det?.email !== body.P_EMAILADDR) { throw new BadRequestException(); } - - const adminTokenUrl = `${this.configService.get('KEYCLOAK_URL')}/realms/${this.configService.get('REALM')}/protocol/openid-connect/token`; - - console.log(adminTokenUrl); - - const adminParams = new URLSearchParams(); - adminParams.append('grant_type', 'client_credentials'); - adminParams.append('client_id', this.configService.get('CLIENT_ID') || ''); - adminParams.append('client_secret', this.configService.get('CLIENT_SECRET') || ''); - - const adminTokenResponse = await axios.post(adminTokenUrl, - adminParams, - { - headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, - }); - console.log(adminTokenResponse.data); - - // const adminAccessToken = adminTokenResponse.data.access_token; - const adminAccessToken = adminTokenResponse.data.access_token; - - // Now create user - const createUserUrl = `${this.configService.get('KEYCLOAK_URL')}/admin/realms/${this.configService.get('REALM')}/users`; - const userPayload = { username: body.P_EMAILADDR, // firstName:"A", @@ -371,9 +338,10 @@ export class AuthService { console.log(userPayload); + const adminAccessToken = await this.getAdminAccessToken(); try { - const response = await axios.post(createUserUrl, userPayload, { + const response = await axios.post(this.USERS_URL, userPayload, { headers: { Authorization: `Bearer ${adminAccessToken}`, 'Content-Type': 'application/json', @@ -381,6 +349,13 @@ export class AuthService { }); 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' }; } @@ -397,11 +372,11 @@ export class AuthService { } async logoutUser(refreshToken: string): Promise { - const url = `${this.configService.get('KEYCLOAK_URL')}/realms/${this.configService.get('REALM')}/protocol/openid-connect/logout`; + const url = `${this.KEYCLOAK_BASE_URL}/logout`; const params = new URLSearchParams(); - params.append('client_id', this.configService.get('CLIENT_ID') || ''); - params.append('client_secret', this.configService.get('CLIENT_SECRET') || ''); + params.append('client_id', this.CLIENT_ID); + params.append('client_secret', this.CLIENT_SECRET); params.append('refresh_token', refreshToken); try { @@ -414,6 +389,104 @@ export class AuthService { } } + 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']; @@ -421,35 +494,18 @@ export class AuthService { throw new UnauthorizedException('Authentication failed'); } - let dk = await this.introspectToken(refreshToken); - - console.log('--------------validation------------------------'); - console.log(dk); - - console.log('--------------validation------------------------'); - - const url = `${this.configService.get('KEYCLOAK_URL')}/realms/${this.configService.get('REALM')}/protocol/openid-connect/token`; - const params = new URLSearchParams(); params.append('grant_type', 'refresh_token'); - params.append('client_id', this.configService.get('CLIENT_ID') || ''); - params.append('client_secret', this.configService.get('CLIENT_SECRET') || ''); + 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, { + const response = await axios.post(this.TOKEN_URL, params, { headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, }); - console.log('refresh token response...'); - console.log(response.data); - - return response.data; // tokens and user info - - // console.log(response.data); - - // return { message: "tokens refreshed successfully...." } - + return response.data; } catch (error) { console.log(error.message); throw new UnauthorizedException('Authentication failed'); diff --git a/src/decorators/roles.decorator.ts b/src/decorators/roles.decorator.ts new file mode 100644 index 0000000..1fe6962 --- /dev/null +++ b/src/decorators/roles.decorator.ts @@ -0,0 +1,5 @@ +// roles.decorator.ts +import { SetMetadata } from '@nestjs/common'; + +export const ROLES_KEY = 'roles'; +export const Roles = (...roles: string[]) => SetMetadata(ROLES_KEY, roles); diff --git a/src/main.ts b/src/main.ts index fc84f5d..7cb0531 100644 --- a/src/main.ts +++ b/src/main.ts @@ -7,6 +7,7 @@ import { import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger'; import { SwaggerDocumentOptions } from '@nestjs/swagger'; import { BadRequestException } from './exceptions/badRequest.exception'; +import * as cookieParser from 'cookie-parser'; async function bootstrap() { const app = await NestFactory.create(AppModule, { @@ -20,23 +21,17 @@ async function bootstrap() { }, }); + app.use(cookieParser()); + function extractConstraints(validationErrors: ValidationError[]) { const constraints: { [key: string]: string }[] = []; function traverse(errors: ValidationError[]) { - // console.log(errors); - // console.log("--------------"); for (const error of errors) { - // If the error has constraints, add them to the list - - // console.log(error); - // console.log("--------------"); - if (error.constraints) { constraints.push(error.constraints); } - // // If there are children, recursively traverse them if (error.children && error.children.length > 0) { traverse(error.children); }