D gamer007/add microsoft oauth (#5103)

Need to create a new branch because original branch name is `main` and
we cannot push additional commits
Linked to https://github.com/twentyhq/twenty/pull/4718


![image](https://github.com/twentyhq/twenty/assets/29927851/52b220e7-770a-4ffe-b6e9-468605c2b8fa)

![image](https://github.com/twentyhq/twenty/assets/29927851/7a7a4737-f09f-4d9b-8962-5a9b8c71edc1)

---------

Co-authored-by: DGamer007 <prajapatidhruv266@gmail.com>
This commit is contained in:
martmull
2024-04-24 14:56:02 +02:00
committed by GitHub
parent b3e1d6becf
commit 87a9ecee28
25 changed files with 458 additions and 129 deletions

View File

@ -21,6 +21,7 @@ import { UserWorkspaceModule } from 'src/engine/core-modules/user-workspace/user
import { SignInUpService } from 'src/engine/core-modules/auth/services/sign-in-up.service';
import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
import { FileUploadModule } from 'src/engine/core-modules/file/file-upload/file-upload.module';
import { MicrosoftAuthController } from 'src/engine/core-modules/auth/controllers/microsoft-auth.controller';
import { AppTokenService } from 'src/engine/core-modules/app-token/services/app-token.service';
import { ObjectMetadataRepositoryModule } from 'src/engine/object-metadata-repository/object-metadata-repository.module';
import { ConnectedAccountObjectMetadata } from 'src/modules/connected-account/standard-objects/connected-account.object-metadata';
@ -65,6 +66,7 @@ const jwtModule = JwtModule.registerAsync({
],
controllers: [
GoogleAuthController,
MicrosoftAuthController,
GoogleAPIsAuthController,
VerifyAuthController,
],

View File

@ -0,0 +1,49 @@
import { Controller, Get, Req, Res, UseGuards } from '@nestjs/common';
import { Response } from 'express';
import { TypeORMService } from 'src/database/typeorm/typeorm.service';
import { MicrosoftOAuthGuard } from 'src/engine/core-modules/auth/guards/microsoft-oauth.guard';
import { MicrosoftProviderEnabledGuard } from 'src/engine/core-modules/auth/guards/microsoft-provider-enabled.guard';
import { AuthService } from 'src/engine/core-modules/auth/services/auth.service';
import { TokenService } from 'src/engine/core-modules/auth/services/token.service';
import { MicrosoftRequest } from 'src/engine/core-modules/auth/strategies/microsoft.auth.strategy';
@Controller('auth/microsoft')
export class MicrosoftAuthController {
constructor(
private readonly tokenService: TokenService,
private readonly typeORMService: TypeORMService,
private readonly authService: AuthService,
) {}
@Get()
@UseGuards(MicrosoftProviderEnabledGuard, MicrosoftOAuthGuard)
async microsoftAuth() {
// As this method is protected by Microsoft Auth guard, it will trigger Microsoft SSO flow
return;
}
@Get('redirect')
@UseGuards(MicrosoftProviderEnabledGuard, MicrosoftOAuthGuard)
async microsoftAuthRedirect(
@Req() req: MicrosoftRequest,
@Res() res: Response,
) {
const { firstName, lastName, email, picture, workspaceInviteHash } =
req.user;
const user = await this.authService.signInUp({
email,
firstName,
lastName,
picture,
workspaceInviteHash,
fromSSO: true,
});
const loginToken = await this.tokenService.generateLoginToken(user.email);
return res.redirect(this.tokenService.computeRedirectURI(loginToken.token));
}
}

View File

@ -16,8 +16,7 @@ export class GoogleOauthGuard extends AuthGuard('google') {
if (workspaceInviteHash && typeof workspaceInviteHash === 'string') {
request.params.workspaceInviteHash = workspaceInviteHash;
}
const activate = (await super.canActivate(context)) as boolean;
return activate;
return (await super.canActivate(context)) as boolean;
}
}

View File

@ -0,0 +1,22 @@
import { ExecutionContext, Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Injectable()
export class MicrosoftOAuthGuard extends AuthGuard('microsoft') {
constructor() {
super({
prompt: 'select_account',
});
}
async canActivate(context: ExecutionContext) {
const request = context.switchToHttp().getRequest();
const workspaceInviteHash = request.query.inviteHash;
if (workspaceInviteHash && typeof workspaceInviteHash === 'string') {
request.params.workspaceInviteHash = workspaceInviteHash;
}
return (await super.canActivate(context)) as boolean;
}
}

View File

@ -0,0 +1,21 @@
import { Injectable, CanActivate, NotFoundException } from '@nestjs/common';
import { Observable } from 'rxjs';
import { EnvironmentService } from 'src/engine/integrations/environment/environment.service';
import { MicrosoftStrategy } from 'src/engine/core-modules/auth/strategies/microsoft.auth.strategy';
@Injectable()
export class MicrosoftProviderEnabledGuard implements CanActivate {
constructor(private readonly environmentService: EnvironmentService) {}
canActivate(): boolean | Promise<boolean> | Observable<boolean> {
if (!this.environmentService.get('AUTH_MICROSOFT_ENABLED')) {
throw new NotFoundException('Microsoft auth is not enabled');
}
new MicrosoftStrategy(this.environmentService);
return true;
}
}

View File

@ -0,0 +1,76 @@
import { BadRequestException } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { Request } from 'express';
import { VerifyCallback } from 'passport-google-oauth20';
import { Strategy } from 'passport-microsoft';
import { EnvironmentService } from 'src/engine/integrations/environment/environment.service';
export type MicrosoftRequest = Omit<
Request,
'user' | 'workspace' | 'cacheVersion'
> & {
user: {
firstName?: string | null;
lastName?: string | null;
email: string;
picture: string | null;
workspaceInviteHash?: string;
};
};
export class MicrosoftStrategy extends PassportStrategy(Strategy, 'microsoft') {
constructor(environmentService: EnvironmentService) {
super({
clientID: environmentService.get('AUTH_MICROSOFT_CLIENT_ID'),
clientSecret: environmentService.get('AUTH_MICROSOFT_CLIENT_SECRET'),
callbackURL: environmentService.get('AUTH_MICROSOFT_CALLBACK_URL'),
tenant: environmentService.get('AUTH_MICROSOFT_TENANT_ID'),
scope: ['user.read'],
passReqToCallback: true,
});
}
authenticate(req: any, options: any) {
options = {
...options,
state: JSON.stringify({
workspaceInviteHash: req.params.workspaceInviteHash,
}),
};
return super.authenticate(req, options);
}
async validate(
request: MicrosoftRequest,
accessToken: string,
refreshToken: string,
profile: any,
done: VerifyCallback,
): Promise<void> {
const { name, emails, photos } = profile;
const state =
typeof request.query.state === 'string'
? JSON.parse(request.query.state)
: undefined;
const email = emails?.[0]?.value ?? null;
if (!email) {
throw new BadRequestException('No email found in your Microsoft profile');
}
const user: MicrosoftRequest['user'] = {
email,
firstName: name.givenName,
lastName: name.familyName,
picture: photos?.[0]?.value,
workspaceInviteHash: state.workspaceInviteHash,
};
done(null, user);
}
}

View File

@ -10,6 +10,9 @@ class AuthProviders {
@Field(() => Boolean)
password: boolean;
@Field(() => Boolean)
microsoft: boolean;
}
@ObjectType()

View File

@ -14,7 +14,8 @@ export class ClientConfigResolver {
authProviders: {
google: this.environmentService.get('AUTH_GOOGLE_ENABLED'),
magicLink: false,
password: true,
password: this.environmentService.get('AUTH_PASSWORD_ENABLED'),
microsoft: this.environmentService.get('AUTH_MICROSOFT_ENABLED'),
},
telemetry: {
enabled: this.environmentService.get('TELEMETRY_ENABLED'),