mirror of
https://github.com/twentyhq/twenty.git
synced 2024-08-17 18:00:29 +03:00
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:
parent
b3e1d6becf
commit
87a9ecee28
@ -61,6 +61,7 @@
|
||||
"@types/lodash.pick": "^4.3.7",
|
||||
"@types/mailparser": "^3.4.4",
|
||||
"@types/nodemailer": "^6.4.14",
|
||||
"@types/passport-microsoft": "^1.0.3",
|
||||
"add": "^2.0.6",
|
||||
"afterframe": "^1.0.2",
|
||||
"apollo-server-express": "^3.12.0",
|
||||
@ -136,6 +137,7 @@
|
||||
"passport-google-oauth20": "^2.0.0",
|
||||
"passport-jwt": "^4.0.1",
|
||||
"passport-local": "^1.0.0",
|
||||
"passport-microsoft": "^2.0.0",
|
||||
"patch-package": "^8.0.0",
|
||||
"pg": "^8.11.3",
|
||||
"pg-boss": "^9.0.3",
|
||||
|
@ -70,10 +70,16 @@ import TabItem from '@theme/TabItem';
|
||||
['MESSAGING_PROVIDER_GMAIL_ENABLED', 'false', 'Enable Gmail API connection'],
|
||||
['CALENDAR_PROVIDER_GMAIL_ENABLED', 'false', 'Enable Google Calendar API connection'],
|
||||
['AUTH_GOOGLE_APIS_CALLBACK_URL', '', 'Google APIs auth callback'],
|
||||
['AUTH_GOOGLE_ENABLED', 'false', 'Enable Goole SSO login'],
|
||||
['AUTH_PASSWORD_ENABLED', 'false', 'Enable Email/Password login'],
|
||||
['AUTH_GOOGLE_ENABLED', 'false', 'Enable Google SSO login'],
|
||||
['AUTH_GOOGLE_CLIENT_ID', '', 'Google client ID'],
|
||||
['AUTH_GOOGLE_CLIENT_SECRET', '', 'Google client secret'],
|
||||
['AUTH_GOOGLE_CALLBACK_URL', '', 'Google auth callback'],
|
||||
['AUTH_MICROSOFT_ENABLED', 'false', 'Enable Microsoft SSO login'],
|
||||
['AUTH_MICROSOFT_CLIENT_ID', '', 'Microsoft client ID'],
|
||||
['AUTH_MICROSOFT_TENANT_ID', '', 'Microsoft tenant ID'],
|
||||
['AUTH_MICROSOFT_CLIENT_SECRET', '', 'Microsoft client secret'],
|
||||
['AUTH_MICROSOFT_CALLBACK_URL', '', 'Microsoft auth callback'],
|
||||
['FRONT_AUTH_CALLBACK_URL', 'http://localhost:3001/verify ', 'Callback used for Login page'],
|
||||
['IS_SIGN_UP_DISABLED', 'false', 'Disable sign-up'],
|
||||
['PASSWORD_RESET_TOKEN_EXPIRES_IN', '5m', 'Password reset token expiration time'],
|
||||
|
@ -56,6 +56,7 @@ export type AuthProviders = {
|
||||
__typename?: 'AuthProviders';
|
||||
google: Scalars['Boolean'];
|
||||
magicLink: Scalars['Boolean'];
|
||||
microsoft: Scalars['Boolean'];
|
||||
password: Scalars['Boolean'];
|
||||
};
|
||||
|
||||
@ -562,6 +563,7 @@ export type RemoteServer = {
|
||||
|
||||
export type RemoteTable = {
|
||||
__typename?: 'RemoteTable';
|
||||
id?: Maybe<Scalars['UUID']>;
|
||||
name: Scalars['String'];
|
||||
schema: Scalars['String'];
|
||||
status: RemoteTableStatus;
|
||||
@ -1111,7 +1113,7 @@ export type UpdateBillingSubscriptionMutation = { __typename?: 'Mutation', updat
|
||||
export type GetClientConfigQueryVariables = Exact<{ [key: string]: never; }>;
|
||||
|
||||
|
||||
export type GetClientConfigQuery = { __typename?: 'Query', clientConfig: { __typename?: 'ClientConfig', signInPrefilled: boolean, signUpDisabled: boolean, debugMode: boolean, authProviders: { __typename?: 'AuthProviders', google: boolean, password: boolean }, billing: { __typename?: 'Billing', isBillingEnabled: boolean, billingUrl?: string | null, billingFreeTrialDurationInDays?: number | null }, telemetry: { __typename?: 'Telemetry', enabled: boolean, anonymizationEnabled: boolean }, support: { __typename?: 'Support', supportDriver: string, supportFrontChatId?: string | null }, sentry: { __typename?: 'Sentry', dsn?: string | null, environment?: string | null, release?: string | null } } };
|
||||
export type GetClientConfigQuery = { __typename?: 'Query', clientConfig: { __typename?: 'ClientConfig', signInPrefilled: boolean, signUpDisabled: boolean, debugMode: boolean, authProviders: { __typename?: 'AuthProviders', google: boolean, password: boolean, microsoft: boolean }, billing: { __typename?: 'Billing', isBillingEnabled: boolean, billingUrl?: string | null, billingFreeTrialDurationInDays?: number | null }, telemetry: { __typename?: 'Telemetry', enabled: boolean, anonymizationEnabled: boolean }, support: { __typename?: 'Support', supportDriver: string, supportFrontChatId?: string | null }, sentry: { __typename?: 'Sentry', dsn?: string | null, environment?: string | null, release?: string | null } } };
|
||||
|
||||
export type UploadFileMutationVariables = Exact<{
|
||||
file: Scalars['Upload'];
|
||||
@ -2140,6 +2142,7 @@ export const GetClientConfigDocument = gql`
|
||||
authProviders {
|
||||
google
|
||||
password
|
||||
microsoft
|
||||
}
|
||||
billing {
|
||||
isBillingEnabled
|
||||
|
@ -116,8 +116,9 @@ describe('useAuth', () => {
|
||||
expect(state.icons).toEqual({});
|
||||
expect(state.authProviders).toEqual({
|
||||
google: false,
|
||||
microsoft: false,
|
||||
magicLink: false,
|
||||
password: true,
|
||||
password: false,
|
||||
});
|
||||
expect(state.billing).toBeNull();
|
||||
expect(state.isSignInPrefilled).toBe(false);
|
||||
|
@ -234,6 +234,14 @@ export const useAuth = () => {
|
||||
}` || '';
|
||||
}, []);
|
||||
|
||||
const handleMicrosoftLogin = useCallback((workspaceInviteHash?: string) => {
|
||||
const authServerUrl = REACT_APP_SERVER_BASE_URL;
|
||||
window.location.href =
|
||||
`${authServerUrl}/auth/microsoft/${
|
||||
workspaceInviteHash ? '?inviteHash=' + workspaceInviteHash : ''
|
||||
}` || '';
|
||||
}, []);
|
||||
|
||||
return {
|
||||
challenge: handleChallenge,
|
||||
verify: handleVerify,
|
||||
@ -244,5 +252,6 @@ export const useAuth = () => {
|
||||
signUpWithCredentials: handleCredentialsSignUp,
|
||||
signInWithCredentials: handleCrendentialsSignIn,
|
||||
signInWithGoogle: handleGoogleLogin,
|
||||
signInWithMicrosoft: handleMicrosoftLogin,
|
||||
};
|
||||
};
|
||||
|
@ -1,12 +1,19 @@
|
||||
import { JSX } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
const StyledSeparator = styled.div`
|
||||
type HorizontalSeparatorProps = {
|
||||
visible?: boolean;
|
||||
};
|
||||
const StyledSeparator = styled.div<HorizontalSeparatorProps>`
|
||||
background-color: ${({ theme }) => theme.border.color.medium};
|
||||
height: 1px;
|
||||
height: ${({ visible }) => (visible ? '1px' : 0)};
|
||||
margin-bottom: ${({ theme }) => theme.spacing(3)};
|
||||
margin-top: ${({ theme }) => theme.spacing(3)};
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
export const HorizontalSeparator = (): JSX.Element => <StyledSeparator />;
|
||||
export const HorizontalSeparator = ({
|
||||
visible = true,
|
||||
}: HorizontalSeparatorProps): JSX.Element => (
|
||||
<StyledSeparator visible={visible} />
|
||||
);
|
||||
|
@ -4,11 +4,12 @@ import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { motion } from 'framer-motion';
|
||||
import { useRecoilState } from 'recoil';
|
||||
import { IconGoogle } from 'twenty-ui';
|
||||
import { IconGoogle, IconMicrosoft } from 'twenty-ui';
|
||||
|
||||
import { useHandleResetPassword } from '@/auth/sign-in-up/hooks/useHandleResetPassword';
|
||||
import { useSignInUpForm } from '@/auth/sign-in-up/hooks/useSignInUpForm';
|
||||
import { useSignInWithGoogle } from '@/auth/sign-in-up/hooks/useSignInWithGoogle';
|
||||
import { useSignInWithMicrosoft } from '@/auth/sign-in-up/hooks/useSignInWithMicrosoft';
|
||||
import { useWorkspaceFromInviteHash } from '@/auth/sign-in-up/hooks/useWorkspaceFromInviteHash';
|
||||
import { authProvidersState } from '@/client-config/states/authProvidersState';
|
||||
import { Loader } from '@/ui/feedback/loader/components/Loader';
|
||||
@ -27,7 +28,6 @@ import { HorizontalSeparator } from './HorizontalSeparator';
|
||||
const StyledContentContainer = styled.div`
|
||||
margin-bottom: ${({ theme }) => theme.spacing(8)};
|
||||
margin-top: ${({ theme }) => theme.spacing(4)};
|
||||
width: 200px;
|
||||
`;
|
||||
|
||||
const StyledForm = styled.form`
|
||||
@ -51,6 +51,7 @@ export const SignInUpForm = () => {
|
||||
const { handleResetPassword } = useHandleResetPassword();
|
||||
const workspace = useWorkspaceFromInviteHash();
|
||||
const { signInWithGoogle } = useSignInWithGoogle();
|
||||
const { signInWithMicrosoft } = useSignInWithMicrosoft();
|
||||
const { form } = useSignInUpForm();
|
||||
|
||||
const {
|
||||
@ -125,119 +126,133 @@ export const SignInUpForm = () => {
|
||||
onClick={signInWithGoogle}
|
||||
fullWidth
|
||||
/>
|
||||
<HorizontalSeparator />
|
||||
<HorizontalSeparator visible={!authProviders.microsoft} />
|
||||
</>
|
||||
)}
|
||||
|
||||
<StyledForm
|
||||
onSubmit={(event) => {
|
||||
event.preventDefault();
|
||||
}}
|
||||
>
|
||||
{signInUpStep !== SignInUpStep.Init && (
|
||||
<StyledFullWidthMotionDiv
|
||||
initial={{ opacity: 0, height: 0 }}
|
||||
animate={{ opacity: 1, height: 'auto' }}
|
||||
transition={{
|
||||
type: 'spring',
|
||||
stiffness: 800,
|
||||
damping: 35,
|
||||
}}
|
||||
>
|
||||
<Controller
|
||||
name="email"
|
||||
control={form.control}
|
||||
render={({
|
||||
field: { onChange, onBlur, value },
|
||||
fieldState: { error },
|
||||
}) => (
|
||||
<StyledInputContainer>
|
||||
<TextInput
|
||||
autoFocus
|
||||
value={value}
|
||||
placeholder="Email"
|
||||
onBlur={onBlur}
|
||||
onChange={(value: string) => {
|
||||
onChange(value);
|
||||
if (signInUpStep === SignInUpStep.Password) {
|
||||
continueWithEmail();
|
||||
}
|
||||
}}
|
||||
error={showErrors ? error?.message : undefined}
|
||||
fullWidth
|
||||
disableHotkeys
|
||||
onKeyDown={handleKeyDown}
|
||||
/>
|
||||
</StyledInputContainer>
|
||||
)}
|
||||
/>
|
||||
</StyledFullWidthMotionDiv>
|
||||
)}
|
||||
{signInUpStep === SignInUpStep.Password && (
|
||||
<StyledFullWidthMotionDiv
|
||||
initial={{ opacity: 0, height: 0 }}
|
||||
animate={{ opacity: 1, height: 'auto' }}
|
||||
transition={{
|
||||
type: 'spring',
|
||||
stiffness: 800,
|
||||
damping: 35,
|
||||
}}
|
||||
>
|
||||
<Controller
|
||||
name="password"
|
||||
control={form.control}
|
||||
render={({
|
||||
field: { onChange, onBlur, value },
|
||||
fieldState: { error },
|
||||
}) => (
|
||||
<StyledInputContainer>
|
||||
<TextInput
|
||||
autoFocus
|
||||
value={value}
|
||||
type="password"
|
||||
placeholder="Password"
|
||||
onBlur={onBlur}
|
||||
onChange={onChange}
|
||||
error={showErrors ? error?.message : undefined}
|
||||
fullWidth
|
||||
disableHotkeys
|
||||
onKeyDown={handleKeyDown}
|
||||
/>
|
||||
</StyledInputContainer>
|
||||
)}
|
||||
/>
|
||||
</StyledFullWidthMotionDiv>
|
||||
)}
|
||||
{authProviders.microsoft && (
|
||||
<>
|
||||
<MainButton
|
||||
Icon={() => <IconMicrosoft size={theme.icon.size.lg} />}
|
||||
title="Continue with Microsoft"
|
||||
onClick={signInWithMicrosoft}
|
||||
fullWidth
|
||||
/>
|
||||
<HorizontalSeparator visible={authProviders.password} />
|
||||
</>
|
||||
)}
|
||||
|
||||
<MainButton
|
||||
variant="secondary"
|
||||
title={buttonTitle}
|
||||
type="submit"
|
||||
onClick={() => {
|
||||
if (signInUpStep === SignInUpStep.Init) {
|
||||
continueWithEmail();
|
||||
return;
|
||||
}
|
||||
if (signInUpStep === SignInUpStep.Email) {
|
||||
continueWithCredentials();
|
||||
return;
|
||||
}
|
||||
setShowErrors(true);
|
||||
form.handleSubmit(submitCredentials)();
|
||||
{authProviders.password && (
|
||||
<StyledForm
|
||||
onSubmit={(event) => {
|
||||
event.preventDefault();
|
||||
}}
|
||||
Icon={() => form.formState.isSubmitting && <Loader />}
|
||||
disabled={
|
||||
signInUpStep === SignInUpStep.Init
|
||||
? false
|
||||
: signInUpStep === SignInUpStep.Email
|
||||
? !form.watch('email')
|
||||
: !form.watch('email') ||
|
||||
!form.watch('password') ||
|
||||
form.formState.isSubmitting
|
||||
}
|
||||
fullWidth
|
||||
/>
|
||||
</StyledForm>
|
||||
>
|
||||
{signInUpStep !== SignInUpStep.Init && (
|
||||
<StyledFullWidthMotionDiv
|
||||
initial={{ opacity: 0, height: 0 }}
|
||||
animate={{ opacity: 1, height: 'auto' }}
|
||||
transition={{
|
||||
type: 'spring',
|
||||
stiffness: 800,
|
||||
damping: 35,
|
||||
}}
|
||||
>
|
||||
<Controller
|
||||
name="email"
|
||||
control={form.control}
|
||||
render={({
|
||||
field: { onChange, onBlur, value },
|
||||
fieldState: { error },
|
||||
}) => (
|
||||
<StyledInputContainer>
|
||||
<TextInput
|
||||
autoFocus
|
||||
value={value}
|
||||
placeholder="Email"
|
||||
onBlur={onBlur}
|
||||
onChange={(value: string) => {
|
||||
onChange(value);
|
||||
if (signInUpStep === SignInUpStep.Password) {
|
||||
continueWithEmail();
|
||||
}
|
||||
}}
|
||||
error={showErrors ? error?.message : undefined}
|
||||
fullWidth
|
||||
disableHotkeys
|
||||
onKeyDown={handleKeyDown}
|
||||
/>
|
||||
</StyledInputContainer>
|
||||
)}
|
||||
/>
|
||||
</StyledFullWidthMotionDiv>
|
||||
)}
|
||||
{signInUpStep === SignInUpStep.Password && (
|
||||
<StyledFullWidthMotionDiv
|
||||
initial={{ opacity: 0, height: 0 }}
|
||||
animate={{ opacity: 1, height: 'auto' }}
|
||||
transition={{
|
||||
type: 'spring',
|
||||
stiffness: 800,
|
||||
damping: 35,
|
||||
}}
|
||||
>
|
||||
<Controller
|
||||
name="password"
|
||||
control={form.control}
|
||||
render={({
|
||||
field: { onChange, onBlur, value },
|
||||
fieldState: { error },
|
||||
}) => (
|
||||
<StyledInputContainer>
|
||||
<TextInput
|
||||
autoFocus
|
||||
value={value}
|
||||
type="password"
|
||||
placeholder="Password"
|
||||
onBlur={onBlur}
|
||||
onChange={onChange}
|
||||
error={showErrors ? error?.message : undefined}
|
||||
fullWidth
|
||||
disableHotkeys
|
||||
onKeyDown={handleKeyDown}
|
||||
/>
|
||||
</StyledInputContainer>
|
||||
)}
|
||||
/>
|
||||
</StyledFullWidthMotionDiv>
|
||||
)}
|
||||
|
||||
<MainButton
|
||||
variant="secondary"
|
||||
title={buttonTitle}
|
||||
type="submit"
|
||||
onClick={() => {
|
||||
if (signInUpStep === SignInUpStep.Init) {
|
||||
continueWithEmail();
|
||||
return;
|
||||
}
|
||||
if (signInUpStep === SignInUpStep.Email) {
|
||||
continueWithCredentials();
|
||||
return;
|
||||
}
|
||||
setShowErrors(true);
|
||||
form.handleSubmit(submitCredentials)();
|
||||
}}
|
||||
Icon={() => form.formState.isSubmitting && <Loader />}
|
||||
disabled={
|
||||
signInUpStep === SignInUpStep.Init
|
||||
? false
|
||||
: signInUpStep === SignInUpStep.Email
|
||||
? !form.watch('email')
|
||||
: !form.watch('email') ||
|
||||
!form.watch('password') ||
|
||||
form.formState.isSubmitting
|
||||
}
|
||||
fullWidth
|
||||
/>
|
||||
</StyledForm>
|
||||
)}
|
||||
</StyledContentContainer>
|
||||
{signInUpStep === SignInUpStep.Password && (
|
||||
<ActionLink onClick={handleResetPassword(form.getValues('email'))}>
|
||||
|
@ -0,0 +1,11 @@
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
import { useAuth } from '@/auth/hooks/useAuth.ts';
|
||||
|
||||
export const useSignInWithMicrosoft = () => {
|
||||
const workspaceInviteHash = useParams().workspaceInviteHash;
|
||||
const { signInWithMicrosoft } = useAuth();
|
||||
return {
|
||||
signInWithMicrosoft: () => signInWithMicrosoft(workspaceInviteHash),
|
||||
};
|
||||
};
|
@ -38,6 +38,7 @@ export const ClientConfigProviderEffect = () => {
|
||||
setIsClientConfigLoaded(true);
|
||||
setAuthProviders({
|
||||
google: data?.clientConfig.authProviders.google,
|
||||
microsoft: data?.clientConfig.authProviders.microsoft,
|
||||
password: data?.clientConfig.authProviders.password,
|
||||
magicLink: false,
|
||||
});
|
||||
|
@ -6,6 +6,7 @@ export const GET_CLIENT_CONFIG = gql`
|
||||
authProviders {
|
||||
google
|
||||
password
|
||||
microsoft
|
||||
}
|
||||
billing {
|
||||
isBillingEnabled
|
||||
|
@ -4,5 +4,10 @@ import { AuthProviders } from '~/generated/graphql';
|
||||
|
||||
export const authProvidersState = createState<AuthProviders>({
|
||||
key: 'authProvidersState',
|
||||
defaultValue: { google: false, magicLink: false, password: true },
|
||||
defaultValue: {
|
||||
google: false,
|
||||
magicLink: false,
|
||||
password: false,
|
||||
microsoft: false,
|
||||
},
|
||||
});
|
||||
|
@ -21,12 +21,18 @@ SIGN_IN_PREFILLED=true
|
||||
# REFRESH_TOKEN_EXPIRES_IN=90d
|
||||
# FILE_TOKEN_EXPIRES_IN=1d
|
||||
# FRONT_AUTH_CALLBACK_URL=http://localhost:3001/verify
|
||||
# AUTH_GOOGLE_ENABLED=false
|
||||
# MESSAGING_PROVIDER_GMAIL_ENABLED=false
|
||||
# CALENDAR_PROVIDER_GOOGLE_ENABLED=false
|
||||
# IS_BILLING_ENABLED=false
|
||||
# BILLING_PLAN_REQUIRED_LINK=https://twenty.com/stripe-redirection
|
||||
# AUTH_PASSWORD_ENABLED=false
|
||||
# IS_SIGN_UP_DISABLED=false
|
||||
# AUTH_MICROSOFT_ENABLED=false
|
||||
# AUTH_MICROSOFT_CLIENT_ID=replace_me_with_azure_client_id
|
||||
# AUTH_MICROSOFT_TENANT_ID=replace_me_with_azure_tenant_id
|
||||
# AUTH_MICROSOFT_CLIENT_SECRET=replace_me_with_azure_client_secret
|
||||
# AUTH_MICROSOFT_CALLBACK_URL=http://localhost:3000/auth/microsoft/redirect
|
||||
# AUTH_GOOGLE_ENABLED=false
|
||||
# AUTH_GOOGLE_CLIENT_ID=replace_me_with_google_client_id
|
||||
# AUTH_GOOGLE_CLIENT_SECRET=replace_me_with_google_client_secret
|
||||
# AUTH_GOOGLE_CALLBACK_URL=http://localhost:3000/auth/google/redirect
|
||||
|
@ -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,
|
||||
],
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -10,6 +10,9 @@ class AuthProviders {
|
||||
|
||||
@Field(() => Boolean)
|
||||
password: boolean;
|
||||
|
||||
@Field(() => Boolean)
|
||||
microsoft: boolean;
|
||||
}
|
||||
|
||||
@ObjectType()
|
||||
|
@ -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'),
|
||||
|
@ -52,11 +52,6 @@ export class EnvironmentVariables {
|
||||
@Max(65535)
|
||||
DEBUG_PORT = 9000;
|
||||
|
||||
@CastToBoolean()
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
SIGN_IN_PREFILLED = false;
|
||||
|
||||
@CastToBoolean()
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@ -156,21 +151,53 @@ export class EnvironmentVariables {
|
||||
@IsOptional()
|
||||
FRONT_AUTH_CALLBACK_URL: string;
|
||||
|
||||
@CastToBoolean()
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
AUTH_PASSWORD_ENABLED = true;
|
||||
|
||||
@CastToBoolean()
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@ValidateIf((env) => env.AUTH_PASSWORD_ENABLED)
|
||||
SIGN_IN_PREFILLED = false;
|
||||
|
||||
@CastToBoolean()
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
AUTH_MICROSOFT_ENABLED = false;
|
||||
|
||||
@IsString()
|
||||
@ValidateIf((env) => env.AUTH_MICROSOFT_ENABLED)
|
||||
AUTH_MICROSOFT_CLIENT_ID: string;
|
||||
|
||||
@IsString()
|
||||
@ValidateIf((env) => env.AUTH_MICROSOFT_ENABLED)
|
||||
AUTH_MICROSOFT_TENANT_ID: string;
|
||||
|
||||
@IsString()
|
||||
@ValidateIf((env) => env.AUTH_MICROSOFT_ENABLED)
|
||||
AUTH_MICROSOFT_CLIENT_SECRET: string;
|
||||
|
||||
@IsUrl({ require_tld: false })
|
||||
@ValidateIf((env) => env.AUTH_MICROSOFT_ENABLED)
|
||||
AUTH_MICROSOFT_CALLBACK_URL: string;
|
||||
|
||||
@CastToBoolean()
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
AUTH_GOOGLE_ENABLED = false;
|
||||
|
||||
@IsString()
|
||||
@ValidateIf((env) => env.AUTH_GOOGLE_ENABLED === true)
|
||||
@ValidateIf((env) => env.AUTH_GOOGLE_ENABLED)
|
||||
AUTH_GOOGLE_CLIENT_ID: string;
|
||||
|
||||
@IsString()
|
||||
@ValidateIf((env) => env.AUTH_GOOGLE_ENABLED === true)
|
||||
@ValidateIf((env) => env.AUTH_GOOGLE_ENABLED)
|
||||
AUTH_GOOGLE_CLIENT_SECRET: string;
|
||||
|
||||
@IsUrl({ require_tld: false })
|
||||
@ValidateIf((env) => env.AUTH_GOOGLE_ENABLED === true)
|
||||
@ValidateIf((env) => env.AUTH_GOOGLE_ENABLED)
|
||||
AUTH_GOOGLE_CALLBACK_URL: string;
|
||||
|
||||
// Storage
|
||||
|
7
packages/twenty-ui/src/display/icon/assets/microsoft.svg
Normal file
7
packages/twenty-ui/src/display/icon/assets/microsoft.svg
Normal file
@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<svg width="800px" height="800px" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="none">
|
||||
<path fill="#F35325" d="M1 1h6.5v6.5H1V1z"/>
|
||||
<path fill="#81BC06" d="M8.5 1H15v6.5H8.5V1z"/>
|
||||
<path fill="#05A6F0" d="M1 8.5h6.5V15H1V8.5z"/>
|
||||
<path fill="#FFBA08" d="M8.5 8.5H15V15H8.5V8.5z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 355 B |
@ -0,0 +1,14 @@
|
||||
import { useTheme } from '@emotion/react';
|
||||
|
||||
import IconMicrosoftRaw from '../assets/microsoft.svg?react';
|
||||
|
||||
interface IconMicrosoftProps {
|
||||
size?: number;
|
||||
}
|
||||
|
||||
export const IconMicrosoft = (props: IconMicrosoftProps) => {
|
||||
const theme = useTheme();
|
||||
const size = props.size ?? theme.icon.size.lg;
|
||||
|
||||
return <IconMicrosoftRaw height={size} width={size} />;
|
||||
};
|
@ -8,6 +8,7 @@ export * from './icon/components/IconAddressBook';
|
||||
export * from './icon/components/IconGmail';
|
||||
export * from './icon/components/IconGoogle';
|
||||
export * from './icon/components/IconGoogleCalendar';
|
||||
export * from './icon/components/IconMicrosoft';
|
||||
export * from './icon/components/IconTwentyStar';
|
||||
export * from './icon/components/IconTwentyStarFilled';
|
||||
export * from './icon/components/TablerIcons';
|
||||
|
40
yarn.lock
40
yarn.lock
@ -17093,6 +17093,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/passport-microsoft@npm:^1.0.3":
|
||||
version: 1.0.3
|
||||
resolution: "@types/passport-microsoft@npm:1.0.3"
|
||||
dependencies:
|
||||
"@types/passport-oauth2": "npm:*"
|
||||
checksum: d177f63ae4976253242966146030b9913dc3b77c4eb192f5491d397f9036b64f258325157c244960ef4539e9c46807d0bba53497630253f4ef98d1318142b3d8
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/passport-oauth2@npm:*":
|
||||
version: 1.4.15
|
||||
resolution: "@types/passport-oauth2@npm:1.4.15"
|
||||
@ -38165,6 +38174,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"oauth@npm:0.10.x":
|
||||
version: 0.10.0
|
||||
resolution: "oauth@npm:0.10.0"
|
||||
checksum: 76f3e186cfd76cb33e5d5d442861c86680a5c3b71b2db1b854212087532c265a69de1a2ab9db683e6c6df733e17cfc67476527b81b224a19c1917de2bc3f75fa
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"oauth@npm:0.9.x":
|
||||
version: 0.9.15
|
||||
resolution: "oauth@npm:0.9.15"
|
||||
@ -39110,6 +39126,28 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"passport-microsoft@npm:^2.0.0":
|
||||
version: 2.0.0
|
||||
resolution: "passport-microsoft@npm:2.0.0"
|
||||
dependencies:
|
||||
passport-oauth2: "npm:1.8.0"
|
||||
checksum: 6b7d053673d5af6cbbce3812a628afff2435aa321d70c9feaa3309d7a610a2e61c753a7cde8c0febee2cacf74b971fcdc7699fa1c073c72052bc4334f1b203ec
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"passport-oauth2@npm:1.8.0":
|
||||
version: 1.8.0
|
||||
resolution: "passport-oauth2@npm:1.8.0"
|
||||
dependencies:
|
||||
base64url: "npm:3.x.x"
|
||||
oauth: "npm:0.10.x"
|
||||
passport-strategy: "npm:1.x.x"
|
||||
uid2: "npm:0.0.x"
|
||||
utils-merge: "npm:1.x.x"
|
||||
checksum: 16b431bd856b84dfe0c9c913dcbea6ff54875befac1035171b0dce1c77f79072dc5e26d785b13c2e62c034c8174a1a47571751d1066bdbcdb9108de217c0b19b
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"passport-oauth2@npm:1.x.x":
|
||||
version: 1.7.0
|
||||
resolution: "passport-oauth2@npm:1.7.0"
|
||||
@ -46629,6 +46667,7 @@ __metadata:
|
||||
"@types/nodemailer": "npm:^6.4.14"
|
||||
"@types/passport-google-oauth20": "npm:^2.0.11"
|
||||
"@types/passport-jwt": "npm:^3.0.8"
|
||||
"@types/passport-microsoft": "npm:^1.0.3"
|
||||
"@types/react": "npm:^18.2.39"
|
||||
"@types/react-datepicker": "npm:^6.2.0"
|
||||
"@types/react-dom": "npm:^18.2.15"
|
||||
@ -46745,6 +46784,7 @@ __metadata:
|
||||
passport-google-oauth20: "npm:^2.0.0"
|
||||
passport-jwt: "npm:^4.0.1"
|
||||
passport-local: "npm:^1.0.0"
|
||||
passport-microsoft: "npm:^2.0.0"
|
||||
patch-package: "npm:^8.0.0"
|
||||
pg: "npm:^8.11.3"
|
||||
pg-boss: "npm:^9.0.3"
|
||||
|
Loading…
Reference in New Issue
Block a user