export async function oauthHandleRedirect()

in packages/hub/src/lib/oauth-handle-redirect.ts [114:270]


export async function oauthHandleRedirect(opts?: {
	/**
	 * The URL of the hub. Defaults to {@link HUB_URL}.
	 */
	hubUrl?: string;
	/**
	 * The URL to analyze.
	 *
	 * @default window.location.href
	 */
	redirectedUrl?: string;
	/**
	 * nonce generated by oauthLoginUrl
	 *
	 * @default localStorage.getItem("huggingface.co:oauth:nonce")
	 */
	nonce?: string;
	/**
	 * codeVerifier generated by oauthLoginUrl
	 *
	 * @default localStorage.getItem("huggingface.co:oauth:code_verifier")
	 */
	codeVerifier?: string;
}): Promise<OAuthResult> {
	if (typeof window === "undefined" && !opts?.redirectedUrl) {
		throw new Error("oauthHandleRedirect is only available in the browser, unless you provide redirectedUrl");
	}
	if (typeof localStorage === "undefined" && (!opts?.nonce || !opts?.codeVerifier)) {
		throw new Error(
			"oauthHandleRedirect requires localStorage to be available, unless you provide nonce and codeVerifier"
		);
	}

	const redirectedUrl = opts?.redirectedUrl ?? window.location.href;
	const searchParams = (() => {
		try {
			return new URL(redirectedUrl).searchParams;
		} catch (err) {
			throw new Error("Failed to parse redirected URL: " + redirectedUrl);
		}
	})();

	const [error, errorDescription] = [searchParams.get("error"), searchParams.get("error_description")];

	if (error) {
		throw new Error(`${error}: ${errorDescription}`);
	}

	const code = searchParams.get("code");
	const nonce = opts?.nonce ?? localStorage.getItem("huggingface.co:oauth:nonce");

	if (!code) {
		throw new Error("Missing oauth code from query parameters in redirected URL: " + redirectedUrl);
	}

	if (!nonce) {
		throw new Error("Missing oauth nonce from localStorage");
	}

	const codeVerifier = opts?.codeVerifier ?? localStorage.getItem("huggingface.co:oauth:code_verifier");

	if (!codeVerifier) {
		throw new Error("Missing oauth code_verifier from localStorage");
	}

	const state = searchParams.get("state");

	if (!state) {
		throw new Error("Missing oauth state from query parameters in redirected URL");
	}

	let parsedState: { nonce: string; redirectUri: string; state?: string };

	try {
		parsedState = JSON.parse(state);
	} catch {
		throw new Error("Invalid oauth state in redirected URL, unable to parse JSON: " + state);
	}

	if (parsedState.nonce !== nonce) {
		throw new Error("Invalid oauth state in redirected URL");
	}

	const hubUrl = opts?.hubUrl || HUB_URL;

	const openidConfigUrl = `${new URL(hubUrl).origin}/.well-known/openid-configuration`;
	const openidConfigRes = await fetch(openidConfigUrl, {
		headers: {
			Accept: "application/json",
		},
	});

	if (!openidConfigRes.ok) {
		throw await createApiError(openidConfigRes);
	}

	const openidConfig: {
		authorization_endpoint: string;
		token_endpoint: string;
		userinfo_endpoint: string;
	} = await openidConfigRes.json();

	const tokenRes = await fetch(openidConfig.token_endpoint, {
		method: "POST",
		headers: {
			"Content-Type": "application/x-www-form-urlencoded",
		},
		body: new URLSearchParams({
			grant_type: "authorization_code",
			code,
			redirect_uri: parsedState.redirectUri,
			code_verifier: codeVerifier,
		}).toString(),
	});

	if (!opts?.codeVerifier) {
		localStorage.removeItem("huggingface.co:oauth:code_verifier");
	}
	if (!opts?.nonce) {
		localStorage.removeItem("huggingface.co:oauth:nonce");
	}

	if (!tokenRes.ok) {
		throw await createApiError(tokenRes);
	}

	const token: {
		access_token: string;
		expires_in: number;
		id_token: string;
		// refresh_token: string;
		scope: string;
		token_type: string;
	} = await tokenRes.json();

	const accessTokenExpiresAt = new Date(Date.now() + token.expires_in * 1000);

	const userInfoRes = await fetch(openidConfig.userinfo_endpoint, {
		headers: {
			Authorization: `Bearer ${token.access_token}`,
		},
	});

	if (!userInfoRes.ok) {
		throw await createApiError(userInfoRes);
	}

	const userInfo: UserInfo = await userInfoRes.json();

	return {
		accessToken: token.access_token,
		accessTokenExpiresAt,
		userInfo: userInfo,
		state: parsedState.state,
		scope: token.scope,
	};
}