Path: blob/master/src/packages/database/postgres/passport.ts
5690 views
/*1* This file is part of CoCalc: Copyright © 2022-2025 Sagemath, Inc.2* License: MS-RSL – see LICENSE.md for details3*/45// DEVELOPMENT: use scripts/auth/gen-sso.py to generate some test data67import { PassportStrategyDB } from "@cocalc/database/settings/auth-sso-types";8import {9getPassportsCached,10setPassportsCached,11} from "@cocalc/database/settings/server-settings";12import { callback2 as cb2 } from "@cocalc/util/async-utils";13import { lower_email_address, to_json } from "@cocalc/util/misc";14import { CB } from "@cocalc/util/types/database";15import {16set_account_info_if_different,17set_account_info_if_not_set,18set_email_address_verified,19} from "./account-queries";20import {21CreatePassportOpts,22PassportExistsOpts,23PostgreSQL,24SetAccountFields,25UpdateAccountInfoAndPassportOpts,26} from "./types";2728export async function set_passport_settings(29db: PostgreSQL,30opts: PassportStrategyDB & { cb?: CB },31): Promise<void> {32const { strategy, conf, info } = opts;33let err = null;34try {35await db.async_query({36query: "INSERT INTO passport_settings",37values: {38"strategy::TEXT ": strategy,39"conf ::JSONB": conf,40"info ::JSONB": info,41},42conflict: "strategy",43});44} catch (err) {45err = err;46}47if (typeof opts.cb === "function") {48opts.cb(err);49}50}5152export async function get_passport_settings(53db: PostgreSQL,54opts: { strategy: string; cb?: (data: object) => void },55): Promise<any> {56const { rows } = await db.async_query({57query: "SELECT conf, info FROM passport_settings",58where: { "strategy = $::TEXT": opts.strategy },59});60if (typeof opts.cb === "function") {61opts.cb(rows[0]);62}63return rows[0];64}6566export async function get_all_passport_settings(67db: PostgreSQL,68): Promise<PassportStrategyDB[]> {69return (70await db.async_query<PassportStrategyDB>({71query: "SELECT strategy, conf, info FROM passport_settings",72})73).rows;74}7576export async function get_all_passport_settings_cached(77db: PostgreSQL,78): Promise<PassportStrategyDB[]> {79const passports = getPassportsCached();80if (passports != null) {81return passports;82}83const res = await get_all_passport_settings(db);84setPassportsCached(res);85return res;86}8788// Passports -- accounts linked to Google/Dropbox/Facebook/Github, etc.89// The Schema is slightly redundant, but indexed properly:90// {passports:['google-id', 'facebook-id'], passport_profiles:{'google-id':'...', 'facebook-id':'...'}}9192export function _passport_key(opts) {93const { strategy, id } = opts;94// note: strategy is *our* name of the strategy in the DB, not it's type string!95if (typeof strategy !== "string") {96throw new Error("_passport_key: strategy must be defined");97}98if (typeof id !== "string") {99throw new Error("_passport_key: id must be defined");100}101102return `${strategy}-${id}`;103}104105export async function create_passport(106db: PostgreSQL,107opts: CreatePassportOpts,108): Promise<void> {109const dbg = db._dbg("create_passport");110dbg({ id: opts.id, strategy: opts.strategy, profile: to_json(opts.profile) });111112try {113dbg("setting the passport for the account");114await db.async_query({115query: "UPDATE accounts",116jsonb_set: {117passports: { [_passport_key(opts)]: opts.profile },118},119where: {120"account_id = $::UUID": opts.account_id,121},122});123124dbg(125`setting other account info ${opts.account_id}: ${opts.email_address}, ${opts.first_name}, ${opts.last_name}`,126);127await set_account_info_if_not_set({128db,129account_id: opts.account_id,130email_address: opts.email_address,131first_name: opts.first_name,132last_name: opts.last_name,133});134// we still record that email address as being verified135if (opts.email_address != null) {136await set_email_address_verified({137db,138account_id: opts.account_id,139email_address: opts.email_address,140});141}142opts.cb?.(undefined); // all good143} catch (err) {144if (opts.cb != null) {145opts.cb(err);146} else {147throw err;148}149}150}151152export async function passport_exists(153db: PostgreSQL,154opts: PassportExistsOpts,155): Promise<string | undefined> {156try {157const result = await db.async_query({158query: "SELECT account_id FROM accounts",159where: [160// this uses the corresponding index to only scan a subset of all accounts!161"passports IS NOT NULL",162{ "(passports->>$::TEXT) IS NOT NULL": _passport_key(opts) },163],164});165const account_id = result?.rows[0]?.account_id;166if (opts.cb != null) {167opts.cb(null, account_id);168} else {169return account_id;170}171} catch (err) {172if (opts.cb != null) {173opts.cb(err);174} else {175throw err;176}177}178}179180// this is only used in passport-login/maybeUpdateAccountAndPassport!181export async function update_account_and_passport(182db: PostgreSQL,183opts: UpdateAccountInfoAndPassportOpts,184) {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.188const dbg = db._dbg("update_account_and_passport");189dbg(190`updating account info ${to_json({191first_name: opts.first_name,192last_name: opts.last_name,193email_addres: opts.email_address,194})}`,195);196197const upd: SetAccountFields = {198db: db,199account_id: opts.account_id,200first_name: opts.first_name,201last_name: opts.last_name,202};203204// Only check for existing email if email_address is provided by SSO205// (Some SSO providers don't return email addresses)206if (opts.email_address) {207const email_address = lower_email_address(opts.email_address);208// Most likely, this just returns the very same account (since the account already exists).209const existing_account_id = await cb2(db.account_exists, {210email_address,211});212213if (!existing_account_id) {214// There is no account with the new email address, hence we can update the email address as well215upd.email_address = email_address;216dbg(217`No existing account with email address ${email_address}. Therefore, we change the email address of account ${opts.account_id} as well.`,218);219}220}221222// this set_account_info_if_different checks again if the email exists on another account, but it would throw an error.223const { email_changed } = await set_account_info_if_different(upd);224const key = _passport_key(opts);225dbg(`updating passport ${to_json({ key, profile: opts.profile })}`);226await db.async_query({227query: "UPDATE accounts",228jsonb_set: {229passports: { [key]: opts.profile },230},231where: {232"account_id = $::UUID": opts.account_id,233},234});235236// since we update the email address of an account based on a change from the SSO mechanism237// we can assume the new email address is also "verified"238if (email_changed && typeof upd.email_address === "string") {239await set_email_address_verified({240db,241account_id: opts.account_id,242email_address: upd.email_address,243});244}245}246247248