forked from calcom/cal.com
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add log in with Google and SAML (calcom#1192)
* Add log in with Google * Fix merge conflicts * Merge branch 'main' into feature/copy-add-identity-provider # Conflicts: # pages/api/auth/[...nextauth].tsx # pages/api/auth/forgot-password.ts # pages/settings/security.tsx # prisma/schema.prisma # public/static/locales/en/common.json * WIP: SAML login * fixed login * fixed verified_email check for Google * tweaks to padding * added BoxyHQ SAML service to local docker-compose * identityProvider is missing from the select clause * user may be undefined * fix for yarn build * Added SAML configuration to Settings -> Security page * UI tweaks * get saml login flag from the server * UI tweaks * moved SAMLConfiguration to a component in ee * updated saml migration date * fixed merge conflict * fixed merge conflict * lint fixes * check-types fixes * check-types fixes * fixed type errors * updated docker image for SAML Jackson * added api keys config * added default values for SAML_TENANT_ID and SAML_PRODUCT_ID * - move all env vars related to saml into a separate file for easy access - added SAML_ADMINS comma separated list of emails that will be able to configure the SAML metadata * cleanup after merging main * revert mistake during merge * revert mistake during merge * set info text to indicate SAML has been configured. * tweaks to text * tweaks to text * i18n text * i18n text * tweak * use a separate db for saml to avoid Prisma schema being out of sync * use separate docker-compose file for saml * padding tweak * Prepare for implementing SAML login for the hosted solution * WIP: Support for SAML in the hosted solution * teams view has changed, adjusting saml changes accordingly * enabled SAML only for PRO plan * if user was invited and signs in via saml/google then update the user record * WIP: embed saml lib * 302 instead of 307 * no separate docker-compose file for saml * - ogs cleanup - type fixes * fixed types for jackson * cleaned up cors, not needed by the oauth flow * updated jackson to support encryption at rest * updated saml-jackson lib * allow only the required http methods * fixed issue with latest merge with main * - Added instructions for deploying SAML support - Tweaked SAML audience identifier * fixed check for hosted Cal instance * Added a new route to initiate Google and SAML login flows * updated saml-jackson lib (node engine version is now 14.x or above) * moved SAML instructions from Google Docs to a docs file * moved randomString to lib * comment SAML_DATABASE_URL and SAML_ADMINS in .env.example so that default is SAML off. * fixed path to randomString * updated @boxyhq/saml-jackson to v0.3.0 * fixed TS errors * tweaked SAML config UI * fixed types * added e2e test for Google login * setup secrets for Google login test * test for OAuth login buttons (Google and SAML) * enabled saml for the test * added test for SAML config UI * fixed nextauth import * use pkce flow * tweaked NextAuth config for saml * updated saml-jackson * added ability to delete SAML configuration * SAML variables explainers and refactoring * Prevents constant collision * Var name changes * Env explainers * better validation for email Co-authored-by: Omar López <zomars@me.com> * enabled GOOGLE_API_CREDENTIALS in e2e tests (Github Actions secret) * cleanup (will create an issue to handle forgot password for Google and SAML identities) Co-authored-by: Chris <76668588+bytesbuffer@users.noreply.github.com> Co-authored-by: Omar López <zomars@me.com>
- Loading branch information
1 parent
ffc0f46
commit 1a20b0a
Showing
44 changed files
with
2,723 additions
and
484 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
# SAML Registration with Identity Providers | ||
|
||
This guide explains the settings you’d need to use to configure SAML with your Identity Provider. Once this is set up you should get an XML metadata file that should then be uploaded on your Cal.com self-hosted instance. | ||
|
||
> **Note:** Please do not add a trailing slash at the end of the URLs. Create them exactly as shown below. | ||
**Assertion consumer service URL / Single Sign-On URL / Destination URL:** [http://localhost:3000/api/auth/saml/callback](http://localhost:3000/api/auth/saml/callback) [Replace this with the URL for your self-hosted Cal instance] | ||
|
||
**Entity ID / Identifier / Audience URI / Audience Restriction:** https://saml.cal.com | ||
|
||
**Response:** Signed | ||
|
||
**Assertion Signature:** Signed | ||
|
||
**Signature Algorithm:** RSA-SHA256 | ||
|
||
**Assertion Encryption:** Unencrypted | ||
|
||
**Mapping Attributes / Attribute Statements:** | ||
|
||
id -> user.id | ||
|
||
email -> user.email | ||
|
||
firstName -> user.firstName | ||
|
||
lastName -> user.lastName |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,162 @@ | ||
import React, { useEffect, useState, useRef } from "react"; | ||
|
||
import { useLocale } from "@lib/hooks/useLocale"; | ||
import showToast from "@lib/notification"; | ||
import { trpc } from "@lib/trpc"; | ||
|
||
import { Dialog, DialogTrigger } from "@components/Dialog"; | ||
import ConfirmationDialogContent from "@components/dialog/ConfirmationDialogContent"; | ||
import { Alert } from "@components/ui/Alert"; | ||
import Badge from "@components/ui/Badge"; | ||
import Button from "@components/ui/Button"; | ||
|
||
export default function SAMLConfiguration({ | ||
teamsView, | ||
teamId, | ||
}: { | ||
teamsView: boolean; | ||
teamId: null | undefined | number; | ||
}) { | ||
const [isSAMLLoginEnabled, setIsSAMLLoginEnabled] = useState(false); | ||
const [samlConfig, setSAMLConfig] = useState<string | null>(null); | ||
|
||
const query = trpc.useQuery(["viewer.showSAMLView", { teamsView, teamId }]); | ||
|
||
useEffect(() => { | ||
const data = query.data; | ||
setIsSAMLLoginEnabled(data?.isSAMLLoginEnabled ?? false); | ||
setSAMLConfig(data?.provider ?? null); | ||
}, [query.data]); | ||
|
||
const mutation = trpc.useMutation("viewer.updateSAMLConfig", { | ||
onSuccess: (data: { provider: string | undefined }) => { | ||
showToast(t("saml_config_updated_successfully"), "success"); | ||
setHasErrors(false); // dismiss any open errors | ||
setSAMLConfig(data?.provider ?? null); | ||
samlConfigRef.current.value = ""; | ||
}, | ||
onError: () => { | ||
setHasErrors(true); | ||
setErrorMessage(t("saml_configuration_update_failed")); | ||
document?.getElementsByTagName("main")[0]?.scrollTo({ top: 0, behavior: "smooth" }); | ||
}, | ||
}); | ||
|
||
const deleteMutation = trpc.useMutation("viewer.deleteSAMLConfig", { | ||
onSuccess: () => { | ||
showToast(t("saml_config_deleted_successfully"), "success"); | ||
setHasErrors(false); // dismiss any open errors | ||
setSAMLConfig(null); | ||
samlConfigRef.current.value = ""; | ||
}, | ||
onError: () => { | ||
setHasErrors(true); | ||
setErrorMessage(t("saml_configuration_delete_failed")); | ||
document?.getElementsByTagName("main")[0]?.scrollTo({ top: 0, behavior: "smooth" }); | ||
}, | ||
}); | ||
|
||
const samlConfigRef = useRef<HTMLTextAreaElement>() as React.MutableRefObject<HTMLTextAreaElement>; | ||
|
||
const [hasErrors, setHasErrors] = useState(false); | ||
const [errorMessage, setErrorMessage] = useState(""); | ||
|
||
async function updateSAMLConfigHandler(event: React.FormEvent<HTMLElement>) { | ||
event.preventDefault(); | ||
|
||
const rawMetadata = samlConfigRef.current.value; | ||
|
||
mutation.mutate({ | ||
rawMetadata: rawMetadata, | ||
teamId, | ||
}); | ||
} | ||
|
||
async function deleteSAMLConfigHandler(event: React.MouseEvent<HTMLElement, MouseEvent>) { | ||
event.preventDefault(); | ||
|
||
deleteMutation.mutate({ | ||
teamId, | ||
}); | ||
} | ||
|
||
const { t } = useLocale(); | ||
return ( | ||
<> | ||
<hr className="mt-8" /> | ||
|
||
{isSAMLLoginEnabled ? ( | ||
<> | ||
<div className="mt-6"> | ||
<h2 className="font-cal text-lg leading-6 font-medium text-gray-900"> | ||
{t("saml_configuration")} | ||
<Badge className="text-xs ml-2" variant={samlConfig ? "success" : "gray"}> | ||
{samlConfig ? t("enabled") : t("disabled")} | ||
</Badge> | ||
{samlConfig ? ( | ||
<> | ||
<Badge className="text-xs ml-2" variant={"success"}> | ||
{samlConfig ? samlConfig : ""} | ||
</Badge> | ||
</> | ||
) : null} | ||
</h2> | ||
</div> | ||
|
||
{samlConfig ? ( | ||
<div className="mt-2 flex"> | ||
<Dialog> | ||
<DialogTrigger asChild> | ||
<Button | ||
color="warn" | ||
type="button" | ||
onClick={(e) => { | ||
e.stopPropagation(); | ||
}}> | ||
{t("delete_saml_configuration")} | ||
</Button> | ||
</DialogTrigger> | ||
<ConfirmationDialogContent | ||
variety="danger" | ||
title={t("delete_saml_configuration")} | ||
confirmBtnText={t("confirm_delete_saml_configuration")} | ||
cancelBtnText={t("cancel")} | ||
onConfirm={deleteSAMLConfigHandler}> | ||
{t("delete_saml_configuration_confirmation_message")} | ||
</ConfirmationDialogContent> | ||
</Dialog> | ||
</div> | ||
) : ( | ||
<p className="mt-1 text-sm text-gray-500">{!samlConfig ? t("saml_not_configured_yet") : ""}</p> | ||
)} | ||
|
||
<p className="mt-1 text-sm text-gray-500">{t("saml_configuration_description")}</p> | ||
|
||
<form className="mt-3 divide-y divide-gray-200 lg:col-span-9" onSubmit={updateSAMLConfigHandler}> | ||
{hasErrors && <Alert severity="error" title={errorMessage} />} | ||
|
||
<textarea | ||
data-testid="saml_config" | ||
ref={samlConfigRef} | ||
name="saml_config" | ||
id="saml_config" | ||
required={true} | ||
rows={10} | ||
className="block w-full border-gray-300 rounded-md shadow-sm dark:bg-black dark:text-white dark:border-gray-900 focus:ring-black focus:border-black sm:text-sm" | ||
placeholder={t("saml_configuration_placeholder")} | ||
/> | ||
|
||
<div className="flex justify-end py-8"> | ||
<button | ||
type="submit" | ||
className="ml-2 bg-neutral-900 border border-transparent rounded-sm shadow-sm py-2 px-4 inline-flex justify-center text-sm font-medium text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-black"> | ||
{t("save")} | ||
</button> | ||
</div> | ||
<hr className="mt-4" /> | ||
</form> | ||
</> | ||
) : null} | ||
</> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
import jackson, { IAPIController, IOAuthController, JacksonOption } from "@boxyhq/saml-jackson"; | ||
|
||
import { BASE_URL } from "@lib/config/constants"; | ||
import { samlDatabaseUrl } from "@lib/saml"; | ||
|
||
// Set the required options. Refer to https://github.com/boxyhq/jackson#configuration for the full list | ||
const opts: JacksonOption = { | ||
externalUrl: BASE_URL, | ||
samlPath: "/api/auth/saml/callback", | ||
db: { | ||
engine: "sql", | ||
type: "postgres", | ||
url: samlDatabaseUrl, | ||
encryptionKey: process.env.CALENDSO_ENCRYPTION_KEY, | ||
}, | ||
samlAudience: "https://saml.cal.com", | ||
}; | ||
|
||
let apiController: IAPIController; | ||
let oauthController: IOAuthController; | ||
|
||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
const g = global as any; | ||
|
||
export default async function init() { | ||
if (!g.apiController || !g.oauthController) { | ||
const ret = await jackson(opts); | ||
apiController = ret.apiController; | ||
oauthController = ret.oauthController; | ||
g.apiController = apiController; | ||
g.oauthController = oauthController; | ||
} else { | ||
apiController = g.apiController; | ||
oauthController = g.oauthController; | ||
} | ||
|
||
return { | ||
apiController, | ||
oauthController, | ||
}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
export const randomString = function (length = 12) { | ||
let result = ""; | ||
const characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; | ||
const charactersLength = characters.length; | ||
for (let i = 0; i < length; i++) { | ||
result += characters.charAt(Math.floor(Math.random() * charactersLength)); | ||
} | ||
return result; | ||
}; |
Oops, something went wrong.