File size: 5,931 Bytes
c649ce2 99353b3 e6c40b4 99353b3 2445c98 99353b3 ed8d376 99353b3 7dd5f51 89ceee2 99353b3 54de70b e6e3c41 54de70b 99353b3 9248d57 99353b3 54de70b 99353b3 54de70b 99353b3 54de70b 99353b3 6732e81 99353b3 b198b14 99353b3 b198b14 99353b3 7dd5f51 9248d57 e6e3c41 99353b3 33c55e3 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 |
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("");
}
}
/**
* 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); |