FEAT: create user from identity provider

This commit is contained in:
marcos-silva-rodrigues
2025-06-27 13:21:59 -03:00
parent 57e6e49c7c
commit f726a9883f
10 changed files with 436 additions and 110 deletions
-17
View File
@@ -421,21 +421,4 @@ export class AuthController {
return this.authClient.resetUsers(body.users, metadata);
}
@Get('/idp/:name/callback')
@HttpCode(HttpStatus.OK)
async callbackIdp(@Param('name') idpName: string, @Req() req: Request) {
this.logger.info('/auth/idp/:name/callback');
const { params } = req;
if (!params || !params.code) {
this.logger.error('No code received from IDP');
throw new Error(ErrorCodes.IDENTITY_PROVIDER.INVALID_RESPONSE);
}
return {
ok: true
}
}
}
@@ -0,0 +1,9 @@
export class SsoSignInDto {
readonly nonce: string;
readonly codeVerifier: string;
readonly state: string;
readonly id: string;
readonly clientId: string;
readonly clientSecret: string;
readonly issuerUrl: string;
}
@@ -19,6 +19,8 @@ import { PackTheMetadata } from 'src/utils/ PackTheMetadata';
import { ApiOkResponse } from '@nestjs/swagger';
import { CreateIdentityProvider, IdentityProviderListResponse, IdentityProviderResponse } from './dto/identity-provider.dto';
import { Request } from 'express';
import ErrorCodes from 'src/utils/errorCodes';
import { Authenticated } from 'src/decorators/authentication.decorator';
@Controller('identity-providers')
export class IdentityProviderController {
@@ -35,6 +37,7 @@ export class IdentityProviderController {
@Post()
@HttpCode(HttpStatus.OK)
@ApiOkResponse({ type: IdentityProviderResponse })
@Authenticated()
async addIdentityProvider(
@User() user: RequestUser,
@Body() body: CreateIdentityProvider,
@@ -53,6 +56,7 @@ export class IdentityProviderController {
@Get()
@HttpCode(HttpStatus.OK)
@ApiOkResponse({ type: IdentityProviderListResponse })
@Authenticated()
async getProviders(
@User() user: RequestUser,
@Language() language: LanguageEnum,
@@ -63,7 +67,27 @@ export class IdentityProviderController {
language
})
return await this.identityProviderService.getList(metadata)
const result = await this.identityProviderService.getList(metadata)
return result
}
@Get('/callback')
@HttpCode(HttpStatus.OK)
async callbackIdp(@Req() req: Request) {
this.logger.info('/callback');
const { code, state } = req.query;
if (!code) {
this.logger.error('No code received from IDP');
throw new Error(ErrorCodes.IDENTITY_PROVIDER.INVALID_RESPONSE);
}
if (!state) {
this.logger.error('No state received from IDP');
throw new Error(ErrorCodes.IDENTITY_PROVIDER.INVALID_RESPONSE);
}
return await this.identityProviderService.getTokenByIdp(req);
}
@Get(':id')
@@ -78,14 +102,4 @@ export class IdentityProviderController {
};
}
@Get('/:name/callback')
@HttpCode(HttpStatus.OK)
async callbackIdp(@Param('name') name: string, @Req() req: Request) {
this.logger.info('/openid - /:name/callback');
return {
name,
params: req.params
};
}
}
@@ -4,12 +4,13 @@ import { IdentityProviderService } from './identity-provider.service';
import { ClientsModule } from '@nestjs/microservices';
import { DucClient } from '../duc/client.config';
import DadosferaLogger from '@dadosfera/dadosfera-logs';
import { ServicesModule } from 'src/services/service.module';
const client = new DucClient();
@Module({
imports: [ClientsModule.register([client.providerOptions])],
imports: [ClientsModule.register([client.providerOptions]), ServicesModule],
controllers: [IdentityProviderController],
providers: [IdentityProviderService, DadosferaLogger]
providers: [IdentityProviderService, DadosferaLogger],
})
export class IdentityProviderModule {}
@@ -1,4 +1,4 @@
import { Inject, Injectable, OnModuleInit } from '@nestjs/common';
import { BadRequestException, 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';
@@ -7,6 +7,8 @@ import { lastValueFrom } from 'rxjs';
import { IdentityProviderRequest } from '@dadosfera/protospack-v2/dist/lib/Duc/interfaces/messages';
import { Metadata } from '@grpc/grpc-js';
import { Issuer, generators } from 'openid-client';
import { SsoSignInDto } from './dto/sso-signin.dto';
import { CacheService } from 'src/services/cache.service';
import { Request } from 'express';
@Injectable()
@@ -14,6 +16,7 @@ export class IdentityProviderService implements OnModuleInit {
private identityProviderService: IdentityProviderProtoService;
constructor(
@Inject(DucClient.name) private readonly grpcClient: ClientGrpc,
private readonly cacheService: CacheService<SsoSignInDto>
) {}
onModuleInit() {
@@ -37,46 +40,101 @@ export class IdentityProviderService implements OnModuleInit {
async loginIdentityProvider(id: string) {
const idp = await lastValueFrom(
this.identityProviderService.FindIdentityProvider({ id }),
this.identityProviderService.FindIdentityProvider({ id}),
);
const issuer = await Issuer.discover(idp.issuerUrl);
const redirectUri = this.getCallbackUrl();
const cognitoIssuerURL = this.getCognitoIssuerUrl();
const issuer = await Issuer.discover(cognitoIssuerURL);
const client = new issuer.Client({
client_id: idp.clientId,
client_secret: idp.clientSecret,
redirect_uris: [`http://localhost:3333/auth/idp/${idp.name}/callback`],
redirect_uris: [redirectUri],
response_types: ['code'],
});
const code_verifier: string = generators.codeVerifier();
const code_challenge: string = generators.codeChallenge(code_verifier);
const state = generators.state();
const nonce = generators.nonce();
// Using state because it is returned in the callback
// and we can use it to retrieve the code_verifier and nonce
await this.cacheService.set(state, {
codeVerifier: code_verifier,
nonce,
id: idp.id,
state,
clientId: idp.clientId,
clientSecret: idp.clientSecret,
issuerUrl: cognitoIssuerURL,
});
const url = client.authorizationUrl({
scope: 'openid email profile',
scope: 'openid email',
response_type: 'code',
code_challenge,
code_challenge_method: 'S256',
state,
nonce
});
const idpUrl = url + '&identity_provider=' + idp.name
return idpUrl;
}
// 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 },
// );
private getCallbackUrl() {
const AUTH_CALLBACK = process.env.AUTH_CALLBACK;
return AUTH_CALLBACK;
}
// delete req.session.code_verifier;
private getCognitoIssuerUrl() {
const COGNITO_ISSUER_URL = process.env.COGNITO_ISSUER_URL;
return COGNITO_ISSUER_URL;
}
// console.log('received and validated tokens %j', tokenSet);
// console.log('validated ID Token claims %j', tokenSet.claims());
async getTokenByIdp(req: Request) {
const ssoSign = await this.cacheService.get(req.query.state as string);
// return tokenSet.claims();
// }
if (!ssoSign) {
throw new BadRequestException('SSO sign-in is expired or not found');
}
const callbackUrl = this.getCallbackUrl();
const issuer = await Issuer.discover(ssoSign.issuerUrl);
const client = new issuer.Client({
client_id: ssoSign.clientId,
client_secret: ssoSign.clientSecret,
redirect_uris: [callbackUrl],
response_types: ['code'],
});
if (ssoSign === null) {
throw new BadRequestException('SSO sign-in is expired or not found');
}
const params = client.callbackParams(req)
const tokenSet = await client.callback(
callbackUrl,
params,
{
nonce: ssoSign.nonce,
code_verifier: ssoSign.codeVerifier,
state: ssoSign.state,
}
);
await this.cacheService.delete(ssoSign.state);
return await lastValueFrom(
this.identityProviderService.SignInUser({
accessToken: tokenSet.access_token,
idToken: tokenSet.id_token,
refreshToken: tokenSet.refresh_token,
id: ssoSign.id
}),
);
}
}