Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
sagemathinc
GitHub Repository: sagemathinc/cocalc
Path: blob/master/src/packages/next/components/auth/sso.tsx
5827 views
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/util/auth-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
updateOnLogin: false,
71
};
72
73
return (
74
<div style={{ marginLeft: "-5px", marginTop: "10px" }}>
75
<a href={ssoHREF}>
76
{"Institutional Single Sign-On: "}
77
<StrategyAvatar key={"sso"} strategy={sso} size={size} />
78
</a>
79
</div>
80
);
81
}
82
83
function renderStrategies() {
84
if (strategies == null) return;
85
const s = strategies
86
.filter((s) => showAll || s.public || s.doNotHide)
87
.map((strategy) => {
88
return (
89
<StrategyAvatar
90
key={strategy.name}
91
strategy={strategy}
92
size={size}
93
showName={showName}
94
/>
95
);
96
});
97
return (
98
<div style={{ marginLeft: "-5px", textAlign: "center" }}>
99
{showName ? <Space wrap>{s}</Space> : s}
100
</div>
101
);
102
}
103
104
// The -5px is to offset the initial avatar image, since they
105
// all have a left margin.
106
return (
107
<div style={{ ...style }}>
108
{header}
109
{renderStrategies()}
110
{renderPrivateSSO()}
111
</div>
112
);
113
}
114
115
function useSSOHref(name?: string) {
116
const router = useRouter();
117
if (name == null) return "";
118
return getLink(name, join(router.basePath, router.pathname));
119
}
120
121
interface AvatarProps {
122
strategy: Pick<Strategy, "name" | "display" | "icon" | "backgroundColor">;
123
size: number;
124
noLink?: boolean;
125
toolTip?: ReactNode;
126
showName?: boolean;
127
}
128
129
export function StrategyAvatar(props: AvatarProps) {
130
const { strategy, size, noLink, toolTip, showName = false } = props;
131
const { name, display, backgroundColor } = strategy;
132
const icon = iconName();
133
const ssoHREF = useSSOHref(name);
134
135
const STYLE: CSSProperties = {
136
fontSize: `${size - 2}px`,
137
color: backgroundColor ? "white" : "black",
138
margin: "0 2px",
139
} as const;
140
141
// this derives the name of the icon, that's shown on the avatar
142
// in particular, the old public SSO mechanisms are special cases.
143
function iconName(): string {
144
// icon could be "null"
145
if (strategy.icon != null) return strategy.icon;
146
if ((PRIMARY_SSO as readonly string[]).includes(strategy.name)) {
147
return strategy.name;
148
}
149
return "link"; // a chain link, very general fallback
150
}
151
152
function renderIconImg() {
153
if (icon?.includes("://")) {
154
return (
155
<img
156
src={icon}
157
style={{
158
height: `${size - 2}px`,
159
width: `${size - 2}px`,
160
objectFit: "contain",
161
}}
162
/>
163
);
164
} else {
165
return <Icon name={icon as any} style={{ ...STYLE, backgroundColor }} />;
166
}
167
}
168
169
function renderAvatar() {
170
const avatar = (
171
<Avatar
172
shape="square"
173
size={size}
174
src={renderIconImg()}
175
gap={1}
176
className={styles.icon}
177
/>
178
);
179
180
if (noLink) {
181
return avatar;
182
} else {
183
return <a href={ssoHREF}>{avatar}</a>;
184
}
185
}
186
187
function renderIcon() {
188
if (icon?.includes("://")) return "";
189
return (
190
<Icon
191
name={icon as any}
192
style={{ fontSize: "14pt", marginRight: "10px" }}
193
/>
194
);
195
}
196
197
function renderName() {
198
if (!showName) return;
199
return (
200
<div style={{ textAlign: "center", whiteSpace: "nowrap" }}>{display}</div>
201
);
202
}
203
204
return (
205
<Tooltip
206
title={
207
<>
208
{renderIcon()} {toolTip ?? <>Use your {display} account.</>}
209
</>
210
}
211
color={backgroundColor}
212
>
213
<div style={{ display: "inline-block" }}>
214
{renderAvatar()}
215
{renderName()}
216
</div>
217
</Tooltip>
218
);
219
}
220
221
export function RequiredSSO({ strategy }: { strategy?: Strategy }) {
222
if (strategy == null) return null;
223
if (strategy.name == "null")
224
return <Alert type="error" message={"SSO Strategy not defined!"} />;
225
const ssoLink = join(basePath, "sso", strategy.name);
226
return (
227
<Alert
228
style={{ margin: "15px 0" }}
229
type="warning"
230
showIcon={false}
231
message={`Single Sign-On required`}
232
description={
233
<>
234
<p>
235
You must sign up using the{" "}
236
<AntdLink strong underline href={ssoLink}>
237
{strategy.display}
238
</AntdLink>{" "}
239
Single Sign-On strategy.
240
</p>
241
<p style={{ textAlign: "center" }}>
242
<StrategyAvatar
243
key={strategy.name}
244
strategy={strategy}
245
size={120}
246
/>
247
</p>
248
</>
249
}
250
/>
251
);
252
}
253
254
// based on (partially) entered email address.
255
// if user has to sign up via SSO, this will tell which strategy to use.
256
// this also checks for subdomains via a simple heuristic – the precise test is on the backend.
257
// hence this should be good enough to catch @email.foo.edu for foo.edu domains
258
export function useRequiredSSO(
259
strategies: Strategy[] | undefined,
260
email: string | undefined,
261
): Strategy | undefined {
262
return useMemo(() => {
263
return checkRequiredSSO({ email, strategies });
264
}, [strategies == null, email]);
265
}
266
267