import { BadRequestException, ForbiddenException, Injectable, UnauthorizedException, } from '@nestjs/common'; import { AuthConfigService } from '../auth.config'; import { AuthTokenService } from '../auth-token.service'; import { AuthResult } from '../auth.types'; import { OAuthStateService } from './oauth-state.service'; const MICROSOFT_SCOPE = 'openid profile email User.Read'; @Injectable() export class MicrosoftOAuthProvider { constructor( private readonly authConfig: AuthConfigService, private readonly authToken: AuthTokenService, private readonly oauthState: OAuthStateService, ) {} getAuthorizeUrl() { const config = this.authConfig.getConfig(); this.assertMicrosoftConfig(); const params = new URLSearchParams({ client_id: config.microsoft.clientId!, response_type: 'code', redirect_uri: config.microsoft.redirectUri!, response_mode: 'query', scope: MICROSOFT_SCOPE, state: this.oauthState.createSignedState(), }); return `https://login.microsoftonline.com/${config.microsoft.tenantId}/oauth2/v2.0/authorize?${params.toString()}`; } async authenticateCallback(query: { code?: string; state?: string }): Promise { if (!query.code || !query.state || !this.oauthState.verifySignedState(query.state)) { throw new BadRequestException('Callback Microsoft invalido'); } const tokenResponse = await this.exchangeCode(query.code); const microsoftUser = await this.getMicrosoftUser(tokenResponse.access_token); const email = microsoftUser.mail || microsoftUser.userPrincipalName; const user = { id: microsoftUser.id || email, name: microsoftUser.displayName || email, email, username: microsoftUser.userPrincipalName || email, provider: 'microsoft' as const, }; return { token: this.authToken.issueToken(user), user, }; } private assertMicrosoftConfig() { const config = this.authConfig.getConfig(); if (!config.microsoft.enabled) { throw new ForbiddenException('Login Microsoft desabilitado'); } const missing = [ ['MICROSOFT_CLIENT_ID', config.microsoft.clientId], ['MICROSOFT_CLIENT_SECRET', config.microsoft.clientSecret], ['MICROSOFT_REDIRECT_URI', config.microsoft.redirectUri], ].filter(([, value]) => !value); if (missing.length) { throw new Error(`${missing.map(([name]) => name).join(', ')} nao configurado`); } } private async exchangeCode(code: string) { const config = this.authConfig.getConfig(); this.assertMicrosoftConfig(); const tokenUrl = `https://login.microsoftonline.com/${config.microsoft.tenantId}/oauth2/v2.0/token`; const body = new URLSearchParams({ client_id: config.microsoft.clientId!, client_secret: config.microsoft.clientSecret!, code, grant_type: 'authorization_code', redirect_uri: config.microsoft.redirectUri!, scope: MICROSOFT_SCOPE, }); const response = await fetch(tokenUrl, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, body, }); if (!response.ok) { throw new UnauthorizedException('Falha ao trocar codigo Microsoft por token'); } return response.json(); } private async getMicrosoftUser(accessToken: string) { const response = await fetch('https://graph.microsoft.com/v1.0/me', { headers: { Authorization: `Bearer ${accessToken}`, }, }); if (!response.ok) { throw new UnauthorizedException('Falha ao consultar usuario Microsoft'); } return response.json(); } }