Skip to content
26 changes: 24 additions & 2 deletions @core/cms/src/app.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,24 @@ import { CreateCredentialDto } from "./database-integration/dto/create-credentia
import { ValidateCredentialPipe } from "./common/pipes/validate-credential.pipe";
import { ExternalAuthIntegrationService } from "./external-auth-integration/external-auth-integration.service";

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change

import { BasicAuthDto } from "./dto/basic-auth.dto";
import { BasicAuthVerifyDto } from "./dto/basic-auth-verify.dto";

@Controller()
export class AppController {
constructor(
private databaseIntegrationService: DatabaseIntegrationService,
private encryptionDecryptionService: EncryptionDecryptionService,
private externalAuthIntegrationService: ExternalAuthIntegrationService,
) {}

private readonly logger = new Logger(AppController.name);

// TODO: Add DTO
@MessagePattern("create_credential")
@UsePipes(new ValidateCredentialPipe())
async handleCreateCredential(message: CreateCredentialDto) {
Logger.log("Received data:", message, "AppController");
this.logger.log(`Received data: ${message}`);
return this.databaseIntegrationService.createCredential(message);
}

Expand Down Expand Up @@ -58,7 +64,7 @@ export class AppController {
const token = message.token;
const providerKey = message.providerKey;
const data = await this.externalAuthIntegrationService.verifyToken(providerKey, token);
Logger.log(`Received data: ${JSON.stringify(data)}`, "AppController");
this.logger.log(`Received data: ${JSON.stringify(data)}`);
return data;
}

Expand All @@ -70,4 +76,20 @@ export class AppController {
const data = await this.externalAuthIntegrationService.refreshToken(providerKey, refreshToken);
return data;
}

@MessagePattern("basic_auth")
async handleBasicAuth(message: BasicAuthDto) {
// TODO: Add JWT

const providerKey = message.providerKey;
return await this.externalAuthIntegrationService.authenticate(providerKey);
}

@MessagePattern("basic_auth_verify")
async handleBasicAuthVerify(message: BasicAuthVerifyDto) {
// TODO: Add JWT

const providerKey = message.providerKey;
return await this.externalAuthIntegrationService.verifyBasic(providerKey);
}
}
17 changes: 14 additions & 3 deletions @core/cms/src/config/allowedCredentials.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,15 @@
{
"Google": ["OAuth2", "APIKey"],
"Facebook": ["OAuth2"]
}
"Google": [
"OAuth2",
"APIKey"
],
"Facebook": [
"OAuth2"
],
"GitHub": [
"OAuth2"
],
"Mixpanel": [
"Basic"
]
}
5 changes: 5 additions & 0 deletions @core/cms/src/dto/basic-auth-verify.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { type BasicAuthString } from "~/external-auth-integration/auth-providers/basic-auth/@types";

export class BasicAuthVerifyDto {
providerKey: BasicAuthString;
}
5 changes: 5 additions & 0 deletions @core/cms/src/dto/basic-auth.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { type BasicAuthString } from "~/external-auth-integration/auth-providers/basic-auth/@types";

export class BasicAuthDto {
providerKey: BasicAuthString;
}
7 changes: 7 additions & 0 deletions @core/cms/src/external-auth-integration/@types/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { type OAuthString } from "../auth-providers/oauth2/@types";
import { type BasicAuthString } from "../auth-providers/basic-auth/@types";
import { IOAuth } from "../auth-providers/oauth2/interface/ioauth.interface";
import { IBasicAuth } from "../auth-providers/basic-auth/interface/basic-auth.interface";

export type AuthString = OAuthString | BasicAuthString;
export type AuthService = IOAuth | IBasicAuth;
Original file line number Diff line number Diff line change
@@ -1,21 +1,14 @@
import {
Controller,
Get,
Logger,
Query,
Res,
HttpException,
HttpStatus,
} from "@nestjs/common";
import { Controller, Get, Logger, Query, Res, HttpException, HttpStatus } from "@nestjs/common";
import { AuthProviderFactory } from "./auth-provider.factory";
import { EncryptionDecryptionService } from "~/encryption-decryption/encryption-decryption.service";
import { decodeBase64UrlToString } from "./utils";
import { type OAuthString } from "./auth-providers/oauth2/@types";

