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();
|
||
|
|
}
|
||
|
|
}
|