File size: 6,193 Bytes
c649ce2
99353b3
e6c40b4
99353b3
 
 
2445c98
99353b3
 
 
ed8d376
99353b3
 
 
 
 
 
 
497f0f2
 
 
 
 
 
 
 
 
 
 
 
 
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
203
204
205
206
207
208
209
210
211
212
213
214
215
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);