|
|
<!DOCTYPE html> |
|
|
<html> |
|
|
<head> |
|
|
<meta charset="utf-8" /> |
|
|
<meta name="viewport" content="width=device-width" /> |
|
|
<title>OAuth in a static Space (Vanilla JS)</title> |
|
|
<style> |
|
|
body { font-family: sans-serif; background: white; color: #222; margin: 0; padding: 2rem; } |
|
|
.card { max-width: 420px; margin: 0 auto; background: #fff; border-radius: 16px; box-shadow: 0 2px 8px #0001; padding: 2rem; border: 1px solid #eee; } |
|
|
h1 { font-size: 20px; margin-top: 0; } |
|
|
p { color: #666; font-size: 14px; } |
|
|
#status { margin-top: 1rem; color: #28a745; } |
|
|
button, img { margin-top: 1rem; cursor: pointer; } |
|
|
pre { background: #f8f9fa; padding: 1em; border-radius: 8px; margin-top: 1rem; font-size: 12px; } |
|
|
</style> |
|
|
</head> |
|
|
<body> |
|
|
<div class="card"> |
|
|
<h1>OAuth in a static Space (Vanilla JS)</h1> |
|
|
<p>Demonstration of Hugging Face OAuth authentication with PKCE in vanilla JS.</p> |
|
|
|
|
|
<img src="https://huggingface.co/datasets/huggingface/badges/resolve/main/sign-in-with-huggingface-xl-dark.svg" |
|
|
alt="Sign in with Hugging Face" id="signin" style="display:none;max-width:100%;"> |
|
|
<button id="signout" style="display:none;">Sign out</button> |
|
|
<div id="status"></div> |
|
|
<pre id="userinfo" style="display:none"></pre> |
|
|
</div> |
|
|
<script> |
|
|
|
|
|
const CLIENT_ID = window.huggingface?.variables?.OAUTH_CLIENT_ID; |
|
|
const REDIRECT_URI = window.location.origin + window.location.pathname; |
|
|
const HF_OAUTH_URL = 'https://huggingface.co/oauth/authorize'; |
|
|
const HF_TOKEN_URL = 'https://huggingface.co/oauth/token'; |
|
|
|
|
|
|
|
|
const showLoggedIn = (userinfo) => { |
|
|
document.getElementById('signin').style.display = 'none'; |
|
|
document.getElementById('signout').style.display = 'block'; |
|
|
document.getElementById('status').textContent = 'Logged in!'; |
|
|
document.getElementById('userinfo').style.display = 'block'; |
|
|
document.getElementById('userinfo').textContent = userinfo; |
|
|
}; |
|
|
const showLoggedOut = () => { |
|
|
document.getElementById('signin').style.display = 'block'; |
|
|
document.getElementById('signout').style.display = 'none'; |
|
|
document.getElementById('status').textContent = ''; |
|
|
document.getElementById('userinfo').style.display = 'none'; |
|
|
}; |
|
|
|
|
|
|
|
|
const generateCodeVerifier = () => { |
|
|
const array = new Uint8Array(32); |
|
|
crypto.getRandomValues(array); |
|
|
return btoa(String.fromCharCode(...array)).replace(/[+/=]/g, m => ({'+':'-','/':'_','=':''}[m])); |
|
|
}; |
|
|
|
|
|
const generateCodeChallenge = async (verifier) => { |
|
|
const data = new TextEncoder().encode(verifier); |
|
|
const digest = await crypto.subtle.digest('SHA-256', data); |
|
|
return btoa(String.fromCharCode(...new Uint8Array(digest))).replace(/[+/=]/g, m => ({'+':'-','/':'_','=':''}[m])); |
|
|
}; |
|
|
|
|
|
|
|
|
document.getElementById('signin').onclick = async () => { |
|
|
const state = Math.random().toString(36).slice(2); |
|
|
const codeVerifier = generateCodeVerifier(); |
|
|
const codeChallenge = await generateCodeChallenge(codeVerifier); |
|
|
|
|
|
localStorage.setItem('hf_oauth_state', state); |
|
|
localStorage.setItem('hf_oauth_code_verifier', codeVerifier); |
|
|
|
|
|
window.location = `${HF_OAUTH_URL}?client_id=${CLIENT_ID}&redirect_uri=${encodeURIComponent(REDIRECT_URI)}&response_type=code&scope=openid%20profile&state=${state}&code_challenge=${codeChallenge}&code_challenge_method=S256`; |
|
|
}; |
|
|
|
|
|
document.getElementById('signout').onclick = () => { |
|
|
localStorage.clear(); |
|
|
showLoggedOut(); |
|
|
window.history.replaceState({}, '', window.location.pathname); |
|
|
}; |
|
|
|
|
|
|
|
|
window.onload = async () => { |
|
|
const params = new URLSearchParams(window.location.search); |
|
|
|
|
|
if (params.has('code') && params.has('state')) { |
|
|
const state = params.get('state'); |
|
|
if (state !== localStorage.getItem('hf_oauth_state')) { |
|
|
document.getElementById('status').textContent = 'Security error detected.'; |
|
|
return; |
|
|
} |
|
|
|
|
|
document.getElementById('status').textContent = 'Exchanging code...'; |
|
|
|
|
|
try { |
|
|
|
|
|
const tokenResp = await fetch(HF_TOKEN_URL, { |
|
|
method: 'POST', |
|
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, |
|
|
body: new URLSearchParams({ |
|
|
grant_type: 'authorization_code', |
|
|
code: params.get('code'), |
|
|
redirect_uri: REDIRECT_URI, |
|
|
code_verifier: localStorage.getItem('hf_oauth_code_verifier') |
|
|
}) |
|
|
}); |
|
|
|
|
|
const { access_token } = await tokenResp.json(); |
|
|
|
|
|
|
|
|
const userResp = await fetch('https://huggingface.co/oauth/userinfo', { |
|
|
headers: { Authorization: `Bearer ${access_token}` } |
|
|
}); |
|
|
const userinfo = await userResp.json(); |
|
|
|
|
|
|
|
|
const userinfoStr = JSON.stringify(userinfo, null, 2); |
|
|
localStorage.setItem('hf_oauth_token', access_token); |
|
|
localStorage.setItem('hf_oauth_userinfo', userinfoStr); |
|
|
showLoggedIn(userinfoStr); |
|
|
|
|
|
window.history.replaceState({}, '', window.location.pathname); |
|
|
} catch (error) { |
|
|
document.getElementById('status').textContent = `OAuth error: ${error.message}`; |
|
|
showLoggedOut(); |
|
|
} |
|
|
return; |
|
|
} |
|
|
|
|
|
|
|
|
const userinfo = localStorage.getItem('hf_oauth_userinfo'); |
|
|
userinfo ? showLoggedIn(userinfo) : showLoggedOut(); |
|
|
}; |
|
|
</script> |
|
|
</body> |
|
|
</html> |