Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Organization client credentials flow #478

Merged
merged 20 commits into from
Feb 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
e391784
WIP for decode jwt access token.
nishad-ayanworks Jan 4, 2024
a36cd4e
Worked on POST API of creating client credentials
nishad-ayanworks Jan 12, 2024
e4aa892
Merge branch 'develop' of https://github.com/credebl/platform into cl…
nishad-ayanworks Jan 12, 2024
6e4753d
worked on POST API of client token
nishad-ayanworks Jan 16, 2024
d5d2186
wip user data migration to keycloak
nishad-ayanworks Jan 16, 2024
9d30de3
worked on the GET API to migrate supabase users to keycloak
nishad-ayanworks Jan 29, 2024
6b26e3e
worked on the DELETE API of organization client credentials
nishad-ayanworks Jan 29, 2024
f66912c
worked on the DELETE API refactoring
nishad-ayanworks Jan 30, 2024
0fe85c0
worked on user registration on keycloak
nishad-ayanworks Jan 31, 2024
29fbed2
worked on the user reset password
nishad-ayanworks Feb 1, 2024
956c465
refactored reset password API
nishad-ayanworks Feb 2, 2024
cc5871e
removed unnecessary functions related to keycloak
nishad-ayanworks Feb 2, 2024
3ea2377
refactor reset password function
nishad-ayanworks Feb 2, 2024
09814e6
Merge branch 'develop' of https://github.com/credebl/platform into cl…
nishad-ayanworks Feb 2, 2024
c2ea38b
removed unused imports in user service
nishad-ayanworks Feb 2, 2024
c39cfd7
checked old and new password condition in reset
nishad-ayanworks Feb 2, 2024
60ba427
refactored the message while authentication
nishad-ayanworks Feb 2, 2024
b27c519
Merge branch 'develop' of https://github.com/credebl/platform into cl…
nishad-ayanworks Feb 5, 2024
9af1a3d
Merge branch 'develop' of https://github.com/credebl/platform into cl…
nishad-ayanworks Feb 5, 2024
2ca5b59
Created single migration file for the keycloak client credential setup
nishad-ayanworks Feb 6, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions apps/api-gateway/src/authz/authz.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { AuthTokenResponse } from './dtos/auth-token-res.dto';
import { LoginUserDto } from '../user/dto/login-user.dto';
import { AddUserDetailsDto } from '../user/dto/add-user.dto';
import { CustomExceptionFilter } from 'apps/api-gateway/common/exception-handler';
import { ResetPasswordDto } from './dtos/reset-password.dto';


