Update index.js
Browse files
index.js
CHANGED
@@ -1,215 +1,3 @@
|
|
1 |
-
|
2 |
|
3 |
-
|
4 |
-
throw new Error (await res.text());
|
5 |
-
}
|
6 |
-
|
7 |
-
function hexFromBytes(arr) {
|
8 |
-
if (globalThis.Buffer) {
|
9 |
-
return globalThis.Buffer.from(arr).toString("hex");
|
10 |
-
} else {
|
11 |
-
const bin = [];
|
12 |
-
arr.forEach((byte) => {
|
13 |
-
bin.push(byte.toString(16).padStart(2, "0"));
|
14 |
-
});
|
15 |
-
return bin.join("");
|
16 |
-
}
|
17 |
-
}
|
18 |
-
|
19 |
-
function base64FromBytes(arr) {
|
20 |
-
if (globalThis.Buffer) {
|
21 |
-
return globalThis.Buffer.from(arr).toString("base64");
|
22 |
-
} else {
|
23 |
-
const bin = [];
|
24 |
-
arr.forEach((byte) => {
|
25 |
-
bin.push(String.fromCharCode(byte));
|
26 |
-
});
|
27 |
-
return globalThis.btoa(bin.join(""));
|
28 |
-
}
|
29 |
-
}
|
30 |
-
|
31 |
-
|
32 |
-
|
33 |
-
/**
|
34 |
-
* Use "Sign in with Hub" to authenticate a user, and get oauth user info / access token.
|
35 |
-
*
|
36 |
-
* When called the first time, it will redirect the user to the Hub login page, which then redirects
|
37 |
-
* to the current URL (or custom URL set).
|
38 |
-
*
|
39 |
-
* When called the second time, after the redirect, it will check the query parameters and return
|
40 |
-
* the oauth user info / access token.
|
41 |
-
*
|
42 |
-
* If called inside an iframe, it will open a new window instead of redirecting the iframe, by default.
|
43 |
-
*
|
44 |
-
* When called from inside a static Space with OAuth enabled, it will load the config from the space.
|
45 |
-
*
|
46 |
-
* (Theoretically, this function could be used to authenticate a user for any OAuth provider supporting PKCE and OpenID Connect by changing `hubUrl`,
|
47 |
-
* but it is currently only tested with the Hugging Face Hub.)
|
48 |
-
*/
|
49 |
-
async function oauthLogin(opts) {
|
50 |
-
if (typeof window === "undefined") {
|
51 |
-
throw new Error("oauthLogin is only available in the browser");
|
52 |
-
}
|
53 |
-
|
54 |
-
console.log("localstorage before", JSON.parse(JSON.stringify(localStorage)));
|
55 |
-
|
56 |
-
const hubUrl = opts?.hubUrl || HUB_URL;
|
57 |
-
const openidConfigUrl = `${new URL(hubUrl).origin}/.well-known/openid-configuration`;
|
58 |
-
const openidConfigRes = await fetch(openidConfigUrl, {
|
59 |
-
headers: {
|
60 |
-
Accept: "application/json",
|
61 |
-
},
|
62 |
-
});
|
63 |
-
|
64 |
-
if (!openidConfigRes.ok) {
|
65 |
-
throw await createApiError(openidConfigRes);
|
66 |
-
}
|
67 |
-
|
68 |
-
const opendidConfig = await openidConfigRes.json();
|
69 |
-
|
70 |
-
const searchParams = new URLSearchParams(window.location.search);
|
71 |
-
|
72 |
-
const [error, errorDescription] = [searchParams.get("error"), searchParams.get("error_description")];
|
73 |
-
|
74 |
-
if (error) {
|
75 |
-
throw new Error(`${error}: ${errorDescription}`);
|
76 |
-
}
|
77 |
-
|
78 |
-
const code = searchParams.get("code");
|
79 |
-
const nonce = localStorage.getItem("huggingface.co:oauth:nonce");
|
80 |
-
|
81 |
-
if (code && !nonce) {
|
82 |
-
console.warn("Missing oauth nonce from localStorage");
|
83 |
-
}
|
84 |
-
|
85 |
-
if (code && nonce) {
|
86 |
-
const codeVerifier = localStorage.getItem("huggingface.co:oauth:code_verifier");
|
87 |
-
|
88 |
-
if (!codeVerifier) {
|
89 |
-
throw new Error("Missing oauth code_verifier from localStorage");
|
90 |
-
}
|
91 |
-
|
92 |
-
const state = searchParams.get("state");
|
93 |
-
|
94 |
-
if (!state) {
|
95 |
-
throw new Error("Missing oauth state from query parameters in redirected URL");
|
96 |
-
}
|
97 |
-
|
98 |
-
let parsedState;
|
99 |
-
|
100 |
-
try {
|
101 |
-
parsedState = JSON.parse(state);
|
102 |
-
} catch {
|
103 |
-
throw new Error("Invalid oauth state in redirected URL, unable to parse JSON: " + state);
|
104 |
-
}
|
105 |
-
|
106 |
-
if (parsedState.nonce !== nonce) {
|
107 |
-
throw new Error("Invalid oauth state in redirected URL");
|
108 |
-
}
|
109 |
-
|
110 |
-
console.log("codeVerifier", codeVerifier)
|
111 |
-
|
112 |
-
const tokenRes = await fetch(opendidConfig.token_endpoint, {
|
113 |
-
method: "POST",
|
114 |
-
headers: {
|
115 |
-
"Content-Type": "application/x-www-form-urlencoded",
|
116 |
-
},
|
117 |
-
body: new URLSearchParams({
|
118 |
-
grant_type: "authorization_code",
|
119 |
-
code,
|
120 |
-
redirect_uri: parsedState.redirectUri,
|
121 |
-
code_verifier: codeVerifier,
|
122 |
-
}).toString(),
|
123 |
-
});
|
124 |
-
|
125 |
-
localStorage.removeItem("huggingface.co:oauth:code_verifier");
|
126 |
-
localStorage.removeItem("huggingface.co:oauth:nonce");
|
127 |
-
|
128 |
-
if (!tokenRes.ok) {
|
129 |
-
throw await createApiError(tokenRes);
|
130 |
-
}
|
131 |
-
|
132 |
-
const token = await tokenRes.json();
|
133 |
-
|
134 |
-
const accessTokenExpiresAt = new Date(Date.now() + token.expires_in * 1000);
|
135 |
-
|
136 |
-
const userInfoRes = await fetch(opendidConfig.userinfo_endpoint, {
|
137 |
-
headers: {
|
138 |
-
Authorization: `Bearer ${token.access_token}`,
|
139 |
-
},
|
140 |
-
});
|
141 |
-
|
142 |
-
if (!userInfoRes.ok) {
|
143 |
-
throw await createApiError(userInfoRes);
|
144 |
-
}
|
145 |
-
|
146 |
-
const userInfo = await userInfoRes.json();
|
147 |
-
|
148 |
-
return {
|
149 |
-
accessToken: token.access_token,
|
150 |
-
accessTokenExpiresAt,
|
151 |
-
userInfo: {
|
152 |
-
id: userInfo.sub,
|
153 |
-
name: userInfo.name,
|
154 |
-
fullname: userInfo.preferred_username,
|
155 |
-
email: userInfo.email,
|
156 |
-
emailVerified: userInfo.email_verified,
|
157 |
-
avatarUrl: userInfo.picture,
|
158 |
-
websiteUrl: userInfo.website,
|
159 |
-
isPro: userInfo.isPro,
|
160 |
-
orgs: userInfo.orgs || [],
|
161 |
-
},
|
162 |
-
state: parsedState.state,
|
163 |
-
scope: token.scope,
|
164 |
-
};
|
165 |
-
}
|
166 |
-
|
167 |
-
const newNonce = crypto.randomUUID();
|
168 |
-
// Two random UUIDs concatenated together, because min length is 43 and max length is 128
|
169 |
-
const newCodeVerifier = crypto.randomUUID() + crypto.randomUUID();
|
170 |
-
|
171 |
-
localStorage.setItem("huggingface.co:oauth:nonce", newNonce);
|
172 |
-
localStorage.setItem("huggingface.co:oauth:code_verifier", newCodeVerifier);
|
173 |
-
|
174 |
-
const redirectUri = opts?.redirectUri || window.location.href;
|
175 |
-
const state = JSON.stringify({
|
176 |
-
nonce: newNonce,
|
177 |
-
redirectUri,
|
178 |
-
state: opts?.state,
|
179 |
-
});
|
180 |
-
|
181 |
-
// @ts-expect-error window.huggingface is defined inside static Spaces.
|
182 |
-
const variables = window?.huggingface?.variables ?? null;
|
183 |
-
|
184 |
-
const clientId = opts?.clientId || variables?.OAUTH_CLIENT_ID;
|
185 |
-
|
186 |
-
if (!clientId) {
|
187 |
-
if (variables) {
|
188 |
-
throw new Error("Missing clientId, please add hf_oauth: true to the README.md's metadata in your static Space");
|
189 |
-
}
|
190 |
-
throw new Error("Missing clientId");
|
191 |
-
}
|
192 |
-
|
193 |
-
const challenge = base64FromBytes(
|
194 |
-
new Uint8Array(await globalThis.crypto.subtle.digest("SHA-256", new TextEncoder().encode(newCodeVerifier)))
|
195 |
-
)
|
196 |
-
.replace(/[+]/g, "-")
|
197 |
-
.replace(/[/]/g, "_")
|
198 |
-
.replace(/=/g, "");
|
199 |
-
|
200 |
-
console.log("localstorage after", JSON.parse(JSON.stringify(localStorage)))
|
201 |
-
console.log("challenge after", challenge, newCodeVerifier)
|
202 |
-
|
203 |
-
window.location.href = `${opendidConfig.authorization_endpoint}?${new URLSearchParams({
|
204 |
-
client_id: clientId,
|
205 |
-
scope: opts?.scopes || "openid profile",
|
206 |
-
response_type: "code",
|
207 |
-
redirect_uri: redirectUri,
|
208 |
-
state,
|
209 |
-
code_challenge: challenge,
|
210 |
-
code_challenge_method: "S256",
|
211 |
-
}).toString()}`;
|
212 |
-
throw new Error("Redirected");
|
213 |
-
}
|
214 |
-
|
215 |
-
oauthLogin().then(console.log);
|
|
|
1 |
+
import { oauthLoginUrl } from "@huggingface/hub";
|
2 |
|
3 |
+
window.location.href = await oauthLoginUrl();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|