From a381f50e886ddf65e171d4ccaacb75ca73481337 Mon Sep 17 00:00:00 2001 From: marcos-silva-rodrigues Date: Thu, 3 Jul 2025 15:52:55 -0300 Subject: [PATCH 1/8] FEAT: set cookie after user login --- docsfera.json | 211 ++++------------------------ package-lock.json | 34 +++++ package.json | 2 + src/main.ts | 4 + src/modules/auth/auth.controller.ts | 125 ++++++++++++++-- 5 files changed, 183 insertions(+), 193 deletions(-) diff --git a/docsfera.json b/docsfera.json index 96612ac..4749daf 100644 --- a/docsfera.json +++ b/docsfera.json @@ -34,14 +34,7 @@ }, "responses": { "200": { - "description": "", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/AuthSignInRes" - } - } - } + "description": "" } }, "tags": [ @@ -675,6 +668,33 @@ ] } }, + "/auth/me": { + "get": { + "operationId": "AuthController_getMe", + "parameters": [ + { + "name": "dadosfera-lang", + "in": "header", + "required": false, + "schema": { + "enum": [ + "pt-br", + "en-us" + ], + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "" + } + }, + "tags": [ + "Auth" + ] + } + }, "/connections": { "post": { "operationId": "ConnectionController_createConnection", @@ -6476,181 +6496,6 @@ "password" ] }, - "AuthCustomer": { - "type": "object", - "properties": { - "modules": { - "type": "array", - "items": { - "type": "string" - } - }, - "id": { - "type": "string" - }, - "name": { - "type": "string" - }, - "displayName": { - "type": "string" - }, - "tier": { - "type": "string" - }, - "scheduleLimit": { - "type": "string" - }, - "links": { - "type": "array", - "items": { - "type": "string" - } - }, - "themeEnabled": { - "type": "boolean" - }, - "enforceMfa": { - "type": "boolean" - } - }, - "required": [ - "modules", - "id", - "name", - "displayName", - "tier", - "scheduleLimit", - "links", - "themeEnabled", - "enforceMfa" - ] - }, - "AuthUser": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "name": { - "type": "string" - }, - "username": { - "type": "string" - }, - "createdAt": { - "type": "string" - } - }, - "required": [ - "id", - "name", - "username", - "createdAt" - ] - }, - "AuthTokens": { - "type": "object", - "properties": { - "accessToken": { - "type": "string" - }, - "refreshToken": { - "type": "string" - }, - "termsOfUseToken": { - "type": "string" - }, - "idToken": { - "type": "string", - "deprecated": true - } - }, - "required": [ - "accessToken", - "refreshToken", - "idToken" - ] - }, - "TermsOfUse": { - "type": "object", - "properties": { - "version": { - "type": "number" - }, - "publicUrl": { - "type": "string" - }, - "enforceDate": { - "type": "string" - }, - "createdAt": { - "type": "string" - } - }, - "required": [ - "version", - "publicUrl", - "enforceDate", - "createdAt" - ] - }, - "TermsOfUseStatus": { - "type": "object", - "properties": { - "status": { - "type": "string", - "enum": [ - "pending", - "required", - "ok" - ] - }, - "lastSigned": { - "$ref": "#/components/schemas/TermsOfUse" - }, - "next": { - "$ref": "#/components/schemas/TermsOfUse" - } - }, - "required": [ - "status" - ] - }, - "AuthSignInRes": { - "type": "object", - "properties": { - "permissions": { - "type": "array", - "items": { - "type": "string" - } - }, - "mfaStatus": { - "type": "string", - "enum": [ - "pending", - "none", - "totp" - ] - }, - "customer": { - "$ref": "#/components/schemas/AuthCustomer" - }, - "user": { - "$ref": "#/components/schemas/AuthUser" - }, - "tokens": { - "$ref": "#/components/schemas/AuthTokens" - }, - "termsOfUse": { - "$ref": "#/components/schemas/TermsOfUseStatus" - } - }, - "required": [ - "permissions", - "mfaStatus" - ] - }, "AuthRefreshAccessTokenReq": { "type": "object", "properties": { diff --git a/package-lock.json b/package-lock.json index 2add2f4..e1b3830 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29,6 +29,7 @@ "axios": "^0.27.2", "class-transformer": "^0.5.1", "class-validator": "^0.14.0", + "cookie-parser": "^1.4.7", "cron-parser": "^4.9.0", "csv": "^6.3.11", "dotenv": "^14.3.2", @@ -52,6 +53,7 @@ "swagger-ui-express": "^4.6.3" }, "devDependencies": { + "@types/cookie-parser": "^1.4.9", "@types/express": "^4.17.17", "@types/jest": "27.0.2", "@types/jsonwebtoken": "^8.5.9", @@ -3664,6 +3666,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", @@ -5740,6 +5752,28 @@ "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-signature": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", diff --git a/package.json b/package.json index b59aa9c..8cc3e5a 100644 --- a/package.json +++ b/package.json @@ -47,6 +47,7 @@ "axios": "^0.27.2", "class-transformer": "^0.5.1", "class-validator": "^0.14.0", + "cookie-parser": "^1.4.7", "cron-parser": "^4.9.0", "csv": "^6.3.11", "dotenv": "^14.3.2", @@ -73,6 +74,7 @@ "multer": "1.4.5-lts.1" }, "devDependencies": { + "@types/cookie-parser": "^1.4.9", "@types/express": "^4.17.17", "@types/jest": "27.0.2", "@types/jsonwebtoken": "^8.5.9", diff --git a/src/main.ts b/src/main.ts index 6fd8445..ee5a1f2 100644 --- a/src/main.ts +++ b/src/main.ts @@ -9,6 +9,8 @@ import { AppModule } from './app.module'; import { writeFileSync } from 'fs'; import { execSync } from 'child_process'; import { INestApplication } from '@nestjs/common'; +import cookieParser from 'cookie-parser'; + async function bootstrap() { DadosferaLogger.setupLogger({ serviceName: 'maestro', @@ -23,9 +25,11 @@ async function bootstrap() { methods: 'GET,HEAD,PUT,PATCH,POST,DELETE', preflightContinue: false, optionsSuccessStatus: 204, + credentials: true }, }); app.use(helmet()); + app.use(cookieParser(process.env.COOKIE_SECRET)); if (process.env.ENV === 'prd') { app.use('/catalog/register-dataset', json({ limit: '10mb' })); app.use('/catalog/register-dataset', urlencoded({ extended: true, limit: '10mb' })); diff --git a/src/modules/auth/auth.controller.ts b/src/modules/auth/auth.controller.ts index edefd98..edc4dc0 100644 --- a/src/modules/auth/auth.controller.ts +++ b/src/modules/auth/auth.controller.ts @@ -12,6 +12,7 @@ import { Redirect, Req, Param, + Res, } from '@nestjs/common'; import { ApiHeaders, @@ -47,9 +48,9 @@ import { } from './dtos/login'; import { PackTheMetadata } from 'src/utils/PackTheMetadata'; import { AuthGuard } from '@nestjs/passport'; -import { Request } from 'express'; +import { Request, Response } from 'express'; import ErrorCodes, { OauthErrors } from 'src/utils/errorCodes'; -import jwt from 'jsonwebtoken'; +import jwt, { JwtPayload } from 'jsonwebtoken'; import { LanguageEnum } from 'src/utils/languages.enum'; import { Language } from 'src/decorators/language.decorator'; import { ApiInternalOnlyEndpoint } from 'src/decorators/swagger.decorator'; @@ -86,11 +87,20 @@ export class AuthController { async signIn( @Body() { username, password, totp }: AuthSignInReq, @Language() language: LanguageEnum, - ): Promise { - this.logger.info('/auth - SignIn'); - const metadata = PackTheMetadata({ language }); - this.logger.info('metadata: ' + JSON.stringify(metadata.toJSON())); - return this.authClient.signIn({ username, password, totp }, metadata); + @Res() res: Response, + ) { + try { + this.logger.info('/auth - SignIn'); + const metadata = PackTheMetadata({ language }); + this.logger.info('metadata: ' + JSON.stringify(metadata.toJSON())); + const data = await this.authClient.signIn({ username, password, totp }, metadata); + this.addTokenInCookie(res, data.tokens); + + return res.send(data); + } catch (error) { + this.logger.error('/auth - SignIn - ERROR', error); + } + } @Post('refresh-access-token') @@ -100,17 +110,23 @@ export class AuthController { @Body() body: AuthRefreshAccessTokenReq, @Language() language: LanguageEnum, @Headers('origin') origin: string, + @Res() res: Response, ) { this.logger.info('/auth - RefreshAccessToken'); const { refreshToken, customerName: customer_name } = body; - const frontHost = origin.replace(/^https?:\/\//, ''); + const metadata = PackTheMetadata({ language, customer_name, - custom_host: frontHost, }); - return this.authClient.refreshAccessToken({ refreshToken }, metadata); + const data = await this.authClient.refreshAccessToken({ refreshToken }, metadata); + + this.addTokenInCookie(res, { + accessToken: data.accessToken, + }); + + return res.send(data); } @ApiInternalOnlyEndpoint() @@ -418,4 +434,93 @@ export class AuthController { return this.authClient.resetUsers(body.users, metadata); } + + @Get('me') + async getMe(@Req() req: Request, @Res() res: Response) { + // Lê cookies + const accessToken = req.cookies['ddf-auth']; + const refreshToken = req.cookies['ddf-refresh-auth']; + + console.log(req.cookies) + let payload: any; + let userInfo: any = {}; + try { + // Decodifica e valida o JWT de acesso + const decoded: any = accessToken && jwt.decode(accessToken, { complete: true }); + if (!decoded) throw new Error('Invalid token'); + const { kid } = decoded.header; + // Busca a chave pública + const { keys } = await this.authClient.getPublicKeys(); + const pemValue = keys.find((k) => k.kid === kid)?.pem; + if (!pemValue) throw new Error('Public key not found'); + jwt.verify(accessToken, pemValue); + payload = decoded.payload; + userInfo = { + id: payload.user_id, + name: payload.username, + customer: { + id: payload.customer_id, + name: payload.customer_name, + tier: payload.customer_tier, + } + }; + return res.status(200).json(userInfo); + } catch (err) { + // Se access token inválido, tenta refresh + if (!refreshToken) { + return res.status(401).json({ error: 'Not authenticated' }); + } + try { + // Chama refreshAccessToken + const metadata = PackTheMetadata({}); + const data = await this.authClient.refreshAccessToken({ refreshToken }, metadata); + // Retorna novo access token e dados mínimos + this.addTokenInCookie(res, { + accessToken: data.accessToken, + }); + // Decodifica novo token + const decoded: any = jwt.decode(data.accessToken, { complete: true }); + const payload = decoded.payload; + userInfo = { + id: payload.user_id, + name: payload.username, + customer: { + id: payload.customer_id, + name: payload.customer_name, + tier: payload.customer_tier, + } + }; + return res.status(200).json(userInfo); + } catch (refreshErr) { + return res.status(401).json({ error: 'Not authenticated' }); + } + } + } + + private addTokenInCookie(res: Response, tokens: { accessToken?: string, refreshToken?: string }) { + let exp = 1000 * 60 * 5; // 5 minutes + + if (tokens.accessToken) { + const { exp: expiration } = jwt.decode(tokens.accessToken) as JwtPayload; + exp = (expiration - 30) * 1000; // exp em segundos, maxAge em ms + + res.cookie('ddf-auth', tokens.accessToken, { + domain: 'dadosfera.local', + maxAge: exp, + httpOnly: true, + secure: true, + sameSite: 'none', // Necessário para cookies em requisições cross-site + }); + } + + if (tokens.refreshToken) { + res.cookie('ddf-refresh-auth', tokens.refreshToken, { + domain: 'dadosfera.local', + maxAge: exp, + httpOnly: true, + secure: true, + sameSite: 'none', // Necessário para cookies em requisições cross-site + }); + } + } } From 8efa6790d63a88e5f8e8abd049a60a24aac72bfb Mon Sep 17 00:00:00 2001 From: marcos-silva-rodrigues Date: Fri, 4 Jul 2025 18:03:08 -0300 Subject: [PATCH 2/8] FEAT: sign out endpoint --- src/modules/auth/auth.controller.ts | 62 ++++++++++++++++++++++++++--- 1 file changed, 57 insertions(+), 5 deletions(-) diff --git a/src/modules/auth/auth.controller.ts b/src/modules/auth/auth.controller.ts index edc4dc0..7910f17 100644 --- a/src/modules/auth/auth.controller.ts +++ b/src/modules/auth/auth.controller.ts @@ -103,6 +103,43 @@ export class AuthController { } + @Post('sign-out') + @HttpCode(HttpStatus.NO_CONTENT) + async signOut( + @Language() language: LanguageEnum, + @Res() res: Response, + ) { + try { + this.logger.info('/auth - SignOut'); + const exp = 1000 * 60 * 3; + + res.cookie('ddf-auth', '', { + domain: 'dadosfera.local', + maxAge: Date.now() - exp, + expires: new Date(), + httpOnly: true, + secure: true, + sameSite: 'none', // Necessário para cookies em requisições cross-site + }); + + res.cookie('ddf-refresh-auth', '', { + domain: 'dadosfera.local', + maxAge: Date.now() - exp, + expires: new Date(), + httpOnly: true, + secure: true, + sameSite: 'none', // Necessário para cookies em requisições cross-site + }); + + this.logger.info('Clean cookie sessions'); + + return res.send(); + } catch (error) { + this.logger.error('/auth - SignIn - ERROR', error); + } + + } + @Post('refresh-access-token') @HttpCode(HttpStatus.OK) @ApiOkResponse({ type: AuthRefreshAccessTokenRes }) @@ -437,18 +474,20 @@ export class AuthController { @Get('me') async getMe(@Req() req: Request, @Res() res: Response) { + this.logger.info('GET /auth/me ') // Lê cookies const accessToken = req.cookies['ddf-auth']; - const refreshToken = req.cookies['ddf-refresh-auth']; + let customerName = ''; - console.log(req.cookies) + this.logger.info('Has cookie: ' + Boolean(accessToken)) let payload: any; let userInfo: any = {}; try { // Decodifica e valida o JWT de acesso const decoded: any = accessToken && jwt.decode(accessToken, { complete: true }); - if (!decoded) throw new Error('Invalid token'); + if (!decoded) throw new Error('Invalid token') const { kid } = decoded.header; + customerName = decoded.payload.customer_name; // Busca a chave pública const { keys } = await this.authClient.getPublicKeys(); const pemValue = keys.find((k) => k.kid === kid)?.pem; @@ -466,14 +505,24 @@ export class AuthController { }; return res.status(200).json(userInfo); } catch (err) { + this.logger.error(err.message); + const refreshToken = req.cookies['ddf-refresh-auth']; + + this.logger.info('Token is invalid') + this.logger.info('Has Refresh Token: '+ Boolean(refreshToken)) // Se access token inválido, tenta refresh - if (!refreshToken) { + if (!refreshToken || !customerName) { + this.logger.error('Invalid refresh token or customer name'); return res.status(401).json({ error: 'Not authenticated' }); } try { // Chama refreshAccessToken - const metadata = PackTheMetadata({}); + const metadata = PackTheMetadata({ + customer_name: customerName + }); + this.logger.info('Call Refresh Token') const data = await this.authClient.refreshAccessToken({ refreshToken }, metadata); + this.logger.info('Finish Refresh Token') // Retorna novo access token e dados mínimos this.addTokenInCookie(res, { accessToken: data.accessToken, @@ -492,6 +541,7 @@ export class AuthController { }; return res.status(200).json(userInfo); } catch (refreshErr) { + this.logger.error(refreshErr) return res.status(401).json({ error: 'Not authenticated' }); } } @@ -504,6 +554,7 @@ export class AuthController { const { exp: expiration } = jwt.decode(tokens.accessToken) as JwtPayload; exp = (expiration - 30) * 1000; // exp em segundos, maxAge em ms + this.logger.info('Set Cookie ddf-auth') res.cookie('ddf-auth', tokens.accessToken, { domain: 'dadosfera.local', maxAge: exp, @@ -514,6 +565,7 @@ export class AuthController { } if (tokens.refreshToken) { + this.logger.info('Set Cookie ddf-refresh-auth') res.cookie('ddf-refresh-auth', tokens.refreshToken, { domain: 'dadosfera.local', maxAge: exp, From f626a9bb5e7b5783b4e3a792263cb04598bcb2bb Mon Sep 17 00:00:00 2001 From: marcos-silva-rodrigues Date: Thu, 7 Aug 2025 10:32:22 -0300 Subject: [PATCH 3/8] UPDATE: add more logger --- .../identity-provider.controller.ts | 96 +++++++++++++------ .../identity-provider.service.ts | 36 +++++-- 2 files changed, 97 insertions(+), 35 deletions(-) diff --git a/src/modules/identity-provider/identity-provider.controller.ts b/src/modules/identity-provider/identity-provider.controller.ts index de22e1c..c612f4e 100644 --- a/src/modules/identity-provider/identity-provider.controller.ts +++ b/src/modules/identity-provider/identity-provider.controller.ts @@ -153,12 +153,27 @@ export class IdentityProviderController { throw new Error(ErrorCodes.IDENTITY_PROVIDER.INVALID_RESPONSE); } - const origin = req.headers['origin'] as string; + try { + const origin = req.headers['origin'] as string; + this.logger.info('Header Origin: ' + origin); - const lang = language.substring(0, 2) + language.substring(2).toUpperCase(); - const callbackUrl = process.env.ENV !== "prd" ? `${origin}/auth/callback` : `${origin}/${lang}/auth/callback`; + const lang = + language.substring(0, 2) + language.substring(2).toUpperCase(); + const callbackUrl = + process.env.ENV !== 'prd' + ? `${origin}/auth/callback` + : `${origin}/${lang}/auth/callback`; - return await this.identityProviderService.getTokenByIdp(code, state, callbackUrl); + this.logger.info('Callback URL: ' + callbackUrl); + return await this.identityProviderService.getTokenByIdp( + code, + state, + callbackUrl, + ); + } catch (error) { + this.logger.error(error); + throw error; + } } @Get('/links') @@ -166,41 +181,66 @@ export class IdentityProviderController { async providerLinks(@Req() req: Request) { this.logger.info('GET /identity-providers/links'); - const frontDomain = req.headers['origin'] as string; + try { + const frontDomain = req.headers['origin'] as string; + this.logger.info('Header Origin: ' + frontDomain); - if (!frontDomain) { - throw new Error(ErrorCodes.IDENTITY_PROVIDER.INVALID_HEADER); + if (!frontDomain) { + this.logger.info('Not found front domain'); + throw new Error(ErrorCodes.IDENTITY_PROVIDER.INVALID_HEADER); + } + + const result = + await this.identityProviderService.identityProvidersLinksPerDomain( + frontDomain, + ); + return result; + } catch (error) { + this.logger.error(error); + throw error; } - - const result = - await this.identityProviderService.identityProvidersLinksPerDomain( - frontDomain, - ); - return result; } @Get(':id') @HttpCode(HttpStatus.OK) @Redirect() - async loginIdp(@Param('id') id: string, @Req() req: Request, @Language() language: LanguageEnum) { + async loginIdp( + @Param('id') id: string, + @Req() req: Request, + @Language() language: LanguageEnum, + ) { this.logger.info('GET /identity-providers/:id'); + try { + const frontDomain = + (req.headers['origin'] as string) || (req.headers['referer'] as string); + this.logger.info(`Front domain: ${frontDomain}`); + const host = + frontDomain.lastIndexOf('/') !== -1 + ? frontDomain.substring(0, frontDomain.lastIndexOf('/')) + : frontDomain; - const frontDomain = req.headers['origin'] as string || req.headers['referer'] as string; - this.logger.info(`Front domain: ${frontDomain}`); - const host = frontDomain.lastIndexOf('/') !== -1 - ? frontDomain.substring(0, frontDomain.lastIndexOf('/')) - : frontDomain; + const lang = + language.substring(0, 2) + language.substring(2).toUpperCase(); + const callbackUrl = + process.env.ENV !== 'prd' + ? `${host}/auth/callback` + : `${host}/${lang}/auth/callback`; - const lang = language.substring(0, 2) + language.substring(2).toUpperCase(); - const callbackUrl = process.env.ENV !== "prd" ? `${host}/auth/callback` : `${host}/${lang}/auth/callback`; + this.logger.info('Callback URL: ' + callbackUrl); + const redirectUrl = + await this.identityProviderService.loginIdentityProvider( + id, + callbackUrl, + ); - const redirectUrl = - await this.identityProviderService.loginIdentityProvider(id, callbackUrl); - - this.logger.info(`Redirecting to: ${redirectUrl}`); - return { - url: redirectUrl, - }; + this.logger.info(`Redirecting to: ${redirectUrl}`); + return { + url: redirectUrl, + }; + } catch (error) { + this.logger.error(error); + throw error; + } } } diff --git a/src/modules/identity-provider/identity-provider.service.ts b/src/modules/identity-provider/identity-provider.service.ts index 4cca0fe..a2e7084 100644 --- a/src/modules/identity-provider/identity-provider.service.ts +++ b/src/modules/identity-provider/identity-provider.service.ts @@ -14,16 +14,21 @@ import { Metadata } from '@grpc/grpc-js'; import { Issuer, generators } from 'openid-client'; import { SsoSignInDto } from './dto/sso-signin.dto'; import { CacheService } from 'src/services/cache.service'; -import { Request } from 'express'; import { CreateIdentityProvider } from './dto/identity-provider.dto'; +import DadosferaLogger from '@dadosfera/dadosfera-logs'; @Injectable() export class IdentityProviderService implements OnModuleInit { + private logger: DadosferaLogger; private identityProviderService: IdentityProviderProtoService; constructor( @Inject(DucClient.name) private readonly grpcClient: ClientGrpc, private readonly cacheService: CacheService, - ) {} + @Inject(DadosferaLogger) + private dadosferaLoggger: DadosferaLogger + ) { + this.logger = dadosferaLoggger.logger; + } onModuleInit() { this.identityProviderService = @@ -33,22 +38,26 @@ export class IdentityProviderService implements OnModuleInit { } async create(body: IdentityProviderRequest, metadata: Metadata) { + this.logger.info("Call IdentityProvider GRPC Create") return await lastValueFrom( this.identityProviderService.Create(body, metadata), ); } async getList(metadata: Metadata) { + this.logger.info("Call IdentityProvider GRPC GetList") return await lastValueFrom( this.identityProviderService.GetList({}, metadata), ); } async loginIdentityProvider(id: string, callbackUrl: string) { + this.logger.info("Call IdentityProvider GRPC FindIdentityProvider with: " + id); const idp = await lastValueFrom( this.identityProviderService.FindIdentityProvider({ id }), ); + this.logger.info("Discovery issueURL: " + idp.issuerUrl) const issuer = await Issuer.discover(idp.issuerUrl); const client = new issuer.Client({ client_id: idp.clientId, @@ -57,14 +66,19 @@ export class IdentityProviderService implements OnModuleInit { response_types: ['code'], }); + this.logger.info("Generate Challenge") const code_verifier: string = generators.codeVerifier(); const code_challenge: string = generators.codeChallenge(code_verifier); + this.logger.info("Generate State") const state = generators.state(); + + this.logger.info("Generate Nonce") const nonce = generators.nonce(); // Using state because it is returned in the callback // and we can use it to retrieve the code_verifier and nonce + this.logger.info("Save Login parameters in redis") await this.cacheService.set(state, { codeVerifier: code_verifier, nonce, @@ -76,6 +90,7 @@ export class IdentityProviderService implements OnModuleInit { redirectUrls: idp.redirectUrls, }); + this.logger.info("Generate Authorization URL") const url = client.authorizationUrl({ scope: 'openid email', response_type: 'code', @@ -86,16 +101,20 @@ export class IdentityProviderService implements OnModuleInit { redirect_uri: callbackUrl, }); const idpUrl = url + '&identity_provider=' + idp.name; + this.logger.info(idpUrl) return idpUrl; } async getTokenByIdp(code: string, state: string, callbackUrl: string) { + this.logger.info("Get login parameters in redis") const ssoSign = await this.cacheService.get(state); if (!ssoSign) { + this.logger.info("Login Parameters Not Found") throw new BadRequestException('SSO sign-in is expired or not found'); } + this.logger.info("Discovery Issue URL: " + ssoSign.issuerUrl) const issuer = await Issuer.discover(ssoSign.issuerUrl); const client = new issuer.Client({ client_id: ssoSign.clientId, @@ -103,22 +122,22 @@ export class IdentityProviderService implements OnModuleInit { redirect_uris: ssoSign.redirectUrls, }); - if (ssoSign === null) { - throw new BadRequestException('SSO sign-in is expired or not found'); - } - const params = client.callbackParams( `${callbackUrl}?code=${code}&state=${state}`, ); try { + + this.logger.info("Get Token Set"); const tokenSet = await client.callback(callbackUrl, params, { nonce: ssoSign.nonce, code_verifier: ssoSign.codeVerifier, state: ssoSign.state }); + this.logger.info("Delete parameters in redis"); await this.cacheService.delete(ssoSign.state); + this.logger.info("Call IdentityProvider GRPC SignInUser"); return await lastValueFrom( this.identityProviderService.SignInUser({ accessToken: tokenSet.access_token, @@ -128,12 +147,13 @@ export class IdentityProviderService implements OnModuleInit { }), ); } catch (error) { - console.log(error); + this.logger.error(error); throw error; } } async deleteIdentityProvider(id: string, metadata: Metadata) { + this.logger.info("Call IdentityProvider GRPC Delete with: " + id) return await lastValueFrom( this.identityProviderService.DeleteIdentityProvider({ id }, metadata), ); @@ -144,6 +164,7 @@ export class IdentityProviderService implements OnModuleInit { body: CreateIdentityProvider, metadata: Metadata, ) { + this.logger.info("Call IdentityProvider GRPC Update with: " + id) return await lastValueFrom( this.identityProviderService.UpdateIdentityProvider( { @@ -156,6 +177,7 @@ export class IdentityProviderService implements OnModuleInit { } async identityProvidersLinksPerDomain(frontDomain: string) { + this.logger.info("Call IdentityProvider GRPC LinksPerDomain with: " + frontDomain) return await lastValueFrom( this.identityProviderService.GetProviderLinksFromDomain({ frontDomain }), ); From 193031dfdd1d05e107b12d807640ad244cd14b14 Mon Sep 17 00:00:00 2001 From: marcos-silva-rodrigues Date: Thu, 7 Aug 2025 10:41:48 -0300 Subject: [PATCH 4/8] UPDATE: increase redis ttl --- src/services/service.module.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/service.module.ts b/src/services/service.module.ts index d129eaf..0ce735f 100644 --- a/src/services/service.module.ts +++ b/src/services/service.module.ts @@ -7,7 +7,7 @@ import { redisStore } from 'cache-manager-ioredis-yet'; CacheModule.registerAsync({ useFactory: async () => ({ store: await redisStore({ - ttl: 1000 * 60, //1 minute + ttl: 5 * 1000 * 60, // 5 minute host: process.env.REDIS_HOST, port: process.env.REDIS_PORT && Number(process.env.REDIS_PORT), db: process.env.REDIS_DATABASE && Number(process.env.REDIS_DATABASE), From a654baef138427456e639877f12cde196ac189ee Mon Sep 17 00:00:00 2001 From: marcos-silva-rodrigues Date: Thu, 14 Aug 2025 15:37:17 -0300 Subject: [PATCH 5/8] FIX: set cookies if exists tokens --- src/modules/auth/auth.controller.ts | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/modules/auth/auth.controller.ts b/src/modules/auth/auth.controller.ts index 3be929b..3e66e94 100644 --- a/src/modules/auth/auth.controller.ts +++ b/src/modules/auth/auth.controller.ts @@ -101,11 +101,15 @@ export class AuthController { const metadata = PackTheMetadata({ language }); this.logger.info('metadata: ' + JSON.stringify(metadata.toJSON())); const data = await this.authClient.signIn({ username, password, totp }, metadata); - this.addTokenInCookie(res, { - accessToken: data.tokens.accessToken, - refreshToken: data.tokens.refreshToken, - userId: data.user.id - }); + + if (data.tokens) { + this.addTokenInCookie(res, { + accessToken: data.tokens.accessToken, + refreshToken: data.tokens.refreshToken, + userId: data.user.id + }); + } + return res.send(data); } catch (error) { From 216349f303b84bfbdebe52ba3254beb7fea809ac Mon Sep 17 00:00:00 2001 From: marcos-silva-rodrigues Date: Thu, 14 Aug 2025 16:03:28 -0300 Subject: [PATCH 6/8] CHORE: update nginx ingress annotations --- deploy/helm-chart/templates/ingress.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/deploy/helm-chart/templates/ingress.yaml b/deploy/helm-chart/templates/ingress.yaml index 1594baa..072b557 100644 --- a/deploy/helm-chart/templates/ingress.yaml +++ b/deploy/helm-chart/templates/ingress.yaml @@ -9,6 +9,9 @@ metadata: nginx.ingress.kubernetes.io/server-snippet: | underscores_in_headers on; ignore_invalid_headers on; + nginx.ingress.kubernetes.io/proxy-buffer-size: "16k" + nginx.ingress.kubernetes.io/proxy-buffers-number: "8" + nginx.ingress.kubernetes.io/proxy-busy-buffers-size: "64k" {{- if .Values.maestro.restricted_ip}} nginx.ingress.kubernetes.io/whitelist-source-range: {{ .Values.maestro.restricted_ip }} {{- end }} From ba53934068ca3e39212e5cfba2eb7127f042c161 Mon Sep 17 00:00:00 2001 From: marcos-silva-rodrigues Date: Thu, 14 Aug 2025 17:23:40 -0300 Subject: [PATCH 7/8] CI: add cookie secret --- deploy/helm-chart/templates/deployment.yaml | 2 ++ deploy/helm-chart/values-stg.yaml | 1 + deploy/helm-chart/values.yaml | 1 + environment.d.ts | 1 + 4 files changed, 5 insertions(+) diff --git a/deploy/helm-chart/templates/deployment.yaml b/deploy/helm-chart/templates/deployment.yaml index 31310e5..c239c93 100644 --- a/deploy/helm-chart/templates/deployment.yaml +++ b/deploy/helm-chart/templates/deployment.yaml @@ -94,6 +94,8 @@ spec: value: {{ .Values.maestro.open_group_id }} - name: DEDICATED_PROXY value: {{ .Values.maestro.dedicated_proxy }} + - name: COOKIE_SECRET + value: {{ .Values.maestro.cookie_secret }} - name: REDIS_DATABASE value: "{{ .Values.maestro.redis_database }}" - name: REDIS_HOST diff --git a/deploy/helm-chart/values-stg.yaml b/deploy/helm-chart/values-stg.yaml index 9bcd584..6a9b23b 100644 --- a/deploy/helm-chart/values-stg.yaml +++ b/deploy/helm-chart/values-stg.yaml @@ -6,6 +6,7 @@ maestro: tr_factory_url: in-factory.stg.dadosfera.ai open_customer_id: b3e3dfe5-b992-4586-a73c-c0b0c00f615d open_group_id: e3f98a2f-7748-4981-8505-7695c8ca8218 + cookie_secret: "ff7bc13823edb2ae50d248e5780bddc9d4b31c36" hostname: maestro.stg.dadosfera.ai diff --git a/deploy/helm-chart/values.yaml b/deploy/helm-chart/values.yaml index 9ed722c..4844fbc 100644 --- a/deploy/helm-chart/values.yaml +++ b/deploy/helm-chart/values.yaml @@ -48,6 +48,7 @@ maestro: redis_host: "product-redis-prd.z4xvqj.0001.use1.cache.amazonaws.com" redis_port: "6379" redis_database: "0" + cookie_secret: "13cc5e136d3074bcc05bec8697092ec1f5f376bf" autoscaling: enabled: false minReplicas: 1 diff --git a/environment.d.ts b/environment.d.ts index 3558f26..3b44250 100644 --- a/environment.d.ts +++ b/environment.d.ts @@ -15,6 +15,7 @@ declare global { OPEN_GROUP_ID: string; OPEN_CUSTOMER_ID: string; DEDICATED_PROXY: string; + COOKIE_SECRET: string; } } } From 301b6e98ec12e6cab0079f61a4e0cffa0923ca5a Mon Sep 17 00:00:00 2001 From: marcos-silva-rodrigues Date: Thu, 14 Aug 2025 17:42:42 -0300 Subject: [PATCH 8/8] UPDATE: test subdomain --- src/modules/auth/auth.controller.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/modules/auth/auth.controller.ts b/src/modules/auth/auth.controller.ts index 3e66e94..d32a2d7 100644 --- a/src/modules/auth/auth.controller.ts +++ b/src/modules/auth/auth.controller.ts @@ -572,7 +572,7 @@ export class AuthController { this.logger.info('Set Cookie ddf-auth') res.cookie('ddf-auth', data.accessToken, { - domain: 'dadosfera.ai', + domain: 'stg.dadosfera.ai', maxAge: exp, httpOnly: true, secure: true, @@ -583,7 +583,7 @@ export class AuthController { if (data.refreshToken) { this.logger.info('Set Cookie ddf-refresh-auth') res.cookie('ddf-refresh-auth', data.refreshToken, { - domain: 'dadosfera.ai', + domain: 'stg.dadosfera.ai', maxAge: exp, httpOnly: true, secure: true, @@ -594,7 +594,7 @@ export class AuthController { if (data.userId) { this.logger.info('Set Cookie ddf-refresh-auth') res.cookie('ddf-user-id', data.userId, { - domain: 'dadosfera.ai', + domain: 'stg.dadosfera.ai', maxAge: exp, httpOnly: true, secure: true,