mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-11-05 16:16:49 +00:00
The hand-off parked a password before its question, for up to five minutes, and on a shared computer another local user could ask for someone's sign-in with a link of their own. Now nothing leaves the page before the yes, and the instance has a switch for such machines. - `/check` tests a password and keeps nothing, `/park` runs on the yes and hands over tokens wherever the server allows, a password as a last resort - `/discard` and a timer take out what SSO has to park first, and a no revokes the refresh token, and the shared store's TTL is the real expiry - `DESKTOP_BROWSER_SIGN_IN`, default on, closes the routes, the page's hand-off and the app's button; the question says which device asked - tokens from a typed password stay a password account under the name typed (#1432) - redeem refuses a server of the user's own when the device's rule says no - the park budget is reserved before going upstream, and only failures count - "Not now" ends the flow in the app at once, and the question names the account `desktop-handoff-routes.test.ts` now tests the two-step flow against the real budgets. `sso-start-route.test.ts`'s config mock answers the new setting, and the SSO test's expected answer has the account in it.
64 lines
2.7 KiB
TypeScript
64 lines
2.7 KiB
TypeScript
import { configManager } from '@/lib/admin/config-manager';
|
|
import { findServerById, findServerByUrl, parseJmapServers, resolveTrustedJmapUrl, type JmapServerEntry } from '@/lib/admin/jmap-servers';
|
|
import { IS_DESKTOP_BUILD } from '@/lib/desktop/flag';
|
|
import { isPublicHttpUrl } from '@/lib/security/url-guard';
|
|
|
|
/**
|
|
* What the desktop app's sign-in routes share (app/api/auth/desktop-handoff). The same
|
|
* pinning as the other sign-in routes, and the instance admin's switch: on a terminal
|
|
* server another local user can bind a loopback port, so the admin can turn the hand-off
|
|
* off there. A device's own `BrowserSignIn` doesn't reach this: it's about the app, and
|
|
* the other user's link opens in a browser.
|
|
*/
|
|
export interface DesktopHandoffRefusal {
|
|
error: string;
|
|
status: number;
|
|
}
|
|
|
|
export function desktopHandoffEnabled(): boolean {
|
|
return !IS_DESKTOP_BUILD && configManager.get<boolean>('desktopBrowserSignIn', true);
|
|
}
|
|
|
|
export type DesktopHandoffServer =
|
|
| { refusal: DesktopHandoffRefusal }
|
|
| {
|
|
entry: JmapServerEntry | undefined;
|
|
/** What's checked and parked, whatever the body said. */
|
|
resolved: string;
|
|
upstream: string;
|
|
trusted: boolean;
|
|
/** The user's own server, by `allowCustomJmapEndpoint`. A device's policy may refuse it. */
|
|
custom: boolean;
|
|
};
|
|
|
|
export async function resolveDesktopHandoffServer(serverUrl: string, serverId: unknown): Promise<DesktopHandoffServer> {
|
|
await configManager.ensureLoaded();
|
|
if (!desktopHandoffEnabled()) return { refusal: { error: 'disabled', status: 403 } };
|
|
if (configManager.get<boolean>('oauthEnabled', false) && configManager.get<boolean>('oauthOnly', false)) {
|
|
return { refusal: { error: 'oauth_only', status: 403 } };
|
|
}
|
|
const configured =
|
|
configManager.get<string>('jmapServerUrl', '') || process.env.JMAP_SERVER_URL || process.env.NEXT_PUBLIC_JMAP_SERVER_URL || '';
|
|
const list = parseJmapServers(configManager.get<unknown>('jmapServers', []));
|
|
const requested = typeof serverId === 'string' && serverId ? serverId : null;
|
|
const entry = findServerById(list, requested) || findServerByUrl(list, serverUrl) || undefined;
|
|
let resolved: string;
|
|
let trusted = true;
|
|
let custom = false;
|
|
if (entry) {
|
|
resolved = entry.url;
|
|
} else {
|
|
const pinned = resolveTrustedJmapUrl(serverUrl, configured, list);
|
|
if (pinned) {
|
|
resolved = pinned;
|
|
} else if (configManager.get<boolean>('allowCustomJmapEndpoint', false) && (await isPublicHttpUrl(serverUrl))) {
|
|
resolved = serverUrl;
|
|
trusted = false;
|
|
custom = true;
|
|
} else {
|
|
return { refusal: { error: 'invalid_server_url', status: 400 } };
|
|
}
|
|
}
|
|
return { entry, resolved, upstream: resolved.replace(/\/+$/, ''), trusted, custom };
|
|
}
|