mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-02-18 21:57:44 +01:00
86 lines
2.6 KiB
TypeScript
86 lines
2.6 KiB
TypeScript
import type { AccessToken } from './store';
|
|
|
|
export function isTokenExpired(token: AccessToken) {
|
|
return token.expiresAt && Date.now() > token.expiresAt;
|
|
}
|
|
|
|
export function extractCode(urlStr: string, redirectUri: string | null): string | null {
|
|
const url = new URL(urlStr);
|
|
|
|
if (!urlMatchesRedirect(url, redirectUri)) {
|
|
console.log('[oauth2] URL does not match redirect origin/path; skipping.');
|
|
return null;
|
|
}
|
|
|
|
// Prefer query param; fall back to fragment if query lacks it
|
|
|
|
const query = url.searchParams;
|
|
const queryError = query.get('error');
|
|
const queryDesc = query.get('error_description');
|
|
const queryUri = query.get('error_uri');
|
|
|
|
let hashParams: URLSearchParams | null = null;
|
|
if (url.hash && url.hash.length > 1) {
|
|
hashParams = new URLSearchParams(url.hash.slice(1));
|
|
}
|
|
const hashError = hashParams?.get('error');
|
|
const hashDesc = hashParams?.get('error_description');
|
|
const hashUri = hashParams?.get('error_uri');
|
|
|
|
const error = queryError || hashError;
|
|
if (error) {
|
|
const desc = queryDesc || hashDesc;
|
|
const uri = queryUri || hashUri;
|
|
let message = `Failed to authorize: ${error}`;
|
|
if (desc) message += ` (${desc})`;
|
|
if (uri) message += ` [${uri}]`;
|
|
throw new Error(message);
|
|
}
|
|
|
|
const queryCode = query.get('code');
|
|
if (queryCode) return queryCode;
|
|
|
|
const hashCode = hashParams?.get('code');
|
|
if (hashCode) return hashCode;
|
|
|
|
console.log('[oauth2] Code not found');
|
|
return null;
|
|
}
|
|
|
|
export function urlMatchesRedirect(url: URL, redirectUrl: string | null): boolean {
|
|
if (!redirectUrl) return true;
|
|
|
|
let redirect;
|
|
try {
|
|
redirect = new URL(redirectUrl);
|
|
} catch {
|
|
console.log('[oauth2] Invalid redirect URI; skipping.');
|
|
return false;
|
|
}
|
|
|
|
const sameProtocol = url.protocol === redirect.protocol;
|
|
|
|
const sameHost = url.hostname.toLowerCase() === redirect.hostname.toLowerCase();
|
|
|
|
const normalizePort = (u: URL) =>
|
|
(u.protocol === 'https:' && (!u.port || u.port === '443')) ||
|
|
(u.protocol === 'http:' && (!u.port || u.port === '80'))
|
|
? ''
|
|
: u.port;
|
|
|
|
const samePort = normalizePort(url) === normalizePort(redirect);
|
|
|
|
const normPath = (p: string) => {
|
|
const withLeading = p.startsWith('/') ? p : `/${p}`;
|
|
// strip trailing slashes, keep root as "/"
|
|
return withLeading.replace(/\/+$/g, '') || '/';
|
|
};
|
|
|
|
// Require redirect path to be a prefix of the navigated URL path
|
|
const urlPath = normPath(url.pathname);
|
|
const redirectPath = normPath(redirect.pathname);
|
|
const pathMatches = urlPath === redirectPath || urlPath.startsWith(`${redirectPath}/`);
|
|
|
|
return sameProtocol && sameHost && samePort && pathMatches;
|
|
}
|