|
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(""); |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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(); |
|
|
|
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, |
|
}); |
|
|
|
|
|
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); |