@Controller('auth')
Expand Down Expand Up @@ -100,6 +101,7 @@ export class AuthzController {

if (loginUserDto.email) {
const userData = await this.authzService.login(loginUserDto.email, loginUserDto.password, loginUserDto.isPasskey);

const finalResponse: IResponseType = {
statusCode: HttpStatus.OK,
message: ResponseMessages.user.success.login,
Expand All @@ -112,4 +114,23 @@ export class AuthzController {
}
}

@Post('/reset-password')
@ApiOperation({
summary: 'Reset password',
description: 'Reset Password of the user'
})
@ApiResponse({ status: HttpStatus.OK, description: 'Success', type: ApiResponseDto })
async resetPassword(@Body() resetPasswordDto: ResetPasswordDto, @Res() res: Response): Promise<Response> {

const userData = await this.authzService.resetPassword(resetPasswordDto);
const finalResponse: IResponseType = {
statusCode: HttpStatus.OK,
message: ResponseMessages.user.success.resetPassword,
data: userData
};

return res.status(HttpStatus.OK).json(finalResponse);

}

}
4 changes: 3 additions & 1 deletion apps/api-gateway/src/authz/authz.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { UserService } from '../user/user.service';
import { VerificationService } from '../verification/verification.service';
import { EcosystemService } from '../ecosystem/ecosystem.service';
import { getNatsOptions } from '@credebl/common/nats.config';
import { OrganizationService } from '../organization/organization.service';

@Module({
imports: [
Expand Down Expand Up @@ -47,7 +48,8 @@ import { getNatsOptions } from '@credebl/common/nats.config';
CommonService,
UserService,
SupabaseService,
EcosystemService
EcosystemService,
OrganizationService
],
exports: [
PassportModule,
Expand Down
7 changes: 6 additions & 1 deletion apps/api-gateway/src/authz/authz.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ import {
import { UserEmailVerificationDto } from '../user/dto/create-user.dto';
import { EmailVerificationDto } from '../user/dto/email-verify.dto';
import { AddUserDetailsDto } from '../user/dto/add-user.dto';
import { ISendVerificationEmail, ISignInUser, IVerifyUserEmail } from '@credebl/common/interfaces/user.interface';
import { IResetPasswordResponse, ISendVerificationEmail, ISignInUser, IVerifyUserEmail } from '@credebl/common/interfaces/user.interface';
import { ResetPasswordDto } from './dtos/reset-password.dto';

@Injectable()
@WebSocketGateway()
Expand Down Expand Up @@ -43,6 +44,10 @@ export class AuthzService extends BaseService {
return this.sendNatsMessage(this.authServiceProxy, 'user-holder-login', payload);
}

async resetPassword(resetPasswordDto: ResetPasswordDto): Promise<IResetPasswordResponse> {
return this.sendNatsMessage(this.authServiceProxy, 'user-reset-password', resetPasswordDto);
}

async addUserDetails(userInfo: AddUserDetailsDto): Promise<string> {
const payload = { userInfo };
return this.sendNatsMessage(this.authServiceProxy, 'add-user', payload);
Expand Down
25 changes: 25 additions & 0 deletions apps/api-gateway/src/authz/dtos/reset-password.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { IsEmail, IsNotEmpty, IsString } from 'class-validator';

import { ApiProperty } from '@nestjs/swagger';
import { Transform } from 'class-transformer';
import { trim } from '@credebl/common/cast.helper';

export class ResetPasswordDto {
@ApiProperty({ example: 'awqx@getnada.com' })
@IsEmail({}, { message: 'Please provide a valid email' })
@IsNotEmpty({ message: 'Email is required' })
@IsString({ message: 'Email should be a string' })
@Transform(({ value }) => trim(value))
email: string;

@ApiProperty()
@Transform(({ value }) => trim(value))
@IsNotEmpty({ message: 'oldPassword is required.' })
oldPassword: string;

@ApiProperty()
@Transform(({ value }) => trim(value))
@IsNotEmpty({ message: 'newPassword is required.' })
newPassword?: string;

}
2 changes: 1 addition & 1 deletion apps/api-gateway/src/authz/guards/org-roles.guard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ export class OrgRolesGuard implements CanActivate {
}
return orgDetails.orgId.toString().trim() === orgId.toString().trim();
});

if (!specificOrg) {
throw new ForbiddenException(ResponseMessages.organisation.error.orgNotMatch, { cause: new Error(), description: ResponseMessages.errorMessages.forbidden });
}
Expand Down
73 changes: 63 additions & 10 deletions apps/api-gateway/src/authz/jwt.strategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,30 +7,83 @@ import { JwtPayload } from './jwt-payload.interface';
import { NotFoundException } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { UserService } from '../user/user.service';
import * as jwt from 'jsonwebtoken';
import { passportJwtSecret } from 'jwks-rsa';
import { CommonConstants } from '@credebl/common/common.constant';
import { OrganizationService } from '../organization/organization.service';
import { IOrganization } from '@credebl/common/interfaces/organization.interface';
import { ResponseMessages } from '@credebl/common/response-messages';

dotenv.config();

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
private readonly logger = new Logger();
private readonly logger = new Logger('Jwt Strategy');

constructor(
private readonly usersService: UserService
) {
private readonly usersService: UserService,
private readonly organizationService: OrganizationService
) {

super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: process.env.SUPABASE_JWT_SECRET,
ignoreExpiration: false
});
secretOrKeyProvider: (request, jwtToken, done) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const decodedToken: any = jwt.decode(jwtToken);

const audiance = decodedToken.iss.toString();
const jwtOptions = {
cache: true,
rateLimit: true,
jwksRequestsPerMinute: 5,
jwksUri: `${audiance}${CommonConstants.URL_KEYCLOAK_JWKS}`
};
const secretprovider = passportJwtSecret(jwtOptions);
let certkey;
secretprovider(request, jwtToken, async (err, data) => {
certkey = data;
done(null, certkey);
});
},
algorithms: ['RS256']
});
}

