import { OnModuleInit, Inject, Injectable, ForbiddenException, UnauthorizedException, } from '@nestjs/common'; import { ClientGrpc } from '@nestjs/microservices'; import { DadosferaLogger } from '@dadosfera/dadosfera-logs'; import { lastValueFrom } from 'rxjs'; import { ProtoServices } from '@dadosfera/protospack-v2/dist/lib/Duc'; import { AuthProtoService as AuthServiceInterface, IdentityProviderProtoService, UsersProtoService, } from '@dadosfera/protospack-v2/dist/lib/Duc/interfaces/write-service'; import { AuthSnowflakeSignInRequest, AuthSignInRequest, AuthRefreshAccessTokenRequest, AuthEnableTotpMfaRequest, AuthDisableTotpMfaRequest, AuthDismissTotpMfaRequest, AuthVerifyTotpMfaRequest, AuthChangePasswordRequest, AuthResetPasswordRequest, AuthVerifyResetPasswordCodeRequest, AuthConfirmResetPasswordRequest, AuthSignInResponse, } from '@dadosfera/protospack-v2/dist/lib/Duc/interfaces/messages'; import { DucClient } from '../duc/client.config'; import { Metadata } from '@grpc/grpc-js'; import { BulkEditResponse, UserDTO } from './dtos/login'; import jwt, { JwtPayload } from 'jsonwebtoken'; import { PackTheMetadata } from 'src/utils/PackTheMetadata'; import { Request, Response } from 'express'; type AuthSession = { accessToken?: string; refreshToken?: string; userId?: string; }; @Injectable() export class AuthClientService implements OnModuleInit { logger: DadosferaLogger; private authService: AuthServiceInterface; private userService: UsersProtoService; constructor( @Inject(DadosferaLogger) dadosferaLogger: DadosferaLogger, @Inject(DucClient.name) private readonly grpcClient: ClientGrpc, ) { this.logger = dadosferaLogger.logger; } onModuleInit() { this.authService = this.grpcClient.getService( ProtoServices.AuthProtoService, ); this.userService = this.grpcClient.getService( ProtoServices.UsersProtoService, ); } async getPublicKeys() { this.logger.info('GetPublicKeys'); return lastValueFrom(this.authService.AuthGetPublicKeys({})); } async snowflakeSignIn(input: AuthSnowflakeSignInRequest) { this.logger.info('snowflakeSignIn'); return lastValueFrom(this.authService.AuthSnowflakeSignIn(input)); } checkDedicatedProxy({ customer }: AuthSignInResponse) { const DEDICATED_PROXY = process.env.DEDICATED_PROXY || ''; this.logger.info( 'SignIn - Setting customer ID for dedicated proxy: ' + DEDICATED_PROXY, ); this.logger.info('Customer ID: ' + customer.id); if (DEDICATED_PROXY !== '' && DEDICATED_PROXY !== customer.id) { throw new ForbiddenException(); } // Bloquear o customer de acesso o maestro publico this.logger.info( 'Check if customer have network policy: ' + customer.modules, ); const hasNetworkPolicyModule = customer.modules.includes('network-policy'); if (hasNetworkPolicyModule && DEDICATED_PROXY === '') { throw new ForbiddenException(); } } async signIn( { username, password, totp }: AuthSignInRequest, metadata: Metadata, ) { this.logger.info('SignIn'); let result: AuthSignInResponse; try { result = await lastValueFrom( this.authService.AuthSignIn({ username, password, totp }, metadata), ); } catch (error) { this.logger.error('SignIn - Error during sign-in'); this.logger.error(error); throw error; } if (result.customer) { this.checkDedicatedProxy(result); } return result; } async refreshAccessToken( { refreshToken, userId }: AuthRefreshAccessTokenRequest, metadata: Metadata, ) { this.logger.info('RefreshAccessToken'); return lastValueFrom( this.authService.AuthRefreshAccessToken( { refreshToken, userId }, metadata, ), ); } async changePassword({ accessToken, oldPassword, newPassword, }: AuthChangePasswordRequest) { this.logger.info('ChangePassword'); return lastValueFrom( this.authService.AuthChangePassword({ accessToken, oldPassword, newPassword, }), ); } async resetPassword( { username }: AuthResetPasswordRequest, metadata: Metadata, ) { this.logger.info('resetPassword'); return lastValueFrom( this.authService.AuthResetPassword({ username }, metadata), ); } async verifyResetPasswordCode({ username, code, }: AuthVerifyResetPasswordCodeRequest) { this.logger.info('verifyResetPasswordCode'); return lastValueFrom( this.authService.AuthVerifyResetPasswordCode({ username, code }), ); } async confirmResetPassword( { username, code, newPassword }: AuthConfirmResetPasswordRequest, metadata: Metadata, ) { this.logger.info('confirmResetPassword'); return lastValueFrom( this.authService.AuthConfirmResetPassword( { username, code, newPassword, }, metadata, ), ); } async enableTotpMFA({ accessToken, password }: AuthEnableTotpMfaRequest) { this.logger.info('enableTotpMFA'); return lastValueFrom( this.authService.AuthEnableTotpMfa({ accessToken, password }), ); } async disableTotpMFA({ accessToken, password }: AuthDisableTotpMfaRequest) { this.logger.info('disableTotpMFA'); return lastValueFrom( this.authService.AuthDisableTotpMfa({ accessToken, password }), ); } async dismissTotpMFA({ accessToken }: AuthDismissTotpMfaRequest) { this.logger.info('dismissTotpMFA'); return lastValueFrom(this.authService.AuthDismissTotpMfa({ accessToken })); } async verifyTotp({ accessToken, totp }: AuthVerifyTotpMfaRequest) { this.logger.info('disableTotpMFA'); return lastValueFrom( this.authService.AuthVerifyTotpMfa({ accessToken, totp }), ); } async getSession(session: string) { return lastValueFrom(this.authService.AuthGetSession({ session })); } async oauthSignIn(data: { username: string; token: string }) { const { username, token } = data; return lastValueFrom( this.authService.AuthOauthSignIn({ token, username, refreshToken: '' }), ); } async blockUsers( users: string[], metadata: Metadata, ): Promise { this.logger.info('blockUsers - Service starting'); try { this.logger.debug('Calling BlockUser gRPC method', { metadata: { access_token: metadata.get('access_token'), language: metadata.get('language'), }, }); const response = await lastValueFrom( this.authService.BlockUser({ users }, metadata), ); this.logger.info('blockUsers - Service success', { response }); return response; } catch (error) { this.logger.error('blockUsers - Service error', { error: error.message, stack: error.stack, }); throw error; } } async unblockUsers( users: string[], metadata: Metadata, ): Promise { this.logger.info('unblockUsers'); return await lastValueFrom( this.authService.UnblockUser({ users }, metadata), ); } async resetUsers( users: string[], metadata: Metadata, ): Promise { this.logger.info('resetUsers'); try { this.logger.debug('Calling ResetUser gRPC method', { metadata: { access_token: metadata.get('access_token'), language: metadata.get('language'), }, }); const response = await lastValueFrom( this.authService.ResetUser({ users }, metadata), ); this.logger.info('resetUsers - Success', { response }); return response; } catch (error) { this.logger.error('resetUsers - Error', { error: error.message, stack: error.stack, }); throw error; } } public async validateUserSession(accessToken: any, originHeader: string) { const payload = await this.validateJwtToken(accessToken); const userDto = await this.getUserfromPayload(payload); this.validateResourceAccess(originHeader, userDto); return userDto; } public async refreshUserSession( refreshToken: string, userId: string, originHeader: string, ): Promise<{ user: UserDTO; authSession: AuthSession; }> { const metadata = PackTheMetadata({}); this.logger.info('Call Refresh Token'); const refreshCredentials = await this.refreshAccessToken( { refreshToken, userId }, metadata, ); this.logger.info('Finish Refresh Token'); const userDto = await this.validateUserSession( refreshCredentials.accessToken, originHeader, ); return { user: userDto, authSession: { accessToken: refreshCredentials.accessToken, refreshToken: refreshCredentials.refreshToken, userId, }, }; } public writeAuthSession(res: Response, data: AuthSession) { let exp = 1000 * 60 * 5; // 5 minutes if (data.accessToken) { const { exp: expiration } = jwt.decode(data.accessToken) as JwtPayload; exp = (expiration - 30) * 1000; // exp em segundos, maxAge em ms this.logger.info('Set Cookie ddf-auth'); res.cookie('ddf-auth', data.accessToken, { domain: '.dadosfera.ai', maxAge: exp, httpOnly: true, secure: true, sameSite: 'none', // Necessário para cookies em requisições cross-site }); } if (data.refreshToken) { this.logger.info('Set Cookie ddf-refresh-auth'); res.cookie('ddf-refresh-auth', data.refreshToken, { domain: '.dadosfera.ai', maxAge: exp, httpOnly: true, secure: true, sameSite: 'none', // Necessário para cookies em requisições cross-site }); } if (data.userId) { this.logger.info('Set Cookie ddf-refresh-auth'); res.cookie('ddf-user-id', data.userId, { domain: '.dadosfera.ai', maxAge: exp, httpOnly: true, secure: true, sameSite: 'none', // Necessário para cookies em requisições cross-site }); } } public cleanUpAuthSession(res: Response) { const exp = 1000 * 60 * 3; res.cookie('ddf-auth', '', { domain: 'dadosfera.ai', 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.ai', 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'); } private async validateJwtToken(token: string) { const decoded: any = token && jwt.decode(token, { complete: true }); if (!decoded) throw new Error('Invalid token'); const { kid } = decoded.header; // Busca a chave pública const { keys } = await this.getPublicKeys(); const pemValue = keys.find((k) => k.kid === kid)?.pem; if (!pemValue) throw new Error('Public key not found'); jwt.verify(token, pemValue); return decoded.payload; } private async getUserfromPayload(payload: JwtPayload): Promise { this.logger.info('getUser'); const metadata = PackTheMetadata({ customer_id: payload.customer_id, }); const { user } = await lastValueFrom( this.userService.UserFindOneById({ id: payload.user_id }, metadata), ); const userDto: UserDTO = { id: user.id, name: user.username, jobTitle: user?.jobTitle || null, department: user?.department || null, hierarchy: user?.hierarchy || null, customer: { id: payload.customer_id, name: payload.customer_name, tier: payload.customer_tier, }, }; return userDto; } private validateResourceAccess(host: string, user: UserDTO) { this.logger.info( "Validate whether the source URL is a resource belonging to the user's client", ); this.logger.info('Host: ' + host); this.logger.info('Customer: ' + user.customer.name); const hostParts = host.split('.'); const domain = hostParts[0]; const isResouceStg = hostParts[1] === 'stg'; const domainParts = domain.split('-'); const customerInDomain = domainParts[domainParts.length - 1]; if (isResouceStg && process.env.ENV !== 'stg') { throw new ForbiddenException( `Customer ${user.customer.name} cannot access ${origin}`, ); } if (customerInDomain != user.customer.name) { throw new ForbiddenException( `Customer ${user.customer.name} cannot access ${origin}`, ); } return; } }