const HUB_URL = "https://huggingface.co"; async function createApiError(res) { throw new Error (await res.text()); } function hexFromBytes(arr) { if (globalThis.Buffer) { return globalThis.Buffer.from(arr).toString("hex"); } else { const bin = []; arr.forEach((byte) => { bin.push(byte.toString(16).padStart(2, "0")); }); return bin.join(""); } } function base64FromBytes(arr) { if (globalThis.Buffer) { return globalThis.Buffer.from(arr).toString("base64"); } else { const bin = []; arr.forEach((byte) => { bin.push(String.fromCharCode(byte)); }); return globalThis.btoa(bin.join("")); } } /** * Use "Sign in with Hub" to authenticate a user, and get oauth user info / access token. * * When called the first time, it will redirect the user to the Hub login page, which then redirects * to the current URL (or custom URL set). * * When called the second time, after the redirect, it will check the query parameters and return * the oauth user info / access token. * * If called inside an iframe, it will open a new window instead of redirecting the iframe, by default. * * When called from inside a static Space with OAuth enabled, it will load the config from the space. * * (Theoretically, this function could be used to authenticate a user for any OAuth provider supporting PKCE and OpenID Connect by changing `hubUrl`, * but it is currently only tested with the Hugging Face Hub.) */ async function oauthLogin(opts) { if (typeof window === "undefined") { throw new Error("oauthLogin is only available in the browser"); } console.log("localstorage before", JSON.parse(JSON.stringify(localStorage))); 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 opendidConfig = await openidConfigRes.json(); const searchParams = new URLSearchParams(window.location.search); const [error, errorDescription] = [searchParams.get("error"), searchParams.get("error_description")]; if (error) { throw new Error(`${error}: ${errorDescription}`); } const code = searchParams.get("code"); const nonce = localStorage.getItem("huggingface.co:oauth:nonce"); if (code && !nonce) { console.warn("Missing oauth nonce from localStorage"); } if (code && nonce) { const 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; 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"); } console.log("codeVerifier", codeVerifier) const tokenRes = await fetch(opendidConfig.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(), }); localStorage.removeItem("huggingface.co:oauth:code_verifier"); localStorage.removeItem("huggingface.co:oauth:nonce"); if (!tokenRes.ok) { throw await createApiError(tokenRes); } const token = await tokenRes.json(); const accessTokenExpiresAt = new Date(Date.now() + token.expires_in * 1000); const userInfoRes = await fetch(opendidConfig.userinfo_endpoint, { headers: { Authorization: `Bearer ${token.access_token}`, }, }); if (!userInfoRes.ok) { throw await createApiError(userInfoRes); } const userInfo = await userInfoRes.json(); return { accessToken: token.access_token, accessTokenExpiresAt, userInfo: { id: userInfo.sub, name: userInfo.name, fullname: userInfo.preferred_username, email: userInfo.email, emailVerified: userInfo.email_verified, avatarUrl: userInfo.picture, websiteUrl: userInfo.website, isPro: userInfo.isPro, orgs: userInfo.orgs || [], }, state: parsedState.state, scope: token.scope, }; } const newNonce = crypto.randomUUID(); // Two random UUIDs concatenated together, because min length is 43 and max length is 128 const newCodeVerifier = crypto.randomUUID() + crypto.randomUUID(); localStorage.setItem("huggingface.co:oauth:nonce", newNonce); localStorage.setItem("huggingface.co:oauth:code_verifier", newCodeVerifier); const redirectUri = opts?.redirectUri || window.location.href; const state = JSON.stringify({ nonce: newNonce, redirectUri, state: opts?.state, }); // @ts-expect-error window.huggingface is defined inside static Spaces. const variables = window?.huggingface?.variables ?? null; const clientId = opts?.clientId || variables?.OAUTH_CLIENT_ID; if (!clientId) { if (variables) { throw new Error("Missing clientId, please add hf_oauth: true to the README.md's metadata in your static Space"); } throw new Error("Missing clientId"); } const challenge = base64FromBytes( new Uint8Array(await globalThis.crypto.subtle.digest("SHA-256", new TextEncoder().encode(newCodeVerifier))) ) .replace(/[+]/g, "-") .replace(/[/]/g, "_") .replace(/=/g, ""); console.log("localstorage after", JSON.parse(JSON.stringify(localStorage))) console.log("challenge after", challenge, newCodeVerifier) window.location.href = `${opendidConfig.authorization_endpoint}?${new URLSearchParams({ client_id: clientId, scope: opts?.scopes || "openid profile", response_type: "code", redirect_uri: redirectUri, state, code_challenge: challenge, code_challenge_method: "S256", }).toString()}`; throw new Error("Redirected"); } oauthLogin().then(console.log);