* Bootstrap da aplicação com NestJS + TypeScript * Migração de Node.js puro + JavaScript para NestJS como framework * Estrutura base: AppModule, AppController, health check em /health * loadEnv com busca hierárquica de .env por ambiente * Módulo auth completo com arquitetura em camadas: - AuthController: rotas HTTP de autenticação - AuthService: fachada de negócio - AuthConfigService: leitura centralizada de variáveis de ambiente - AuthTokenService: emissão de JWT próprio da aplicação * Autenticação via LDAP/Active Directory com ldapts * Autenticação via Microsoft OAuth 2.0 (Entra ID) * Proteção CSRF no fluxo OAuth com HMAC state assinado * Endpoint /auth/config para o frontend descobrir provedores ativos * Documentação do módulo em docs/auth.md
120 lines
3.6 KiB
TypeScript
120 lines
3.6 KiB
TypeScript
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<AuthResult> {
|
|
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();
|
|
}
|
|
}
|