async validate(payload: JwtPayload): Promise<object> {

const userDetails = await this.usersService.findUserinSupabase(payload.sub);

let userDetails = null;

if (payload.hasOwnProperty('clientId')) {
const orgDetails: IOrganization = await this.organizationService.findOrganizationOwner(payload['clientId']);

this.logger.log('Organization details fetched');
if (!orgDetails) {
throw new NotFoundException(ResponseMessages.organisation.error.orgNotFound);
}

// eslint-disable-next-line prefer-destructuring
const userOrgDetails = 0 < orgDetails.userOrgRoles.length && orgDetails.userOrgRoles[0];

userDetails = userOrgDetails.user;
userDetails.userOrgRoles = [];
userDetails.userOrgRoles.push({
id: userOrgDetails.id,
userId: userOrgDetails.userId,
orgRoleId: userOrgDetails.orgRoleId,
orgId: userOrgDetails.orgId,
orgRole: userOrgDetails.orgRole
});

this.logger.log('User details set');

} else {
userDetails = await this.usersService.findUserinKeycloak(payload.sub);
}

if (!userDetails) {
throw new NotFoundException('User not found');
throw new NotFoundException(ResponseMessages.user.error.notFound);
}

return {
...userDetails,
...payload
Expand Down
18 changes: 18 additions & 0 deletions apps/api-gateway/src/organization/dtos/client-credentials.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { ApiExtraModels, ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty, IsString } from 'class-validator';

import { Transform } from 'class-transformer';
import { trim } from '@credebl/common/cast.helper';

@ApiExtraModels()
export class ClientCredentialsDto {

clientId: string;

@ApiProperty()
@Transform(({ value }) => trim(value))
@IsNotEmpty({ message: 'clientSecret is required.' })
@IsString({ message: 'clientSecret must be in string format.' })
clientSecret: string;

}
77 changes: 77 additions & 0 deletions apps/api-gateway/src/organization/organization.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { UpdateOrganizationDto } from './dtos/update-organization-dto';
import { CustomExceptionFilter } from 'apps/api-gateway/common/exception-handler';
import { IUserRequestInterface } from '../interfaces/IUserRequestInterface';
import { ImageServiceService } from '@credebl/image-service';
import { ClientCredentialsDto } from './dtos/client-credentials.dto';
import { PaginationDto } from '@credebl/common/dtos/pagination.dto';
import { validate as isValidUUID } from 'uuid';

Expand Down Expand Up @@ -265,6 +266,22 @@ export class OrganizationController {

}

@Get('/:orgId/client_credentials')
@Roles(OrgRoles.OWNER, OrgRoles.ADMIN, OrgRoles.ISSUER, OrgRoles.VERIFIER, OrgRoles.MEMBER)
@ApiOperation({ summary: 'Fetch client credentials for an organization', description: 'Fetch client id and secret for an organization' })
@ApiResponse({ status: HttpStatus.OK, description: 'Success', type: ApiResponseDto })
@UseGuards(AuthGuard('jwt'), OrgRolesGuard)
@ApiBearerAuth()
async fetchOrgCredentials(@Param('orgId') orgId: string, @Res() res: Response, @User() reqUser: user): Promise<Response> {
const orgCredentials = await this.organizationService.fetchOrgCredentials(orgId, reqUser.id);
const finalResponse: IResponse = {
statusCode: HttpStatus.OK,
message: ResponseMessages.organisation.success.fetchedOrgCredentials,
data: orgCredentials
};
return res.status(HttpStatus.OK).json(finalResponse);
}

/**
* @returns Users list of organization
*/
Expand Down Expand Up @@ -319,6 +336,51 @@ export class OrganizationController {
return res.status(HttpStatus.CREATED).json(finalResponse);
}

/**
*
* @param orgId
* @param res
* @param reqUser
* @returns Organization Client Credentials
*/
@Post('/:orgId/client_credentials')
@Roles(OrgRoles.OWNER)
@ApiOperation({ summary: 'Create credentials for an organization', description: 'Create client id and secret for an organization' })
@ApiResponse({ status: HttpStatus.CREATED, description: 'Success', type: ApiResponseDto })
@UseGuards(AuthGuard('jwt'), OrgRolesGuard)
@ApiBearerAuth()
async createOrgCredentials(@Param('orgId') orgId: string, @Res() res: Response, @User() reqUser: user): Promise<Response> {
const orgCredentials = await this.organizationService.createOrgCredentials(orgId, reqUser.id);
const finalResponse: IResponse = {
statusCode: HttpStatus.CREATED,
message: ResponseMessages.organisation.success.orgCredentials,
data: orgCredentials
};
return res.status(HttpStatus.CREATED).json(finalResponse);
}

@Post('/:clientId/token')
@ApiOperation({ summary: 'Authenticate client for credentials', description: 'Authenticate client for credentials' })
@ApiResponse({ status: HttpStatus.OK, description: 'Success', type: ApiResponseDto })
async clientLoginCredentials(
@Param('clientId') clientId: string,
@Body() clientCredentialsDto: ClientCredentialsDto,
@Res() res: Response): Promise<Response> {
clientCredentialsDto.clientId = clientId.trim();

if (!clientCredentialsDto.clientId) {
throw new BadRequestException(ResponseMessages.organisation.error.clientIdRequired);
}

const orgCredentials = await this.organizationService.clientLoginCredentials(clientCredentialsDto);
const finalResponse: IResponse = {
statusCode: HttpStatus.OK,
message: ResponseMessages.organisation.success.clientCredentials,
data: orgCredentials
};
return res.status(HttpStatus.OK).json(finalResponse);
}

@Post('/:orgId/invitations')
@ApiOperation({
summary: 'Create organization invitation',
Expand Down Expand Up @@ -416,6 +478,21 @@ export class OrganizationController {
return res.status(HttpStatus.ACCEPTED).json(finalResponse);
}

@Delete('/:orgId/client_credentials')
@ApiOperation({ summary: 'Delete Organization Client Credentials', description: 'Delete Organization Client Credentials' })
@ApiResponse({ status: HttpStatus.OK, description: 'Success', type: ApiResponseDto })
@ApiBearerAuth()
@UseGuards(AuthGuard('jwt'))
async deleteOrgClientCredentials(@Param('orgId') orgId: string, @Res() res: Response): Promise<Response> {

const deleteResponse = await this.organizationService.deleteOrgClientCredentials(orgId);

const finalResponse: IResponse = {
statusCode: HttpStatus.ACCEPTED,
message: deleteResponse
};
return res.status(HttpStatus.ACCEPTED).json(finalResponse);
}

@Delete('/:orgId/invitations/:invitationId')
@ApiOperation({ summary: 'Delete organization invitation', description: 'Delete organization invitation' })
Expand Down
Loading