Skip to content

Commit 56e7230

Browse files
committed
sso/update email on login: more debugging and double checking
1 parent 0cc199c commit 56e7230

File tree

6 files changed

+62
-48
lines changed

6 files changed

+62
-48
lines changed

src/packages/database/postgres/account-queries.ts

Lines changed: 8 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -12,19 +12,19 @@ import {
1212
len,
1313
} from "@cocalc/util/misc";
1414
import { is_a_site_license_manager } from "./site-license/search";
15-
import { PostgreSQL } from "./types";
15+
import { PostgreSQL, SetAccountFields } from "./types";
1616
//import getLogger from "@cocalc/backend/logger";
1717
//const L = getLogger("db:pg:account-queries");
1818

1919
/* For now we define "paying customer" to mean they have a subscription.
2020
It's OK if it expired. They at least bought one once.
2121
This is mainly used for anti-abuse purposes...
22-
22+
2323
TODO: modernize this or don't use this at all...
2424
*/
2525
export async function is_paying_customer(
2626
db: PostgreSQL,
27-
account_id: string
27+
account_id: string,
2828
): Promise<boolean> {
2929
let x;
3030
try {
@@ -44,25 +44,17 @@ export async function is_paying_customer(
4444
return await is_a_site_license_manager(db, account_id);
4545
}
4646

47-
interface SetAccountFields {
48-
db: PostgreSQL;
49-
account_id: string;
50-
email_address?: string | undefined;
51-
first_name?: string | undefined;
52-
last_name?: string | undefined;
53-
}
54-
5547
// this is like set_account_info_if_different, but only sets the fields if they're not set
5648
export async function set_account_info_if_not_set(
57-
opts: SetAccountFields
49+
opts: SetAccountFields,
5850
): Promise<void> {
5951
return await set_account_info_if_different(opts, false);
6052
}
6153

6254
// This sets the given fields of an account, if it is different from the current value – except for the email address, which we only set but not change
6355
export async function set_account_info_if_different(
6456
opts: SetAccountFields,
65-
overwrite = true
57+
overwrite = true,
6658
): Promise<void> {
6759
const columns = ["email_address", "first_name", "last_name"];
6860

@@ -106,7 +98,7 @@ export async function set_account_info_if_different(
10698
export async function set_account(
10799
db: PostgreSQL,
108100
account_id: string,
109-
set: { [field: string]: any }
101+
set: { [field: string]: any },
110102
): Promise<void> {
111103
await db.async_query({
112104
query: "UPDATE accounts",
@@ -118,7 +110,7 @@ export async function set_account(
118110
export async function get_account(
119111
db: PostgreSQL,
120112
account_id: string,
121-
columns: string[]
113+
columns: string[],
122114
): Promise<void> {
123115
return await callback2(db.get_account.bind(db), {
124116
account_id,
@@ -133,7 +125,7 @@ interface SetEmailAddressVerifiedOpts {
133125
}
134126

135127
export async function set_email_address_verified(
136-
opts: SetEmailAddressVerifiedOpts
128+
opts: SetEmailAddressVerifiedOpts,
137129
): Promise<void> {
138130
const { db, account_id, email_address } = opts;
139131
assert_valid_account_id(account_id);

src/packages/database/postgres/passport.ts

Lines changed: 35 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -8,25 +8,27 @@
88
import { PassportStrategyDB } from "@cocalc/database/settings/auth-sso-types";
99
import {
1010
getPassportsCached,
11-
setPassportsCached
11+
setPassportsCached,
1212
} from "@cocalc/database/settings/server-settings";
13+
import { callback2 as cb2 } from "@cocalc/util/async-utils";
1314
import { to_json } from "@cocalc/util/misc";
1415
import { CB } from "@cocalc/util/types/database";
1516
import {
1617
set_account_info_if_different,
1718
set_account_info_if_not_set,
18-
set_email_address_verified
19+
set_email_address_verified,
1920
} from "./account-queries";
2021
import {
2122
CreatePassportOpts,
2223
PassportExistsOpts,
2324
PostgreSQL,
24-
UpdateAccountInfoAndPassportOpts
25+
SetAccountFields,
26+
UpdateAccountInfoAndPassportOpts,
2527
} from "./types";
2628

2729
export async function set_passport_settings(
2830
db: PostgreSQL,
29-
opts: PassportStrategyDB & { cb?: CB }
31+
opts: PassportStrategyDB & { cb?: CB },
3032
): Promise<void> {
3133
const { strategy, conf, info } = opts;
3234
let err = null;
@@ -50,7 +52,7 @@ export async function set_passport_settings(
5052

5153
export async function get_passport_settings(
5254
db: PostgreSQL,
53-
opts: { strategy: string; cb?: (data: object) => void }
55+
opts: { strategy: string; cb?: (data: object) => void },
5456
): Promise<any> {
5557
const { rows } = await db.async_query({
5658
query: "SELECT conf, info FROM passport_settings",
@@ -63,7 +65,7 @@ export async function get_passport_settings(
6365
}
6466

6567
export async function get_all_passport_settings(
66-
db: PostgreSQL
68+
db: PostgreSQL,
6769
): Promise<PassportStrategyDB[]> {
6870
return (
6971
await db.async_query<PassportStrategyDB>({
@@ -73,7 +75,7 @@ export async function get_all_passport_settings(
7375
}
7476

7577
export async function get_all_passport_settings_cached(
76-
db: PostgreSQL
78+
db: PostgreSQL,
7779
): Promise<PassportStrategyDB[]> {
7880
const passports = getPassportsCached();
7981
if (passports != null) {
@@ -103,7 +105,7 @@ export function _passport_key(opts) {
103105

104106
export async function create_passport(
105107
db: PostgreSQL,
106-
opts: CreatePassportOpts
108+
opts: CreatePassportOpts,
107109
): Promise<void> {
108110
const dbg = db._dbg("create_passport");
109111
dbg({ id: opts.id, strategy: opts.strategy, profile: to_json(opts.profile) });
@@ -121,7 +123,7 @@ export async function create_passport(
121123
});
122124

123125
dbg(
124-
`setting other account info ${opts.account_id}: ${opts.email_address}, ${opts.first_name}, ${opts.last_name}`
126+
`setting other account info ${opts.account_id}: ${opts.email_address}, ${opts.first_name}, ${opts.last_name}`,
125127
);
126128
await set_account_info_if_not_set({
127129
db: db,
@@ -150,7 +152,7 @@ export async function create_passport(
150152

151153
export async function passport_exists(
152154
db: PostgreSQL,
153-
opts: PassportExistsOpts
155+
opts: PassportExistsOpts,
154156
): Promise<string | undefined> {
155157
try {
156158
const result = await db.async_query({
@@ -178,25 +180,41 @@ export async function passport_exists(
178180

179181
export async function update_account_and_passport(
180182
db: PostgreSQL,
181-
opts: UpdateAccountInfoAndPassportOpts
183+
opts: UpdateAccountInfoAndPassportOpts,
182184
) {
183-
// we deliberately do not update the email address, because if the SSO
184-
// strategy sends a different one, this would break the "link".
185-
// rather, if the email (and hence most likely the email address) changes on the
186-
// SSO side, this would equal to creating a new account.
185+
// This also updates the email address, if it is set in opts and does not exist with another account yet.
186+
// NOTE: this changed in July 2024. Prior to that, changing the email address of the same account (by ID) in SSO,
187+
// would not change the email address.
187188
const dbg = db._dbg("update_account_and_passport");
188189
dbg(
189190
`updating account info ${to_json({
190191
first_name: opts.first_name,
191192
last_name: opts.last_name,
192-
})}`
193+
email_addres: opts.email_address,
194+
})}`,
193195
);
194-
await set_account_info_if_different({
196+
197+
const upd: SetAccountFields = {
195198
db: db,
196199
account_id: opts.account_id,
197200
first_name: opts.first_name,
198201
last_name: opts.last_name,
202+
};
203+
204+
// Most likely, this just returns the very same account (since the account already exists).
205+
const existing_account_id = await cb2(db.account_exists, {
206+
email_address: opts.email_address,
199207
});
208+
if (!existing_account_id) {
209+
// There is no account with the new email address, hence we can update the email address as well
210+
upd.email_address = opts.email_address;
211+
dbg(
212+
`No existing account with email address ${opts.email_address}. Therefore, we change the email address of account ${opts.account_id} as well.`,
213+
);
214+
}
215+
216+
// this set_account_info_if_different checks again if the email exists on another account, but it would throw an error.
217+
await set_account_info_if_different(upd);
200218
const key = _passport_key(opts);
201219
dbg(`updating passport ${to_json({ key, profile: opts.profile })}`);
202220
await db.async_query({

src/packages/database/postgres/types.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -318,3 +318,11 @@ export interface PostgreSQL extends EventEmitter {
318318
}>;
319319
}): Promise<void>;
320320
}
321+
322+
export interface SetAccountFields {
323+
db: PostgreSQL;
324+
account_id: string;
325+
email_address?: string | undefined;
326+
first_name?: string | undefined;
327+
last_name?: string | undefined;
328+
}

src/packages/next/pages/sso/index.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@ export default function SignupIndex(props: Props) {
3232

3333
function renderDomains(domains) {
3434
if (domains == null || domains.length === 0) return;
35-
return <Text type="secondary">{to_human_list(domains ?? [])}</Text>;
35+
const names = (domains ?? []).map((d) => (d === "*" ? "all domains" : d));
36+
return <Text type="secondary">{to_human_list(names)}</Text>;
3637
}
3738

3839
function extra(sso) {

src/packages/server/auth/sso/passport-login.ts

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,7 @@ export class PassportLogin {
277277
L(
278278
"check to see if the passport already exists indexed by the given id -- in that case we will log user in",
279279
);
280+
// L({ locals });
280281

281282
const passport_account_id = await this.database.passport_exists({
282283
strategy: opts.strategyName,
@@ -315,11 +316,12 @@ export class PassportLogin {
315316
// user authenticated, passport not known, adding to the user's account
316317
await this.createPassport(opts, locals);
317318
} else {
319+
L(`passport_account_id=${passport_account_id}`);
318320
if (
319321
locals.has_valid_remember_me &&
320322
locals.account_id !== passport_account_id
321323
) {
322-
L("passport exists but is associated with another account already");
324+
L("passport exists, but is associated with another account already");
323325
throw Error(
324326
`Your ${opts.strategyName} account is already attached to another CoCalc account. First sign into that account and unlink ${opts.strategyName} in account settings, if you want to instead associate it with this account.`,
325327
);
@@ -348,6 +350,7 @@ export class PassportLogin {
348350
locals: PassportLoginLocals,
349351
): Promise<void> {
350352
const L = logger.extend("check_existing_emails").debug;
353+
// L({ locals });
351354
// handle case where passport doesn't exist, but we know one or more email addresses → check for matching email
352355
if (locals.account_id != null || opts.emails == null) return;
353356

@@ -414,6 +417,7 @@ export class PassportLogin {
414417
): Promise<void> {
415418
if (locals.account_id) return;
416419
const L = logger.extend("maybe_create_account").debug;
420+
// L({ locals });
417421

418422
L(
419423
"no existing account to link, so create new account that can be accessed using this passport",
@@ -506,18 +510,9 @@ export class PassportLogin {
506510
}
507511

508512
// We update the email address, if it does not belong to another account.
509-
// Most likely, this just returns the very same account (hence an account exists).
513+
510514
if (is_valid_email_address(locals.email_address)) {
511-
const existing_account_id = await cb2(this.database.account_exists, {
512-
email_address: locals.email_address,
513-
});
514-
if (!existing_account_id) {
515-
// There is no account with the new email address, hence we can update the email address as well
516-
upd.email_address = locals.email_address;
517-
L(
518-
`No existing account with email address ${locals.email_address} provided by the SSO strategy. Hence we change the email address of account ${locals.account_id} as well.`,
519-
);
520-
}
515+
upd.email_address = locals.email_address;
521516
}
522517

523518
L(`account exists and we update name of user based on SSO`);

src/scripts/auth/gen-sso.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@
159159
"last_name": "lastName",
160160
"full_name": "displayName",
161161
"emails": "email",
162-
"id": "email",
162+
"id": "id",
163163
},
164164
"issuer": "https://cocalc.com",
165165
"cert": saml20cert
@@ -170,7 +170,7 @@
170170
"public": False,
171171
"display": "Saml20",
172172
"description": "Testing my SAML 2.0 IdP",
173-
"exclusive_domains": ["example.com"],
173+
"exclusive_domains": ["*"],
174174
"update_on_login": True,
175175
"cookie_ttl_s": 24 * 60 * 60, # 24 hours
176176
}

0 commit comments

Comments
 (0)