Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Path: blob/master/src/packages/next/components/auth/sso.tsx
Views: 687
/*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/server/auth/sso/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,69};7071return (72<div style={{ marginLeft: "-5px", marginTop: "10px" }}>73<a href={ssoHREF}>74{"Institutional Single Sign-On: "}75<StrategyAvatar key={"sso"} strategy={sso} size={size} />76</a>77</div>78);79}8081function renderStrategies() {82if (strategies == null) return;83const s = strategies84.filter((s) => showAll || s.public || s.doNotHide)85.map((strategy) => {86return (87<StrategyAvatar88key={strategy.name}89strategy={strategy}90size={size}91showName={showName}92/>93);94});95return (96<div style={{ marginLeft: "-5px", textAlign: "center" }}>97{showName ? <Space wrap>{s}</Space> : s}98</div>99);100}101102// The -5px is to offset the initial avatar image, since they103// all have a left margin.104return (105<div style={{ ...style }}>106{header}107{renderStrategies()}108{renderPrivateSSO()}109</div>110);111}112113function useSSOHref(name?: string) {114const router = useRouter();115if (name == null) return "";116return getLink(name, join(router.basePath, router.pathname));117}118119interface AvatarProps {120strategy: Pick<Strategy, "name" | "display" | "icon" | "backgroundColor">;121size: number;122noLink?: boolean;123toolTip?: ReactNode;124showName?: boolean;125}126127export function StrategyAvatar(props: AvatarProps) {128const { strategy, size, noLink, toolTip, showName = false } = props;129const { name, display, backgroundColor } = strategy;130const icon = iconName();131const ssoHREF = useSSOHref(name);132133const STYLE: CSSProperties = {134fontSize: `${size - 2}px`,135color: backgroundColor ? "white" : "black",136margin: "0 2px",137} as const;138139// this derives the name of the icon, that's shown on the avatar140// in particular, the old public SSO mechanisms are special cases.141function iconName(): string {142// icon could be "null"143if (strategy.icon != null) return strategy.icon;144if ((PRIMARY_SSO as readonly string[]).includes(strategy.name)) {145return strategy.name;146}147return "link"; // a chain link, very general fallback148}149150function renderIconImg() {151if (icon?.includes("://")) {152return (153<img154src={icon}155style={{156height: `${size - 2}px`,157width: `${size - 2}px`,158objectFit: "contain",159}}160/>161);162} else {163return <Icon name={icon as any} style={{ ...STYLE, backgroundColor }} />;164}165}166167function renderAvatar() {168const avatar = (169<Avatar170shape="square"171size={size}172src={renderIconImg()}173gap={1}174className={styles.icon}175/>176);177178if (noLink) {179return avatar;180} else {181return <a href={ssoHREF}>{avatar}</a>;182}183}184185function renderIcon() {186if (icon?.includes("://")) return "";187return (188<Icon189name={icon as any}190style={{ fontSize: "14pt", marginRight: "10px" }}191/>192);193}194195function renderName() {196if (!showName) return;197return (198<div style={{ textAlign: "center", whiteSpace: "nowrap" }}>{display}</div>199);200}201202return (203<Tooltip204title={205<>206{renderIcon()} {toolTip ?? <>Use your {display} account.</>}207</>208}209color={backgroundColor}210>211<div style={{ display: "inline-block" }}>212{renderAvatar()}213{renderName()}214</div>215</Tooltip>216);217}218219export function RequiredSSO({ strategy }: { strategy?: Strategy }) {220if (strategy == null) return null;221if (strategy.name == "null")222return <Alert type="error" message={"SSO Strategy not defined!"} />;223const ssoLink = join(basePath, "sso", strategy.name);224return (225<Alert226style={{ margin: "15px 0" }}227type="warning"228showIcon={false}229message={`Single Sign-On required`}230description={231<>232<p>233You must sign up using the{" "}234<AntdLink strong underline href={ssoLink}>235{strategy.display}236</AntdLink>{" "}237Single Sign-On strategy.238</p>239<p style={{ textAlign: "center" }}>240<StrategyAvatar241key={strategy.name}242strategy={strategy}243size={120}244/>245</p>246</>247}248/>249);250}251252// based on (partially) entered email address.253// if user has to sign up via SSO, this will tell which strategy to use.254// this also checks for subdomains via a simple heuristic – the precise test is on the backend.255// hence this should be good enough to catch @email.foo.edu for foo.edu domains256export function useRequiredSSO(257strategies: Strategy[] | undefined,258email: string | undefined,259): Strategy | undefined {260return useMemo(() => {261return checkRequiredSSO({ email, strategies });262}, [strategies == null, email]);263}264265266