coyotte508 HF Staff commited on
Commit
99353b3
·
1 Parent(s): 002d1a3

Create index.js

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