Skip to content

Commit f024662

Browse files
committed
sso: tweak the checkRequiredSSO logic (match "*" at the end, if no others match) – and write tests to nail this down
1 parent 7a3da33 commit f024662

File tree

3 files changed

+93
-7
lines changed

3 files changed

+93
-7
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import {
2+
emailBelongsToDomain,
3+
getEmailDomain,
4+
checkRequiredSSO,
5+
} from "./auth-check-required-sso";
6+
import { Strategy } from "./types/sso";
7+
8+
const SSO = {
9+
display: "",
10+
backgroundColor: "",
11+
public: false,
12+
doNotHide: true,
13+
updateOnLogin: true,
14+
} as const;
15+
16+
describe("Check Required SSO", () => {
17+
test("getEmailDomain", () => {
18+
expect(getEmailDomain("[email protected]")).toBe("bar.com");
19+
expect(getEmailDomain("[email protected]")).toBe("bar.co.uk");
20+
});
21+
22+
test("emailBelongsToDomain", () => {
23+
expect(emailBelongsToDomain("foo.com", "foo.com")).toBe(true);
24+
expect(emailBelongsToDomain("bar.foo.com", "foo.com")).toBe(true);
25+
expect(emailBelongsToDomain("foo.com", "bar.com")).toBe(false);
26+
expect(emailBelongsToDomain("foo.com", "foo.co.uk")).toBe(false);
27+
expect(emailBelongsToDomain("foo.com", "foo.com.uk")).toBe(false);
28+
expect(emailBelongsToDomain("foobar.com", "bar.com")).toBe(false);
29+
expect(emailBelongsToDomain("foobar.com", "bar.com")).toBe(false);
30+
expect(emailBelongsToDomain("foobar.com", "*")).toBe(false);
31+
});
32+
33+
const foo = { name: "foo", exclusiveDomains: ["foo.co.uk"], ...SSO };
34+
const bar = { name: "bar", exclusiveDomains: ["*"], ...SSO };
35+
const baz = {
36+
name: "baz",
37+
exclusiveDomains: ["baz.com", "abc.com"],
38+
...SSO,
39+
};
40+
41+
test("checkRequiredSSO", () => {
42+
const strategies: Strategy[] = [foo, baz] as const;
43+
44+
expect(checkRequiredSSO({ email: "[email protected]", strategies })?.name).toEqual(
45+
"baz",
46+
);
47+
expect(
48+
checkRequiredSSO({ email: "[email protected]", strategies })?.name,
49+
).toEqual("baz");
50+
expect(
51+
checkRequiredSSO({ email: "[email protected]", strategies })?.name,
52+
).toEqual("foo");
53+
// no match on naive substring from the right
54+
expect(
55+
checkRequiredSSO({ email: "[email protected]", strategies }),
56+
).toBeUndefined();
57+
// no catch-all for an unrelated domain, returns no strategy
58+
expect(
59+
checkRequiredSSO({ email: "[email protected]", strategies }),
60+
).toBeUndefined();
61+
});
62+
63+
test("checkRequiredSSO/catchall", () => {
64+
const strategies: Strategy[] = [foo, bar, baz] as const;
65+
66+
expect(checkRequiredSSO({ email: "[email protected]", strategies })?.name).toEqual(
67+
"baz",
68+
);
69+
expect(
70+
checkRequiredSSO({ email: "[email protected]", strategies })?.name,
71+
).toEqual("baz");
72+
expect(
73+
checkRequiredSSO({ email: "[email protected]", strategies })?.name,
74+
).toEqual("foo");
75+
// this is the essential difference to above
76+
expect(
77+
checkRequiredSSO({ email: "[email protected]", strategies })?.name,
78+
).toEqual("bar");
79+
});
80+
});

src/packages/util/auth-check-required-sso.ts

+12-6
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,11 @@ interface Opts {
1616
* which is configured to be an "exclusive" domain, then return the Strategy.
1717
* This also matches subdomains, i.e. "[email protected]" is goverend by "baz.edu".
1818
*
19-
* Optionally, if @specificStrategy is set, only that strategy is checked!
19+
* Special case: an sso domain "*" covers all domains, not covered by any other
20+
* exclusive SSO strategy. If there is just one such "*"-SSO strategy, it will deal with all
21+
* accounts.
22+
*
23+
* Optionally, if @specificStrategy is set, only that strategy or "*" is checked!
2024
*/
2125
export function checkRequiredSSO(opts: Opts): Strategy | undefined {
2226
const { email, strategies, specificStrategy } = opts;
@@ -29,11 +33,18 @@ export function checkRequiredSSO(opts: Opts): Strategy | undefined {
2933
for (const strategy of strategies) {
3034
if (specificStrategy && specificStrategy !== strategy.name) continue;
3135
for (const ssoDomain of strategy.exclusiveDomains) {
36+
if (ssoDomain === "*") continue; // dealt with below
3237
if (emailBelongsToDomain(emailDomain, ssoDomain)) {
3338
return strategy;
3439
}
3540
}
3641
}
42+
// At this point, we either matched an existing strategy (above) or there is a "*" strategy
43+
for (const strategy of strategies) {
44+
if (strategy.exclusiveDomains.includes("*")) {
45+
return strategy;
46+
}
47+
}
3748
}
3849

3950
export function getEmailDomain(email: string): string {
@@ -43,15 +54,10 @@ export function getEmailDomain(email: string): string {
4354
/**
4455
* This checks if the email's domain is either exactly the ssoDomain or a subdomain.
4556
* E.g. for "foo.edu", an email "[email protected]" is covered as well.
46-
*
47-
* Special case: an sso domain "*" covers all domains. This is kind of a complete "take over",
48-
* because all accounts on that instance of CoCalc have to go through that SSO mechanism.
49-
* Note: In that case, it makes no sense to have more than one SSO mechanism configured.
5057
*/
5158
export function emailBelongsToDomain(
5259
emailDomain: string,
5360
ssoDomain: string,
5461
): boolean {
55-
if (ssoDomain === "*") return true;
5662
return emailDomain === ssoDomain || emailDomain.endsWith(`.${ssoDomain}`);
5763
}

src/packages/util/types/sso.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ export interface Strategy {
1010
icon?: string; // name of or URL to icon to display for SSO
1111
backgroundColor: string; // background color for icon, if not a link
1212
public: boolean; // true for general broad audiences, like google, default true
13-
exclusiveDomains: string[]; // list of domains, e.g. ["foo.com"], which must go through that SSO mechanism (and block regular email signup)
13+
exclusiveDomains: string[]; // list of domains, e.g. ["foo.com"], which must go through that SSO mechanism (and block regular email signup). The domain "*" implies all domains are "taken over" by that startegy – only use that once for one strategy.
1414
doNotHide: boolean; // if true and a public=false, show it directly on the login/signup page
1515
updateOnLogin: boolean; // if true and account is goverend by an exclusiveDomain, user's are not allowed to change their first and last name
1616
}

0 commit comments

Comments
 (0)