Path: blob/master/src/packages/next/components/auth/sso.tsx
5827 views
/*1* This file is part of CoCalc: Copyright © 2022 Sagemath, Inc.2* License: MS-RSL – see LICENSE.md for details3*/45import { Alert, Avatar, Space, Tooltip, Typography } from "antd";6import { useRouter } from "next/router";7import { join } from "path";8import { CSSProperties, ReactNode, useMemo } from "react";910import { Icon } from "@cocalc/frontend/components/icon";11import { checkRequiredSSO } from "@cocalc/util/auth-check-required-sso";12import { PRIMARY_SSO } from "@cocalc/util/types/passport-types";13import { Strategy } from "@cocalc/util/types/sso";14import Loading from "components/share/loading";15import basePath from "lib/base-path";16import { useCustomize } from "lib/customize";1718const { Link: AntdLink } = Typography;1920import styles from "./sso.module.css";2122interface SSOProps {23size?: number;24style?: CSSProperties;25header?: ReactNode;26showAll?: boolean;27showName?: boolean;28}2930export function getLink(strategy: string, target?: string): string {31// special case: private SSO mechanism, we point to the overview page32if (strategy === "sso") {33return `${join(basePath, "sso")}`;34}35// TODO: the target is ignored by the server right now -- it's not implemented36// and I don't know how... yet. Code is currently in src/packages/hub/auth.ts37return `${join(basePath, "auth", strategy)}${38target ? "?target=" + encodeURIComponent(target) : ""39}`;40}4142export default function SSO(props: SSOProps) {43const { size = 60, style, header, showAll = false, showName = false } = props;44const { strategies } = useCustomize();45const ssoHREF = useSSOHref("sso");4647const havePrivateSSO: boolean = useMemo(() => {48return showAll ? false : strategies?.some((s) => !s.public) ?? false;49}, [strategies]);5051if (strategies == null) {52return <Loading style={{ fontSize: "16pt" }} />;53}5455if (strategies.length == 0) return <></>;5657function renderPrivateSSO() {58if (!havePrivateSSO) return;5960// a fake entry to point the user to the page for private SSO login options61const sso: Strategy = {62name: "sso",63display: "institutional Single Sign-On",64icon: "api",65backgroundColor: "",66public: true,67exclusiveDomains: [],68doNotHide: true,69updateOnLogin: false,70};7172return (73<div style={{ marginLeft: "-5px", marginTop: "10px" }}>74<a href={ssoHREF}>75{"Institutional Single Sign-On: "}76<StrategyAvatar key={"sso"} strategy={sso} size={size} />77</a>78</div>79);80}8182function renderStrategies() {83if (strategies == null) return;84const s = strategies85.filter((s) => showAll || s.public || s.doNotHide)86.map((strategy) => {87return (88<StrategyAvatar89key={strategy.name}90strategy={strategy}91size={size}92showName={showName}93/>94);95});96return (97<div style={{ marginLeft: "-5px", textAlign: "center" }}>98{showName ? <Space wrap>{s}</Space> : s}99</div>100);101}102103// The -5px is to offset the initial avatar image, since they104// all have a left margin.105return (106<div style={{ ...style }}>107{header}108{renderStrategies()}109{renderPrivateSSO()}110</div>111);112}113114function useSSOHref(name?: string) {115const router = useRouter();116if (name == null) return "";117return getLink(name, join(router.basePath, router.pathname));118}119120interface AvatarProps {121strategy: Pick<Strategy, "name" | "display" | "icon" | "backgroundColor">;122size: number;123noLink?: boolean;124toolTip?: ReactNode;125showName?: boolean;126}127128export function StrategyAvatar(props: AvatarProps) {129const { strategy, size, noLink, toolTip, showName = false } = props;130const { name, display, backgroundColor } = strategy;131const icon = iconName();132const ssoHREF = useSSOHref(name);133134const STYLE: CSSProperties = {135fontSize: `${size - 2}px`,136color: backgroundColor ? "white" : "black",137margin: "0 2px",138} as const;139140// this derives the name of the icon, that's shown on the avatar141// in particular, the old public SSO mechanisms are special cases.142function iconName(): string {143// icon could be "null"144if (strategy.icon != null) return strategy.icon;145if ((PRIMARY_SSO as readonly string[]).includes(strategy.name)) {146return strategy.name;147}148return "link"; // a chain link, very general fallback149}150151function renderIconImg() {152if (icon?.includes("://")) {153return (154<img155src={icon}156style={{157height: `${size - 2}px`,158width: `${size - 2}px`,159objectFit: "contain",160}}161/>162);163} else {164return <Icon name={icon as any} style={{ ...STYLE, backgroundColor }} />;165}166}167168function renderAvatar() {169const avatar = (170<Avatar171shape="square"172size={size}173src={renderIconImg()}174gap={1}175className={styles.icon}176/>177);178179if (noLink) {180return avatar;181} else {182return <a href={ssoHREF}>{avatar}</a>;183}184}185186function renderIcon() {187if (icon?.includes("://")) return "";188return (189<Icon190name={icon as any}191style={{ fontSize: "14pt", marginRight: "10px" }}192/>193);194}195196function renderName() {197if (!showName) return;198return (199<div style={{ textAlign: "center", whiteSpace: "nowrap" }}>{display}</div>200);201}202203return (204<Tooltip205title={206<>207{renderIcon()} {toolTip ?? <>Use your {display} account.</>}208</>209}210color={backgroundColor}211>212<div style={{ display: "inline-block" }}>213{renderAvatar()}214{renderName()}215</div>216</Tooltip>217);218}219220export function RequiredSSO({ strategy }: { strategy?: Strategy }) {221if (strategy == null) return null;222if (strategy.name == "null")223return <Alert type="error" message={"SSO Strategy not defined!"} />;224const ssoLink = join(basePath, "sso", strategy.name);225return (226<Alert227style={{ margin: "15px 0" }}228type="warning"229showIcon={false}230message={`Single Sign-On required`}231description={232<>233<p>234You must sign up using the{" "}235<AntdLink strong underline href={ssoLink}>236{strategy.display}237</AntdLink>{" "}238Single Sign-On strategy.239</p>240<p style={{ textAlign: "center" }}>241<StrategyAvatar242key={strategy.name}243strategy={strategy}244size={120}245/>246</p>247</>248}249/>250);251}252253// based on (partially) entered email address.254// if user has to sign up via SSO, this will tell which strategy to use.255// this also checks for subdomains via a simple heuristic – the precise test is on the backend.256// hence this should be good enough to catch @email.foo.edu for foo.edu domains257export function useRequiredSSO(258strategies: Strategy[] | undefined,259email: string | undefined,260): Strategy | undefined {261return useMemo(() => {262return checkRequiredSSO({ email, strategies });263}, [strategies == null, email]);264}265266267