CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
sagemathinc

Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.

GitHub Repository: sagemathinc/cocalc
Path: blob/master/src/packages/next/components/auth/sso.tsx
Views: 687
1
/*
2
* This file is part of CoCalc: Copyright © 2022 Sagemath, Inc.
3
* License: MS-RSL – see LICENSE.md for details
4
*/
5
6
import { Alert, Avatar, Space, Tooltip, Typography } from "antd";
7
import { useRouter } from "next/router";
8
import { join } from "path";
9
import { CSSProperties, ReactNode, useMemo } from "react";
10
11
import { Icon } from "@cocalc/frontend/components/icon";
12
import { checkRequiredSSO } from "@cocalc/server/auth/sso/check-required-sso";
13
import { PRIMARY_SSO } from "@cocalc/util/types/passport-types";
14
import { Strategy } from "@cocalc/util/types/sso";
15
import Loading from "components/share/loading";
16
import basePath from "lib/base-path";
17
import { useCustomize } from "lib/customize";
18
19
const { Link: AntdLink } = Typography;
20
21
import styles from "./sso.module.css";
22
23
interface SSOProps {
24
size?: number;
25
style?: CSSProperties;
26
header?: ReactNode;
27
showAll?: boolean;
28
showName?: boolean;
29
}
30
31
export function getLink(strategy: string, target?: string): string {
32
// special case: private SSO mechanism, we point to the overview page
33
if (strategy === "sso") {
34
return `${join(basePath, "sso")}`;
35
}
36
// TODO: the target is ignored by the server right now -- it's not implemented
37
// and I don't know how... yet. Code is currently in src/packages/hub/auth.ts
38
return `${join(basePath, "auth", strategy)}${
39
target ? "?target=" + encodeURIComponent(target) : ""
40
}`;
41
}
42
43
export default function SSO(props: SSOProps) {
44
const { size = 60, style, header, showAll = false, showName = false } = props;
45
const { strategies } = useCustomize();
46
const ssoHREF = useSSOHref("sso");
47
48
const havePrivateSSO: boolean = useMemo(() => {
49
return showAll ? false : strategies?.some((s) => !s.public) ?? false;
50
}, [strategies]);
51
52
if (strategies == null) {
53
return <Loading style={{ fontSize: "16pt" }} />;
54
}
55
56
if (strategies.length == 0) return <></>;
57
58
function renderPrivateSSO() {
59
if (!havePrivateSSO) return;
60
61
// a fake entry to point the user to the page for private SSO login options
62
const sso: Strategy = {
63
name: "sso",
64
display: "institutional Single Sign-On",
65
icon: "api",
66
backgroundColor: "",
67
public: true,
68
exclusiveDomains: [],
69
doNotHide: true,
70
};
71
72
return (
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
}
81
82
function renderStrategies() {
83
if (strategies == null) return;
84
const s = strategies
85
.filter((s) => showAll || s.public || s.doNotHide)
86
.map((strategy) => {
87
return (
88
<StrategyAvatar
89
key={strategy.name}
90
strategy={strategy}
91
size={size}
92
showName={showName}
93
/>
94
);
95
});
96
return (
97
<div style={{ marginLeft: "-5px", textAlign: "center" }}>
98
{showName ? <Space wrap>{s}</Space> : s}
99
</div>
100
);
101
}
102
103
// The -5px is to offset the initial avatar image, since they
104
// all have a left margin.
105
return (
106
<div style={{ ...style }}>
107
{header}
108
{renderStrategies()}
109
{renderPrivateSSO()}
110
</div>
111
);
112
}
113
114
function useSSOHref(name?: string) {
115
const router = useRouter();
116
if (name == null) return "";
117
return getLink(name, join(router.basePath, router.pathname));
118
}
119
120
interface AvatarProps {
121
strategy: Pick<Strategy, "name" | "display" | "icon" | "backgroundColor">;
122
size: number;
123
noLink?: boolean;
124
toolTip?: ReactNode;
125
showName?: boolean;
126
}
127
128
export function StrategyAvatar(props: AvatarProps) {
129
const { strategy, size, noLink, toolTip, showName = false } = props;
130
const { name, display, backgroundColor } = strategy;
131
const icon = iconName();
132
const ssoHREF = useSSOHref(name);
133
134
const STYLE: CSSProperties = {
135
fontSize: `${size - 2}px`,
136
color: backgroundColor ? "white" : "black",
137
margin: "0 2px",
138
} as const;
139
140
// this derives the name of the icon, that's shown on the avatar
141
// in particular, the old public SSO mechanisms are special cases.
142
function iconName(): string {
143
// icon could be "null"
144
if (strategy.icon != null) return strategy.icon;
145
if ((PRIMARY_SSO as readonly string[]).includes(strategy.name)) {
146
return strategy.name;
147
}
148
return "link"; // a chain link, very general fallback
149
}
150
151
function renderIconImg() {
152
if (icon?.includes("://")) {
153
return (
154
<img
155
src={icon}
156
style={{
157
height: `${size - 2}px`,
158
width: `${size - 2}px`,
159
objectFit: "contain",
160
}}
161
/>
162
);
163
} else {
164
return <Icon name={icon as any} style={{ ...STYLE, backgroundColor }} />;
165
}
166
}
167
168
function renderAvatar() {
169
const avatar = (
170
<Avatar
171
shape="square"
172
size={size}
173
src={renderIconImg()}
174
gap={1}
175
className={styles.icon}
176
/>
177
);
178
179
if (noLink) {
180
return avatar;
181
} else {
182
return <a href={ssoHREF}>{avatar}</a>;
183
}
184
}
185
186
function renderIcon() {
187
if (icon?.includes("://")) return "";
188
return (
189
<Icon
190
name={icon as any}
191
style={{ fontSize: "14pt", marginRight: "10px" }}
192
/>
193
);
194
}
195
196
function renderName() {
197
if (!showName) return;
198
return (
199
<div style={{ textAlign: "center", whiteSpace: "nowrap" }}>{display}</div>
200
);
201
}
202
203
return (
204
<Tooltip
205
title={
206
<>
207
{renderIcon()} {toolTip ?? <>Use your {display} account.</>}
208
</>
209
}
210
color={backgroundColor}
211
>
212
<div style={{ display: "inline-block" }}>
213
{renderAvatar()}
214
{renderName()}
215
</div>
216
</Tooltip>
217
);
218
}
219
220
export function RequiredSSO({ strategy }: { strategy?: Strategy }) {
221
if (strategy == null) return null;
222
if (strategy.name == "null")
223
return <Alert type="error" message={"SSO Strategy not defined!"} />;
224
const ssoLink = join(basePath, "sso", strategy.name);
225
return (
226
<Alert
227
style={{ margin: "15px 0" }}
228
type="warning"
229
showIcon={false}
230
message={`Single Sign-On required`}
231
description={
232
<>
233
<p>
234
You must sign up using the{" "}
235
<AntdLink strong underline href={ssoLink}>
236
{strategy.display}
237
</AntdLink>{" "}
238
Single Sign-On strategy.
239
</p>
240
<p style={{ textAlign: "center" }}>
241
<StrategyAvatar
242
key={strategy.name}
243
strategy={strategy}
244
size={120}
245
/>
246
</p>
247
</>
248
}
249
/>
250
);
251
}
252
253
// 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 domains
257
export function useRequiredSSO(
258
strategies: Strategy[] | undefined,
259
email: string | undefined,
260
): Strategy | undefined {
261
return useMemo(() => {
262
return checkRequiredSSO({ email, strategies });
263
}, [strategies == null, email]);
264
}
265
266