REFACTOR: openid module

This commit is contained in:
marcos-silva-rodrigues
2025-04-03 07:46:16 -03:00
parent f1345472b5
commit 4c94a51a9d
7 changed files with 171 additions and 134 deletions
-51
View File
@@ -53,7 +53,6 @@ import jwt from 'jsonwebtoken';
import { LanguageEnum } from 'src/utils/languages.enum';
import { Language } from 'src/decorators/language.decorator';
import { ApiInternalOnlyEndpoint } from 'src/decorators/swagger.decorator';
import { CreatedIdentityProvider, CreateIdentityProvider } from './dtos/openid';
@ApiTags('Auth')
@ApiHeaders([{ name: 'dadosfera-lang', enum: LanguageEnum, required: false }])
@@ -111,56 +110,6 @@ export class AuthController {
return this.authClient.refreshAccessToken({ refreshToken }, metadata);
}
@Post('/idp')
@HttpCode(HttpStatus.OK)
@ApiOkResponse({ type: CreatedIdentityProvider })
async addIdentityProvider(
@User() user: RequestUser,
@Body() body: IdProvider,
@Language() language: LanguageEnum,
) {
this.logger.info('/auth - RefreshAccessToken');
const metadata = PackTheMetadata({
customer_id: user.customer_id,
language,
});
return this.authClient.createIdentityProvider(body, metadata);
}
@Get('/idp/:name')
@HttpCode(HttpStatus.OK)
@Redirect()
async loginIdp(
@Body() body: { name: string },
@Req() req: Request,
) {
this.logger.info('/auth - RefreshAccessToken');
const redirectUrl = this.authClient.loginIdp(body.name, req);
return {
url: redirectUrl,
};
}
@Get('/idp/:name/callback')
@HttpCode(HttpStatus.OK)
@ApiOkResponse({ type: AuthRefreshAccessTokenRes })
@Redirect()
@RequireAllPermissions(PERMISSIONS_GROUPS.SNOWFLAKE.permissions.OPEN)
async callbackIdp(
@Param('name') name: string,
@Req() req: Request,
) {
this.logger.info('/auth - RefreshAccessToken');
const redirectUrl = this.authClient.authorizeIdp(name, req);
return {
url: redirectUrl,
};
}
@ApiInternalOnlyEndpoint()
@Post('/sso/snowflake')
@RequireAllPermissions(PERMISSIONS_GROUPS.SNOWFLAKE.permissions.OPEN)
+1 -82
View File
@@ -17,12 +17,10 @@ import {
AuthResetPasswordRequest,
AuthVerifyResetPasswordCodeRequest,
AuthConfirmResetPasswordRequest,
IdProvider,
} from '@dadosfera/protospack-v2/dist/lib/Duc/interfaces/messages';
import { DucClient } from '../duc/client.config';
import { Metadata } from '@grpc/grpc-js';
import * as client from 'openid-client'
import { Request } from 'express';
@Injectable()
export class AuthClientService implements OnModuleInit {
@@ -180,84 +178,5 @@ export class AuthClientService implements OnModuleInit {
);
}
createIdentityProvider(body: IdProvider, metadata: Metadata) {
return lastValueFrom(
this.identityProviderService.CreateIdentityProvider(body, metadata),
);
}
async loginIdp(name: string, req: Request) {
const idp = await lastValueFrom(
this.identityProviderService.FindIdentityProvider({ name }),
);
const config: client.Configuration = await client.discovery(
new URL(idp.issueUrl),
idp.clientId,
idp.clientSecret,
)
const redirect_uri = `http://localhost:3000/auth/idp/${name}/callback`;
const scope = 'openid email';
const code_verifier: string = client.randomPKCECodeVerifier()
const code_challenge: string =
await client.calculatePKCECodeChallenge(code_verifier)
let state: string;
const parameters: Record<string, string> = {
redirect_uri,
scope,
code_challenge,
code_challenge_method: 'S256',
}
req.session.code_verifier = code_verifier;
if (!config.serverMetadata().supportsPKCE()) {
/**
* We cannot be sure the server supports PKCE so we're going to use state too.
* Use of PKCE is backwards compatible even if the AS doesn't support it which
* is why we're using it regardless. Like PKCE, random state must be generated
* for every redirect to the authorization_endpoint.
*/
state = client.randomState()
parameters.state = state
req.session.state = state;
}
const authUrl = client.buildAuthorizationUrl(config, parameters);
return authUrl;
}
async authorizeIdp(name: string, req: Request) {
const code_verifier_session = req.session.code_verifier;
const state_session = req.session.state;
const url = new URL("http://localhost:3000" + req.url);
const idp = await lastValueFrom(
this.identityProviderService.FindIdentityProvider({ name }),
);
const config: client.Configuration = await client.discovery(
new URL(idp.issueUrl),
idp.clientId,
idp.clientSecret,
)
const tokens: client.TokenEndpointResponse = await client.authorizationCodeGrant(
config,
url,
{
pkceCodeVerifier: code_verifier_session,
expectedState: state_session,
},
)
delete req.session.code_verifier;
delete req.session.state;
console.log('Token Endpoint Response', tokens)
return tokens;
}
}
+73
View File
@@ -0,0 +1,73 @@
import DadosferaLogger from '@dadosfera/dadosfera-logs';
import {
Body,
Controller,
Get,
HttpCode,
HttpStatus,
Inject,
Param,
Post,
Redirect,
Req,
} from '@nestjs/common';
import { OpenidService } from './openid.service';
import { RequestUser, User } from 'src/decorators/user.decorator';
import { Language } from 'src/decorators/language.decorator';
import { LanguageEnum } from 'src/utils/languages.enum';
import { PackTheMetadata } from 'src/utils/ PackTheMetadata';
import { ApiOkResponse } from '@nestjs/swagger';
import { CreatedIdentityProvider } from './dto/openid';
import { Request } from 'express';
@Controller('openid')
export class OpenidController {
logger: DadosferaLogger;
constructor(
@Inject(DadosferaLogger)
dadosferaLogger: DadosferaLogger,
private openidService: OpenidService,
) {
this.logger = dadosferaLogger.logger;
}
@Post()
@HttpCode(HttpStatus.OK)
@ApiOkResponse({ type: CreatedIdentityProvider })
async addIdentityProvider(
@User() user: RequestUser,
@Body() body: CreatedIdentityProvider,
@Language() language: LanguageEnum,
) {
this.logger.info('/openid - addIdentityProvider');
const metadata = PackTheMetadata({
customer_id: user.customer_id,
language,
});
return await this.openidService.createIdentityProvider(body, metadata);
}
@Get(':name')
@HttpCode(HttpStatus.OK)
@Redirect()
async loginIdp(@Param('name') name: string , @Req() req: Request) {
this.logger.info('/openid - :name');
const redirectUrl = await this.openidService.loginIdp(name, req);
return {
url: redirectUrl,
};
}
@Get('/:name/callback')
@HttpCode(HttpStatus.OK)
async callbackIdp(@Param('name') name: string, @Req() req: Request) {
this.logger.info('/openid - /:name/callback');
return await this.openidService.authorizeIdp(name, req);
}
}
+15
View File
@@ -0,0 +1,15 @@
import { Module } from '@nestjs/common';
import { OpenidController } from './openid.controller';
import { OpenidService } from './openid.service';
import { ClientsModule } from '@nestjs/microservices';
import { DucClient } from '../duc/client.config';
import DadosferaLogger from '@dadosfera/dadosfera-logs';
const client = new DucClient();
@Module({
imports: [ClientsModule.register([client.providerOptions])],
controllers: [OpenidController],
providers: [OpenidService, DadosferaLogger]
})
export class OpenidModule {}
+79
View File
@@ -0,0 +1,79 @@
import { Inject, Injectable, OnModuleInit } from '@nestjs/common';
import { DucClient } from '../duc/client.config';
import { ClientGrpc } from '@nestjs/microservices';
import { ProtoServices } from '@dadosfera/protospack-v2/dist/lib/Duc';
import { IdentityProviderProtoService } from '@dadosfera/protospack-v2/dist/lib/Duc/interfaces/write-service';
import { lastValueFrom } from 'rxjs';
import { IdProvider } from '@dadosfera/protospack-v2/dist/lib/Duc/interfaces/messages';
import { Metadata } from '@grpc/grpc-js';
import { Issuer, generators } from 'openid-client';
import { Request } from 'express';
@Injectable()
export class OpenidService implements OnModuleInit {
private identityProviderService: IdentityProviderProtoService;
constructor(
@Inject(DucClient.name) private readonly grpcClient: ClientGrpc,
) {}
onModuleInit() {
this.identityProviderService =
this.grpcClient.getService<IdentityProviderProtoService>(
ProtoServices.IdentityProviderProtoService,
);
}
async createIdentityProvider(body: IdProvider, metadata: Metadata) {
return await lastValueFrom(
this.identityProviderService.CreateIdentityProvider(body, metadata),
);
}
async loginIdp(name: string, req: Request) {
const client = await this.buildOpenIdClient(name);
const code_verifier: string = generators.codeVerifier();
const code_challenge: string = generators.codeChallenge(code_verifier);
req.session.code_verifier = code_verifier;
return client.authorizationUrl({
scope: 'openid email',
response_type: 'code',
code_challenge,
code_challenge_method: 'S256',
});
}
async authorizeIdp(name: string, req: Request) {
const client = await this.buildOpenIdClient(name);
const params = client.callbackParams(req);
const code_verifier = req.session.code_verifier;
const tokenSet = await client.callback(
`http://localhost:3333/openid/${name}/callback`,
params,
{ code_verifier },
);
delete req.session.code_verifier;
console.log('received and validated tokens %j', tokenSet);
console.log('validated ID Token claims %j', tokenSet.claims());
return tokenSet.claims();
}
private async buildOpenIdClient(name: string) {
const idp = await lastValueFrom(
this.identityProviderService.FindIdentityProvider({ name }),
);
const issuer = await Issuer.discover(idp.issueUrl);
return new issuer.Client({
client_id: idp.clientId,
client_secret: idp.clientSecret,
redirect_uris: [`http://localhost:3333/openid/${idp.name}/callback`],
response_types: ['code'],
});
}
}