Line data Source code
1 : import type { OAuthState } from './oauth_types.js';
2 : import { AppFlavor, FLAVORS } from '../config/flavors.js';
3 : import { BASE64URL_CODEC } from '../utils/base64.js';
4 :
5 1 : const PUBLIC_OAUTH_ORIGINS = new Set(['https://dashql.app', 'https://stable.dashql.app', 'https://hyperdb.sh']);
6 1 : const DEVELOPMENT_OAUTH_HOSTS = new Set(['localhost', '127.0.0.1']);
7 :
8 1 : export function isAllowedOAuthCallbackUrl(callbackUrl: string, currentUrl: URL): boolean {
9 1 : let target: URL;
10 1 : try {
11 1 : target = new URL(callbackUrl);
12 1 : } catch {
13 1 : return false;
14 1 : }
15 1 : if (target.pathname !== '/oauth.html' || target.search || target.username || target.password || target.hash) {
16 1 : return false;
17 : }
18 1 : if (target.origin === currentUrl.origin) {
19 1 : return true;
20 : }
21 1 : if (PUBLIC_OAUTH_ORIGINS.has(target.origin)) {
22 1 : return true;
23 : }
24 1 : return (target.protocol === 'http:' || target.protocol === 'https:')
25 1 : && DEVELOPMENT_OAUTH_HOSTS.has(target.hostname);
26 1 : }
27 :
28 1 : export function validateOAuthRedirectState(state: OAuthState, now = Date.now()): void {
29 1 : if (!state || typeof state !== 'object') {
30 0 : throw new Error('OAuth state is missing');
31 : }
32 1 : if (typeof state.flowId !== 'string' || state.flowId.length === 0) {
33 1 : throw new Error('OAuth state has no flow ID');
34 : }
35 1 : if (state.flowVariant !== 'WEB_OPENER_FLOW' && state.flowVariant !== 'NATIVE_LINK_FLOW') {
36 1 : throw new Error('OAuth state has an invalid flow variant');
37 : }
38 1 : const provider = state.salesforceProvider;
39 1 : if (!provider
40 1 : || typeof provider.instanceUrl !== 'string'
41 1 : || typeof provider.appConsumerKey !== 'string'
42 1 : || typeof provider.expiresAt !== 'number') {
43 0 : throw new Error('OAuth state has an invalid Salesforce provider');
44 : }
45 1 : if (provider.expiresAt <= now) {
46 1 : throw new Error('OAuth state has expired');
47 : }
48 1 : }
49 :
50 : /// Decode the base64url-encoded OAuth state of a redirect, it is validated separately
51 1 : export function decodeOAuthState(encoded: string): OAuthState {
52 1 : const buffer = BASE64URL_CODEC.decode(encoded);
53 1 : return JSON.parse(new TextDecoder().decode(buffer)) as OAuthState;
54 1 : }
55 :
56 : /// Get the flavor carried in the encoded OAuth state, null if it carries none or an unknown one.
57 : /// The state is not integrity-protected, the flavor only brands the redirect page.
58 1 : export function getOAuthStateFlavor(encoded: string | null): AppFlavor | null {
59 1 : if (!encoded) {
60 1 : return null;
61 : }
62 1 : try {
63 1 : const flavor = decodeOAuthState(encoded)?.flavor;
64 1 : return typeof flavor === 'string' ? FLAVORS.get(flavor) ?? null : null;
65 1 : } catch {
66 1 : return null;
67 1 : }
68 1 : }
|