Skip to content

Commit

Permalink
feat(sso): allow to use OIDC and SAML (#7246)
Browse files Browse the repository at this point in the history
## What it does
### Backend
- [x] Add a mutation to create OIDC and SAML configuration
- [x] Add a mutation to delete an SSO config
- [x] Add a feature flag to toggle SSO
- [x] Add a mutation to activate/deactivate an SSO config
- [x] Add a mutation to delete an SSO config
- [x] Add strategy to use OIDC or SAML
- [ ] Improve error management

### Frontend
- [x] Add section "security" in settings
- [x] Add page to list SSO configurations
- [x] Add page and forms to create OIDC or SAML configuration
- [x] Add field to "connect with SSO" in the signin/signup process
- [x] Trigger auth when a user switch to a workspace with SSO enable
- [x] Add an option on the security page to activate/deactivate the
global invitation link
- [ ] Add new Icons for SSO Identity Providers (okta, Auth0, Azure,
Microsoft)

---------

Co-authored-by: Félix Malfait <[email protected]>
Co-authored-by: Charles Bochet <[email protected]>
  • Loading branch information
3 people authored Oct 21, 2024
1 parent 11c3f1c commit 0f0a796
Show file tree
Hide file tree
Showing 132 changed files with 5,243 additions and 304 deletions.
49 changes: 49 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@

This project is mostly licensed under the GNU General Public License (GPL) as described below. However, certain files within this project are licensed under a different commercial license. These files are clearly marked with the following comment at the top of the file: /* @license Enterprise */
Files with this comment are not licensed under the aGPL v3, but instead are subject to the commercial license terms defined later in this file.


GNU AFFERO GENERAL PUBLIC LICENSE
Version 3, 19 November 2007

Expand Down Expand Up @@ -659,3 +664,47 @@ specific requirements.
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU AGPL, see
<https://www.gnu.org/licenses/>.



------------------------------------



The Twenty.com Commercial License (the “Commercial License”)
Copyright (c) 2023-present Twenty.com, PBC

With regard to Twenty's Software:

This part of the software and associated documentation files (the "Software") may only be
used in production, if you (and any entity that you represent) have agreed to,
and are in compliance with, the Terms available
at https://twenty.com/legal/terms, or other agreements governing
the use of the Software, as mutually agreed by you and Twenty.com, PBC ("Twenty"),
and otherwise have a valid Twenty Enterprise Edition subscription
for the correct number of hosts and seats as defined in the Commercial Terms.
Subject to the foregoing sentence,
you are free to modify this Software and publish patches to the Software. You agree
that Twenty and/or its licensors (as applicable) retain all right, title and interest in
and to all such modifications and/or patches, and all such modifications and/or
patches may only be used, copied, modified, displayed, distributed, or otherwise
exploited with a valid Commercial Subscription for the correct number of hosts and seats.
Notwithstanding the foregoing, you may copy and modify the Software for development
and testing purposes, without requiring a subscription. You agree that Twenty.Com and/or
its licensors (as applicable) retain all right, title and interest in and to all such
modifications. You are not granted any other rights beyond what is expressly stated herein.
Subject to the foregoing, it is forbidden to copy, merge, publish, distribute, sublicense,
and/or sell the Software.


THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

For all third party components incorporated into the Twenty Software, those
components are licensed under the original license provided by the owner of the
applicable component.
2 changes: 1 addition & 1 deletion packages/twenty-front/folderStructure.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"$schema": "../../node_modules/eslint-plugin-project-structure/folderStructure.schema.json",
"regexParameters": {
"camelCase": "^[a-z]+([A-Za-z0-9]+)+"
"camelCase": "^[a-z]+[A-Za-z0-9]+"
},
"structure": [
{
Expand Down
150 changes: 149 additions & 1 deletion packages/twenty-front/src/generated-metadata/graphql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ export type AuthProviders = {
magicLink: Scalars['Boolean']['output'];
microsoft: Scalars['Boolean']['output'];
password: Scalars['Boolean']['output'];
sso: Scalars['Boolean']['output'];
};

export type AuthToken = {
Expand Down Expand Up @@ -148,6 +149,7 @@ export enum CaptchaDriverType {

export type ClientConfig = {
__typename?: 'ClientConfig';
analyticsEnabled: Scalars['Boolean']['output'];
api: ApiConfig;
authProviders: AuthProviders;
billing: Billing;
Expand Down Expand Up @@ -275,6 +277,15 @@ export type DeleteServerlessFunctionInput = {
id: Scalars['ID']['input'];
};

export type DeleteSsoInput = {
identityProviderId: Scalars['String']['input'];
};

export type DeleteSsoOutput = {
__typename?: 'DeleteSsoOutput';
identityProviderId: Scalars['String']['output'];
};

/** Schema update on a table */
export enum DistantTableUpdate {
ColumnsAdded = 'COLUMNS_ADDED',
Expand All @@ -283,6 +294,20 @@ export enum DistantTableUpdate {
TableDeleted = 'TABLE_DELETED'
}

export type EditSsoInput = {
id: Scalars['String']['input'];
status: SsoIdentityProviderStatus;
};

export type EditSsoOutput = {
__typename?: 'EditSsoOutput';
id: Scalars['String']['output'];
issuer: Scalars['String']['output'];
name: Scalars['String']['output'];
status: SsoIdentityProviderStatus;
type: IdpType;
};

export type EmailPasswordResetLink = {
__typename?: 'EmailPasswordResetLink';
/** Boolean that confirms query was dispatched */
Expand Down Expand Up @@ -372,6 +397,20 @@ export enum FileFolder {
WorkspaceLogo = 'WorkspaceLogo'
}

export type FindAvailableSsoidpInput = {
email: Scalars['String']['input'];
};

export type FindAvailableSsoidpOutput = {
__typename?: 'FindAvailableSSOIDPOutput';
id: Scalars['String']['output'];
issuer: Scalars['String']['output'];
name: Scalars['String']['output'];
status: SsoIdentityProviderStatus;
type: IdpType;
workspace: WorkspaceNameAndId;
};

export type FindManyRemoteTablesInput = {
/** The id of the remote server. */
id: Scalars['ID']['input'];
Expand All @@ -385,13 +424,45 @@ export type FullName = {
lastName: Scalars['String']['output'];
};

export type GenerateJwt = GenerateJwtOutputWithAuthTokens | GenerateJwtOutputWithSsoauth;

export type GenerateJwtOutputWithAuthTokens = {
__typename?: 'GenerateJWTOutputWithAuthTokens';
authTokens: AuthTokens;
reason: Scalars['String']['output'];
success: Scalars['Boolean']['output'];
};

export type GenerateJwtOutputWithSsoauth = {
__typename?: 'GenerateJWTOutputWithSSOAUTH';
availableSSOIDPs: Array<FindAvailableSsoidpOutput>;
reason: Scalars['String']['output'];
success: Scalars['Boolean']['output'];
};

export type GetAuthorizationUrlInput = {
identityProviderId: Scalars['String']['input'];
};

export type GetAuthorizationUrlOutput = {
__typename?: 'GetAuthorizationUrlOutput';
authorizationURL: Scalars['String']['output'];
id: Scalars['String']['output'];
type: Scalars['String']['output'];
};

export type GetServerlessFunctionSourceCodeInput = {
/** The id of the function. */
id: Scalars['ID']['input'];
/** The version of the function */
version?: Scalars['String']['input'];
};

export enum IdpType {
Oidc = 'OIDC',
Saml = 'SAML'
}

export type IndexConnection = {
__typename?: 'IndexConnection';
/** Array of edges. */
Expand Down Expand Up @@ -461,29 +532,35 @@ export type Mutation = {
authorizeApp: AuthorizeApp;
challenge: LoginToken;
checkoutSession: SessionEntity;
createOIDCIdentityProvider: SetupSsoOutput;
createOneAppToken: AppToken;
createOneField: Field;
createOneObject: Object;
createOneRelation: Relation;
createOneRemoteServer: RemoteServer;
createOneServerlessFunction: ServerlessFunction;
createSAMLIdentityProvider: SetupSsoOutput;
deactivateWorkflowVersion: Scalars['Boolean']['output'];
deleteCurrentWorkspace: Workspace;
deleteOneField: Field;
deleteOneObject: Object;
deleteOneRelation: Relation;
deleteOneRemoteServer: RemoteServer;
deleteOneServerlessFunction: ServerlessFunction;
deleteSSOIdentityProvider: DeleteSsoOutput;
deleteUser: User;
deleteWorkspaceInvitation: Scalars['String']['output'];
disablePostgresProxy: PostgresCredentials;
editSSOIdentityProvider: EditSsoOutput;
emailPasswordResetLink: EmailPasswordResetLink;
enablePostgresProxy: PostgresCredentials;
exchangeAuthorizationCode: ExchangeAuthCode;
executeOneServerlessFunction: ServerlessFunctionExecutionResult;
findAvailableSSOIdentityProviders: Array<FindAvailableSsoidpOutput>;
generateApiKeyToken: ApiKeyToken;
generateJWT: AuthTokens;
generateJWT: GenerateJwt;
generateTransientToken: TransientToken;
getAuthorizationUrl: GetAuthorizationUrlOutput;
impersonate: Verify;
publishServerlessFunction: ServerlessFunction;
renewToken: AuthTokens;
Expand Down Expand Up @@ -551,6 +628,11 @@ export type MutationCheckoutSessionArgs = {
};


export type MutationCreateOidcIdentityProviderArgs = {
input: SetupOidcSsoInput;
};


export type MutationCreateOneAppTokenArgs = {
input: CreateOneAppTokenInput;
};
Expand Down Expand Up @@ -581,6 +663,11 @@ export type MutationCreateOneServerlessFunctionArgs = {
};


export type MutationCreateSamlIdentityProviderArgs = {
input: SetupSamlSsoInput;
};


export type MutationDeactivateWorkflowVersionArgs = {
workflowVersionId: Scalars['String']['input'];
};
Expand Down Expand Up @@ -611,11 +698,21 @@ export type MutationDeleteOneServerlessFunctionArgs = {
};


export type MutationDeleteSsoIdentityProviderArgs = {
input: DeleteSsoInput;
};


export type MutationDeleteWorkspaceInvitationArgs = {
appTokenId: Scalars['String']['input'];
};


export type MutationEditSsoIdentityProviderArgs = {
input: EditSsoInput;
};


export type MutationEmailPasswordResetLinkArgs = {
email: Scalars['String']['input'];
};
Expand All @@ -633,6 +730,11 @@ export type MutationExecuteOneServerlessFunctionArgs = {
};


export type MutationFindAvailableSsoIdentityProvidersArgs = {
input: FindAvailableSsoidpInput;
};


export type MutationGenerateApiKeyTokenArgs = {
apiKeyId: Scalars['String']['input'];
expiresAt: Scalars['String']['input'];
Expand All @@ -644,6 +746,11 @@ export type MutationGenerateJwtArgs = {
};


export type MutationGetAuthorizationUrlArgs = {
input: GetAuthorizationUrlInput;
};


export type MutationImpersonateArgs = {
userId: Scalars['String']['input'];
};
Expand Down Expand Up @@ -865,6 +972,7 @@ export type Query = {
getTimelineThreadsFromPersonId: TimelineThreadsWithTotal;
index: Index;
indexMetadatas: IndexConnection;
listSSOIdentityProvidersByWorkspaceId: Array<FindAvailableSsoidpOutput>;
object: Object;
objects: ObjectConnection;
relation: Relation;
Expand Down Expand Up @@ -1091,6 +1199,12 @@ export type RunWorkflowVersionInput = {
workflowVersionId: Scalars['String']['input'];
};

export enum SsoIdentityProviderStatus {
Active = 'Active',
Error = 'Error',
Inactive = 'Inactive'
}

export type SendInvitationsOutput = {
__typename?: 'SendInvitationsOutput';
errors: Array<Scalars['String']['output']>;
Expand Down Expand Up @@ -1179,6 +1293,31 @@ export type SessionEntity = {
url?: Maybe<Scalars['String']['output']>;
};

export type SetupOidcSsoInput = {
clientID: Scalars['String']['input'];
clientSecret: Scalars['String']['input'];
issuer: Scalars['String']['input'];
name: Scalars['String']['input'];
};

export type SetupSamlSsoInput = {
certificate: Scalars['String']['input'];
fingerprint?: InputMaybe<Scalars['String']['input']>;
id: Scalars['String']['input'];
issuer: Scalars['String']['input'];
name: Scalars['String']['input'];
ssoURL: Scalars['String']['input'];
};

export type SetupSsoOutput = {
__typename?: 'SetupSsoOutput';
id: Scalars['String']['output'];
issuer: Scalars['String']['output'];
name: Scalars['String']['output'];
status: SsoIdentityProviderStatus;
type: IdpType;
};

/** Sort Directions */
export enum SortDirection {
Asc = 'ASC',
Expand Down Expand Up @@ -1368,11 +1507,13 @@ export type UpdateWorkspaceInput = {
displayName?: InputMaybe<Scalars['String']['input']>;
domainName?: InputMaybe<Scalars['String']['input']>;
inviteHash?: InputMaybe<Scalars['String']['input']>;
isPublicInviteLinkEnabled?: InputMaybe<Scalars['Boolean']['input']>;
logo?: InputMaybe<Scalars['String']['input']>;
};

export type User = {
__typename?: 'User';
analyticsTinybirdJwt?: Maybe<Scalars['String']['output']>;
canImpersonate: Scalars['Boolean']['output'];
createdAt: Scalars['DateTime']['output'];
defaultAvatarUrl?: Maybe<Scalars['String']['output']>;
Expand Down Expand Up @@ -1467,6 +1608,7 @@ export type Workspace = {
featureFlags?: Maybe<Array<FeatureFlag>>;
id: Scalars['UUID']['output'];
inviteHash?: Maybe<Scalars['String']['output']>;
isPublicInviteLinkEnabled: Scalars['Boolean']['output'];
logo?: Maybe<Scalars['String']['output']>;
metadataVersion: Scalars['Float']['output'];
updatedAt: Scalars['DateTime']['output'];
Expand Down Expand Up @@ -1539,6 +1681,12 @@ export enum WorkspaceMemberTimeFormatEnum {
System = 'SYSTEM'
}

export type WorkspaceNameAndId = {
__typename?: 'WorkspaceNameAndId';
displayName?: Maybe<Scalars['String']['output']>;
id: Scalars['String']['output'];
};

export type Field = {
__typename?: 'field';
createdAt: Scalars['DateTime']['output'];
Expand Down
Loading

0 comments on commit 0f0a796

Please sign in to comment.