thibaud frere
commited on
Commit
·
ea67b4c
1
Parent(s):
69b83cc
fix
Browse files- index.html +53 -37
- test.md +138 -0
index.html
CHANGED
|
@@ -29,13 +29,9 @@
|
|
| 29 |
<div id="status"></div>
|
| 30 |
<pre id="userinfo" style="display:none"></pre>
|
| 31 |
</div>
|
| 32 |
-
<script
|
| 33 |
-
//
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
// Fallback pour les variables OAuth si on utilise la méthode manuelle
|
| 37 |
-
const CLIENT_ID = window.OAUTH_CLIENT_ID || window.huggingface?.space?.oauth?.clientId;
|
| 38 |
-
const CLIENT_SECRET = window.OAUTH_CLIENT_SECRET || window.huggingface?.space?.oauth?.clientSecret;
|
| 39 |
const REDIRECT_URI = window.location.origin + window.location.pathname;
|
| 40 |
const HF_OAUTH_URL = 'https://huggingface.co/oauth/authorize';
|
| 41 |
const HF_TOKEN_URL = 'https://huggingface.co/oauth/token';
|
|
@@ -56,15 +52,14 @@
|
|
| 56 |
document.getElementById('userinfo').textContent = '';
|
| 57 |
}
|
| 58 |
|
| 59 |
-
// Sign in button
|
| 60 |
-
document.getElementById('signin').onclick =
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
}
|
| 68 |
};
|
| 69 |
|
| 70 |
// Sign out button
|
|
@@ -76,34 +71,55 @@
|
|
| 76 |
window.history.replaceState({}, '', window.location.pathname);
|
| 77 |
};
|
| 78 |
|
| 79 |
-
// Handle OAuth callback
|
| 80 |
window.onload = async function () {
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
if (
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 90 |
localStorage.setItem('hf_oauth_userinfo', userinfoStr);
|
| 91 |
showLoggedIn(userinfoStr);
|
| 92 |
// Clean up URL
|
| 93 |
window.history.replaceState({}, '', window.location.pathname);
|
| 94 |
} else {
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
const userinfo = localStorage.getItem('hf_oauth_userinfo');
|
| 98 |
-
if (token && userinfo) {
|
| 99 |
-
showLoggedIn(userinfo);
|
| 100 |
-
} else {
|
| 101 |
-
showLoggedOut();
|
| 102 |
-
}
|
| 103 |
}
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 107 |
showLoggedOut();
|
| 108 |
}
|
| 109 |
};
|
|
|
|
| 29 |
<div id="status"></div>
|
| 30 |
<pre id="userinfo" style="display:none"></pre>
|
| 31 |
</div>
|
| 32 |
+
<script>
|
| 33 |
+
// Utiliser le client_id injecté par Hugging Face dans l'environnement du Space
|
| 34 |
+
const CLIENT_ID = window.huggingface?.variables?.OAUTH_CLIENT_ID;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
const REDIRECT_URI = window.location.origin + window.location.pathname;
|
| 36 |
const HF_OAUTH_URL = 'https://huggingface.co/oauth/authorize';
|
| 37 |
const HF_TOKEN_URL = 'https://huggingface.co/oauth/token';
|
|
|
|
| 52 |
document.getElementById('userinfo').textContent = '';
|
| 53 |
}
|
| 54 |
|
| 55 |
+
// Sign in button
|
| 56 |
+
document.getElementById('signin').onclick = function () {
|
| 57 |
+
const state = Math.random().toString(36).slice(2);
|
| 58 |
+
localStorage.setItem('hf_oauth_state', state);
|
| 59 |
+
const url = `${HF_OAUTH_URL}?client_id=${CLIENT_ID}` +
|
| 60 |
+
`&redirect_uri=${encodeURIComponent(REDIRECT_URI)}` +
|
| 61 |
+
`&response_type=code&scope=openid%20profile&state=${state}&prompt=consent`;
|
| 62 |
+
window.location = url;
|
|
|
|
| 63 |
};
|
| 64 |
|
| 65 |
// Sign out button
|
|
|
|
| 71 |
window.history.replaceState({}, '', window.location.pathname);
|
| 72 |
};
|
| 73 |
|
| 74 |
+
// Handle OAuth callback
|
| 75 |
window.onload = async function () {
|
| 76 |
+
// If returning from OAuth redirect
|
| 77 |
+
const params = new URLSearchParams(window.location.search);
|
| 78 |
+
if (params.has('code') && params.has('state')) {
|
| 79 |
+
const state = params.get('state');
|
| 80 |
+
if (state !== localStorage.getItem('hf_oauth_state')) {
|
| 81 |
+
document.getElementById('status').textContent = 'Invalid state, possible CSRF detected.';
|
| 82 |
+
return;
|
| 83 |
+
}
|
| 84 |
+
const code = params.get('code');
|
| 85 |
+
const body = new URLSearchParams({
|
| 86 |
+
client_id: CLIENT_ID,
|
| 87 |
+
grant_type: 'authorization_code',
|
| 88 |
+
code: code,
|
| 89 |
+
redirect_uri: REDIRECT_URI
|
| 90 |
+
});
|
| 91 |
+
document.getElementById('status').textContent = 'Exchanging code for token...';
|
| 92 |
+
const resp = await fetch(HF_TOKEN_URL, {
|
| 93 |
+
method: 'POST',
|
| 94 |
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
| 95 |
+
body
|
| 96 |
+
});
|
| 97 |
+
const data = await resp.json();
|
| 98 |
+
if (data.access_token) {
|
| 99 |
+
localStorage.setItem('hf_oauth_token', data.access_token);
|
| 100 |
+
// Fetch userinfo
|
| 101 |
+
const respUser = await fetch('https://huggingface.co/oauth/userinfo', {
|
| 102 |
+
headers: { Authorization: `Bearer ${data.access_token}` }
|
| 103 |
+
});
|
| 104 |
+
const userinfo = await respUser.json();
|
| 105 |
+
const userinfoStr = JSON.stringify(userinfo, null, 2);
|
| 106 |
localStorage.setItem('hf_oauth_userinfo', userinfoStr);
|
| 107 |
showLoggedIn(userinfoStr);
|
| 108 |
// Clean up URL
|
| 109 |
window.history.replaceState({}, '', window.location.pathname);
|
| 110 |
} else {
|
| 111 |
+
document.getElementById('status').textContent = 'OAuth failed: ' + JSON.stringify(data);
|
| 112 |
+
showLoggedOut();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 113 |
}
|
| 114 |
+
return;
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
// Already logged in?
|
| 118 |
+
const token = localStorage.getItem('hf_oauth_token');
|
| 119 |
+
const userinfo = localStorage.getItem('hf_oauth_userinfo');
|
| 120 |
+
if (token && userinfo) {
|
| 121 |
+
showLoggedIn(userinfo);
|
| 122 |
+
} else {
|
| 123 |
showLoggedOut();
|
| 124 |
}
|
| 125 |
};
|
test.md
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Adding a Sign-In with HF button to your Space
|
| 2 |
+
|
| 3 |
+
You can enable a built-in sign-in flow in your Space by seamlessly creating and associating an [OAuth/OpenID connect](https://developer.okta.com/blog/2019/10/21/illustrated-guide-to-oauth-and-oidc) app so users can log in with their HF account.
|
| 4 |
+
|
| 5 |
+
This enables new use cases for your Space. For instance, when combined with [Persistent Storage](https://huggingface.co/docs/hub/spaces-storage), a generative AI Space could allow users to log in to access their previous generations, only accessible to them.
|
| 6 |
+
|
| 7 |
+
<Tip>
|
| 8 |
+
If your Space uses **Gradio**, we recommend following the [Gradio OAuth integration guide](https://www.gradio.app/guides/sharing-your-app#o-auth-login-via-hugging-face), which is simpler and built-in.
|
| 9 |
+
**Only follow this guide if you are _not_ using Gradio** (e.g. static, Streamlit, custom JS/Node/Python apps).
|
| 10 |
+
</Tip>
|
| 11 |
+
|
| 12 |
+
<Tip>
|
| 13 |
+
You can also use the HF OAuth flow to create a "Sign in with HF" flow in any website or App, outside of Spaces. [Read our general OAuth page](./oauth).
|
| 14 |
+
</Tip>
|
| 15 |
+
|
| 16 |
+
**This integration can be done in 2 steps:**
|
| 17 |
+
1. Enable OAuth in your Space's configuration
|
| 18 |
+
2. Add the “Sign in with HF” button to your UI and handle authentication in your code
|
| 19 |
+
|
| 20 |
+
## 1. Enable OAuth in your Space's configuration
|
| 21 |
+
|
| 22 |
+
All you need to do is add `hf_oauth: true` to your Space's metadata inside your `README.md` file.
|
| 23 |
+
|
| 24 |
+
Here's an example of metadata for a Gradio Space:
|
| 25 |
+
|
| 26 |
+
```yaml
|
| 27 |
+
title: Gradio Oauth Test
|
| 28 |
+
emoji: 🏆
|
| 29 |
+
colorFrom: pink
|
| 30 |
+
colorTo: pink
|
| 31 |
+
sdk: gradio
|
| 32 |
+
sdk_version: 3.40.0
|
| 33 |
+
python_version: 3.10.6
|
| 34 |
+
app_file: app.py
|
| 35 |
+
|
| 36 |
+
hf_oauth: true
|
| 37 |
+
# optional, default duration is 8 hours/480 minutes. Max duration is 30 days/43200 minutes.
|
| 38 |
+
hf_oauth_expiration_minutes: 480
|
| 39 |
+
# optional, see "Scopes" below. "openid profile" is always included.
|
| 40 |
+
hf_oauth_scopes:
|
| 41 |
+
- read-repos
|
| 42 |
+
- write-repos
|
| 43 |
+
- manage-repos
|
| 44 |
+
- inference-api
|
| 45 |
+
# optional, restrict access to members of specific organizations
|
| 46 |
+
hf_oauth_authorized_org: ORG_NAME
|
| 47 |
+
hf_oauth_authorized_org:
|
| 48 |
+
- ORG_NAME1
|
| 49 |
+
- ORG_NAME2
|
| 50 |
+
```
|
| 51 |
+
|
| 52 |
+
You can check out the [configuration reference docs](./spaces-config-reference) for more information.
|
| 53 |
+
|
| 54 |
+
This will add the following [environment variables](https://huggingface.co/docs/hub/spaces-overview#helper-environment-variables) to your space:
|
| 55 |
+
|
| 56 |
+
- `OAUTH_CLIENT_ID`: the client ID of your OAuth app (public)
|
| 57 |
+
- `OAUTH_CLIENT_SECRET`: the client secret of your OAuth app
|
| 58 |
+
- `OAUTH_SCOPES`: scopes accessible by your OAuth app.
|
| 59 |
+
- `OPENID_PROVIDER_URL`: The URL of the OpenID provider. The OpenID metadata will be available at [`{OPENID_PROVIDER_URL}/.well-known/openid-configuration`](https://huggingface.co/.well-known/openid-configuration).
|
| 60 |
+
|
| 61 |
+
As for any other environment variable, you can use them in your code by using `os.getenv("OAUTH_CLIENT_ID")`, for example.
|
| 62 |
+
|
| 63 |
+
<Tip warning={true}>
|
| 64 |
+
For Spaces OAuth integration, you do NOT need to configure anything in your Hugging Face account settings.
|
| 65 |
+
Everything is managed by adding metadata to your Space and handling the authentication flow in your code.
|
| 66 |
+
</Tip>
|
| 67 |
+
|
| 68 |
+
### Scopes
|
| 69 |
+
|
| 70 |
+
The following scopes are always included for Spaces:
|
| 71 |
+
|
| 72 |
+
- `openid`: Get the ID token in addition to the access token.
|
| 73 |
+
- `profile`: Get the user's profile information (username, avatar, etc.)
|
| 74 |
+
|
| 75 |
+
Those scopes are optional and can be added by setting `hf_oauth_scopes` in your Space's metadata:
|
| 76 |
+
|
| 77 |
+
- `email`: Get the user's email address.
|
| 78 |
+
- `read-billing`: Know whether the user has a payment method set up.
|
| 79 |
+
- `read-repos`: Get read access to the user's personal repos.
|
| 80 |
+
- `write-repos`: Get write/read access to the user's personal repos.
|
| 81 |
+
- `manage-repos`: Get full access to the user's personal repos. Also grants repo creation and deletion.
|
| 82 |
+
- `inference-api`: Get access to the [Inference API](https://huggingface.co/docs/inference-providers/index), you will be able to make inference requests on behalf of the user.
|
| 83 |
+
- `write-discussions`: Open discussions and Pull Requests on behalf of the user as well as interact with discussions (including reactions, posting/editing comments, closing discussions, ...). To open Pull Requests on private repos, you need to request the `read-repos` scope as well.
|
| 84 |
+
|
| 85 |
+
### Accessing organization resources
|
| 86 |
+
|
| 87 |
+
By default, the oauth app does not need to access organization resources.
|
| 88 |
+
|
| 89 |
+
But some scopes like `read-repos` or `read-billing` apply to organizations as well.
|
| 90 |
+
|
| 91 |
+
The user can select which organizations to grant access to when authorizing the app. If you require access to a specific organization, you can add `orgIds=ORG_ID` as a query parameter to the OAuth authorization URL. You have to replace `ORG_ID` with the organization ID, which is available in the `organizations.sub` field of the userinfo response.
|
| 92 |
+
|
| 93 |
+
## 2. Add the sign-in button and handle authentication in your code
|
| 94 |
+
|
| 95 |
+
You now have all the information to add a "Sign-in with HF" button to your Space and implement the authentication flow. Some libraries ([Python](https://github.com/lepture/authlib), [NodeJS](https://github.com/panva/node-openid-client)) can help you implement the OpenID/OAuth protocol.
|
| 96 |
+
|
| 97 |
+
Gradio and huggingface.js also provide **built-in support**, making implementing the Sign-in with HF button a breeze; you can check out the associated guides with [gradio](https://www.gradio.app/guides/sharing-your-app#o-auth-login-via-hugging-face) and with [huggingface.js](https://huggingface.co/docs/huggingface.js/hub/README#oauth-login).
|
| 98 |
+
|
| 99 |
+
Basically, you need to:
|
| 100 |
+
|
| 101 |
+
- **Choose a redirect URL** that targets your Space (e.g., `https://{SPACE_HOST}/login/callback`).
|
| 102 |
+
|
| 103 |
+
- **Redirect the user** to `https://huggingface.co/oauth/authorize?redirect_uri={REDIRECT_URI}&scope=openid%20profile&client_id={CLIENT_ID}&state={STATE}`, where `STATE` is a random string that you will need to verify later.
|
| 104 |
+
|
| 105 |
+
- **Handle the callback** on `/auth/callback` or `/login/callback` (or your own custom callback URL) and verify the `state` parameter.
|
| 106 |
+
|
| 107 |
+
- Use the `code` query parameter to **get an access token and id token** from `https://huggingface.co/oauth/token` (POST request with `client_id`, `code`, `grant_type=authorization_code` and `redirect_uri` as form data, and with `Authorization: Basic {base64(client_id:client_secret)}` as a header).
|
| 108 |
+
|
| 109 |
+
<Tip warning={true}>
|
| 110 |
+
|
| 111 |
+
You should use `target=_blank` on the button to open the sign-in page in a new tab, unless you run the space outside its `iframe`. Otherwise, you might encounter issues with cookies on some browsers.
|
| 112 |
+
|
| 113 |
+
</Tip>
|
| 114 |
+
|
| 115 |
+
## Examples
|
| 116 |
+
|
| 117 |
+
### Code (with [huggingface.js](https://huggingface.co/docs/huggingface.js/hub/README#oauth-login))
|
| 118 |
+
|
| 119 |
+
```js
|
| 120 |
+
import { oauthLoginUrl, oauthHandleRedirectIfPresent } from "@huggingface/hub";
|
| 121 |
+
|
| 122 |
+
const oauthResult = await oauthHandleRedirectIfPresent();
|
| 123 |
+
|
| 124 |
+
if (!oauthResult) {
|
| 125 |
+
// If the user is not logged in, redirect to the login page
|
| 126 |
+
window.location.href = await oauthLoginUrl();
|
| 127 |
+
}
|
| 128 |
+
|
| 129 |
+
// You can use oauthResult.accessToken, oauthResult.userInfo among other things
|
| 130 |
+
console.log(oauthResult);
|
| 131 |
+
```
|
| 132 |
+
|
| 133 |
+
### Spaces
|
| 134 |
+
|
| 135 |
+
- [Client-Side in a Static Space (huggingface.js)](https://huggingface.co/spaces/huggingfacejs/client-side-oauth) – very simple JavaScript example.
|
| 136 |
+
- [Hugging Chat (NodeJS/SvelteKit)](https://huggingface.co/spaces/huggingchat/chat-ui)
|
| 137 |
+
- [Inference Widgets (Auth.js/SvelteKit)](https://huggingface.co/spaces/huggingfacejs/inference-widgets) – uses the `inference-api` scope to make inference requests on behalf of the user.
|
| 138 |
+
- [Gradio test app](https://huggingface.co/spaces/Wauplin/gradio-oauth-test)
|