@Controller("oauth")
export class AuthCallbackController {
constructor(
private providerFactory: AuthProviderFactory,
private encryptionDecryptionService: EncryptionDecryptionService
private encryptionDecryptionService: EncryptionDecryptionService,
) {}

/**
Expand All @@ -28,38 +21,27 @@ export class AuthCallbackController {
async handleCallback(@Query() query: any, @Res() res: Response) {
if (!query.state) {
Logger.error("No state received", "AuthCallbackController");
throw new HttpException(
"State parameter is missing",
HttpStatus.BAD_REQUEST
);
throw new HttpException("State parameter is missing", HttpStatus.BAD_REQUEST);
}

try {
const encryptedState = decodeBase64UrlToString(query.state);
const decryptedState =
await this.encryptionDecryptionService.decryptData(encryptedState);
const decryptedState = await this.encryptionDecryptionService.decryptData(encryptedState);
const state = JSON.parse(decryptedState);

if (
!state.providerInfo ||
!state.providerInfo.provider ||
!state.providerInfo.version
) {
if (!state.providerInfo || !state.providerInfo.provider || !state.providerInfo.version) {
throw new Error("Invalid state format");
}

// TODO: Verify user JWT

const key = `oauth-${state.providerInfo.provider}-${state.providerInfo.version}`;
Logger.log(`Calling oauth callback for provider ${state.providerInfo.provider} ${state.providerInfo.version}`)
const key: OAuthString = `oauth-${state.providerInfo.provider}-${state.providerInfo.version}`;
Logger.log(`Calling oauth callback for provider ${state.providerInfo.provider} ${state.providerInfo.version}`);
const providerService = this.providerFactory.getProvider(key);
await providerService.handleCallback(query, res);
} catch (error) {
Logger.error(error.message, "AuthCallbackController");
throw new HttpException(
"Error processing the OAuth callback",
HttpStatus.INTERNAL_SERVER_ERROR
);
throw new HttpException("Error processing the OAuth callback", HttpStatus.INTERNAL_SERVER_ERROR);
}
}
}
31 changes: 26 additions & 5 deletions @core/cms/src/external-auth-integration/auth-provider.factory.ts
Original file line number Diff line number Diff line change
@@ -1,34 +1,55 @@
import { Injectable } from "@nestjs/common";
import { IOAuth } from "./auth-providers/oauth2/interface/ioauth.interface";
import type { AuthString, AuthService } from "./@types";
import { GoogleV2OAuth2Service } from "./auth-providers/oauth2/google/v2/google.v2.service";
import { GithubV1OAuth2Service } from "./auth-providers/oauth2/github/v1/github.v1.service";
import { AirtableV1OAuth2Service } from "./auth-providers/oauth2/airtable/v1/airtable.v1.service";
import { SlackV2OAuth2Service } from "./auth-providers/oauth2/slack/v2/slack.v2.service";
import { MixpanelV1BasicAuthService } from "./auth-providers/basic-auth/mixpanel/v1/mixpanel.v1.service";
import { type OAuthString } from "./auth-providers/oauth2/@types";
import { IOAuth } from "./auth-providers/oauth2/interface/ioauth.interface";
import { type BasicAuthString } from "./auth-providers/basic-auth/@types";
import { IBasicAuth } from "./auth-providers/basic-auth/interface/basic-auth.interface";

@Injectable()
export class AuthProviderFactory {
private readonly providerMap = new Map<string, IOAuth>();
// A map to hold the associations between authentication keys and their corresponding services
private readonly providerMap = new Map<AuthString, AuthService>();

constructor(
private googleV2OAuth2Service: GoogleV2OAuth2Service,
private githubV1OAuth2Service: GithubV1OAuth2Service,
private airtableV1OAuth2Service: AirtableV1OAuth2Service,
private slackV2OAuth2Service: SlackV2OAuth2Service,
private mixpanelV1BasicAuthService: MixpanelV1BasicAuthService,
) {
// Register available authentication providers
this.registerProvider("oauth-google-v2", this.googleV2OAuth2Service);
this.registerProvider("oauth-github-v1", this.githubV1OAuth2Service);
this.registerProvider("oauth-airtable-v1", this.airtableV1OAuth2Service);
this.registerProvider("oauth-slack-v2", this.slackV2OAuth2Service);

this.registerProvider("basic-mixpanel-v1", this.mixpanelV1BasicAuthService);
}

private registerProvider(key: string, provider: IOAuth) {
// Function overloading for registerProvider to handle different types of auth providers
private registerProvider(key: OAuthString, provider: IOAuth): void;
private registerProvider(key: BasicAuthString, provider: IBasicAuth): void;
private registerProvider(key: AuthString, provider: AuthService): void {
// Register a provider with a specific key in the map
this.providerMap.set(key, provider);
}

public getProvider(key: string): IOAuth {
// Function overloading for getProvider to retrieve the appropriate type of auth provider
public getProvider(key: OAuthString): IOAuth;
public getProvider(key: BasicAuthString): IBasicAuth;
public getProvider(key: AuthString): AuthService;

public getProvider(key: AuthString): AuthService {
// Retrieve the provider associated with the given key
const provider = this.providerMap.get(key);
if (!provider) {
throw new Error(`Provider not found: ${key}`);
// If the provider is not found, throw an error
throw new Error(`Authentication provider not found for key: ${key}`);
}
return provider;
}
Expand Down
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need to change comments

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The purpose of this draft PR is to check if the general idea of the implementation of basic auth is correct; all the comments and docs were pasted over from existing files and are all wrong and should be ignored. I'll add docs and tests and linting and formatting after getting confirmation that the general idea is right...

Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
export type BasicAuthString = `basic-${string}`;

export interface BasicAuthProvider {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IBasicAuthConfiguration

authenticateUrl: string;
verifyUrl?: string;
}

export interface BasicAuthConfig {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found that name in OAuth may be confused, actually BasicAuthConfig cannot store pw and username

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The pw and username are the service account username and secret; I thought that's the credential we're supposed to put in .env (like the clientId and client secret of OAuth)? The authenticate() method basically just verifies it, and the verify() method retrieves an expiration date (generally service accounts don't expire so it would be null most of the time) and some other info. We should discuss it tmr, I'm a bit confused lol

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we will store PW and username in db, actually in OAuth 2, we still store credentials in db, however the interface name ICredential in OAuth is confused, I have made a new PR to change it

provider: BasicAuthProvider;
}

export interface BasicAuthVerificationResponse {
isValid: boolean;
expiresIn?: number;
// Other fields as per your application's requirements
[key: string]: any;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Module } from "@nestjs/common";
import { HttpModule } from "@nestjs/axios";
import { EncryptionDecryptionModule } from "~/encryption-decryption/encryption-decryption.module";
import { MixpanelV1BasicAuthService } from "./mixpanel/v1/mixpanel.v1.service";
import { MixpanelV1BasicAuthConfig } from "./mixpanel/v1/mixpanel.v1.config";

@Module({
imports: [HttpModule, EncryptionDecryptionModule],
providers: [
MixpanelV1BasicAuthService,
{
provide: "MixpanelV1BasicAuthConfig",
useFactory: () => new MixpanelV1BasicAuthConfig().basicAuthConfig,
},
],
exports: [MixpanelV1BasicAuthService],
})
export class BasicAuthModule {}
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Change comment

Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export interface IBasicAuth {
authenticate(username: string, password: string): Promise<any>;

verify?(username: string, password: string): Promise<any>;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Injectable } from "@nestjs/common";
import { BasicAuthConfig } from "../../@types";

/**
* Service for configuring Mixpanel V1 Basic HTTP Authentication.
*/
@Injectable()
export class MixpanelV1BasicAuthConfig {
public basicAuthConfig: BasicAuthConfig;

constructor() {
this.basicAuthConfig = {
provider: {
authenticateUrl: "https://mixpanel.com/api/app/me",
verifyUrl: "https://mixpanel.com/api/app/organizations/{organizationId}/service-accounts",
},
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { Injectable, Inject, Logger } from "@nestjs/common";
import { HttpService } from "@nestjs/axios";
import { EncryptionDecryptionService } from "~/encryption-decryption/encryption-decryption.service";
import { lastValueFrom } from "rxjs";
import { map } from "rxjs/operators";

import { IBasicAuth } from "../../interface/basic-auth.interface";
import { BasicAuthConfig, BasicAuthVerificationResponse } from "../../@types";

/**
* Service to handle Mixpanel V1 Basic HTTP Authentication.
*/
@Injectable()
export class MixpanelV1BasicAuthService implements IBasicAuth {
constructor(
@Inject("MixpanelV1BasicAuthConfig") private readonly config: BasicAuthConfig,
private readonly httpService: HttpService,
private readonly encryptionDecryptionService: EncryptionDecryptionService,
) {}

private readonly logger = new Logger(MixpanelV1BasicAuthService.name);

async authenticate(username: string, password: string): Promise<object> {
try {
const { authenticateUrl } = this.config.provider;
const data = await lastValueFrom(
this.httpService.get(authenticateUrl, { auth: { username, password } }).pipe(map((resp) => resp.data)),
);

return {
status: data.status,
organizationId: Object.keys(data.results?.organizations).at(0),
};
} catch (error) {
this.logger.error(`Authentication failed: ${error}`);
const data = error.response?.data;
return data;
}
}

async verify(username: string, password: string): Promise<BasicAuthVerificationResponse> {
try {
const authenticationResponse = await this.authenticate(username, password);
if (!authenticationResponse["organizationId"]) {
throw new Error("Authentication failed");
}

let { verifyUrl } = this.config.provider;
if (!verifyUrl) {
throw new Error("verifyUrl not found");
}

verifyUrl = verifyUrl.replace("{organizationId}", authenticationResponse["organizationId"]);
const data = await lastValueFrom(
this.httpService.get(verifyUrl, { auth: { username, password } }).pipe(map((resp) => resp.data)),
);

return {
isValid: true,
expiresIn: data.results?.expires,
};
} catch (error) {
this.logger.error(`Service account verification failed: ${error}`);
return { isValid: false };
}
}
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
export type OAuthString = `oauth-${string}`;

/**
* Represents the client credentials for OAuth2 authentication.
* @export
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,14 @@ export class GithubV1OAuth2Service implements IOAuth {
}

const tokenResponse = await exchangeCodeForToken(this.httpService, this.config, query.code);
Logger.log("Token response", tokenResponse);
res.status(HttpStatus.OK).json(tokenResponse);

const params = new URLSearchParams(tokenResponse);
const data = {};
for (const [key, value] of params) {
data[key] = value;
}

res.status(HttpStatus.OK).json(data);
} catch (error) {
Logger.error("Error exchanging code for token", error);
throw new HttpException("Error exchanging code for token", HttpStatus.INTERNAL_SERVER_ERROR);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@ import { EncryptionDecryptionModule } from "~/encryption-decryption/encryption-d
import { AuthProviderFactory } from "./auth-provider.factory";
import { AuthCallbackController } from "./auth-callback.controller";
import { OAuth2Module } from "./auth-providers/oauth2/oauth2.module";
import { BasicAuthModule } from "./auth-providers/basic-auth/basic-auth.module";
import { ExternalAuthIntegrationService } from "./external-auth-integration.service";

@Module({
imports: [OAuth2Module, EncryptionDecryptionModule],
imports: [OAuth2Module, BasicAuthModule, EncryptionDecryptionModule],
providers: [AuthProviderFactory, ExternalAuthIntegrationService],
controllers: [AuthCallbackController],
exports: [ExternalAuthIntegrationService],
Expand Down
Loading