Skip to content

Commit 947d615

Browse files
Wauplingradio-pr-botabidlabshannahblair
authored
Sign in with Hugging Face (OAuth support) (gradio-app#4943)
* first draft * debug * add print * working oauth * inject OAuth profile + enable OAuth when expected + some doc * add changeset * mypy * opt * open in a new tab only from iframe * msg * add changeset * Apply suggestions from code review Co-authored-by: Abubakar Abid <[email protected]> * fix injection + gr.Error * allow third party cookie when possible * add button to sign in/sign out button * feedback changes * oauth as optional dependency * disable login/logout buttons locally * nothing * a bit of documentation * Add tests for Login/Logout buttons * Apply suggestions from code review Co-authored-by: Abubakar Abid <[email protected]> * mention required dependencies * fix package * fix tests * fix windows tests as well * Fake profile on local debug * doc * fix tets * lint * fix test * test buttons * login button fix * lint * fix final tests --------- Co-authored-by: gradio-pr-bot <[email protected]> Co-authored-by: Abubakar Abid <[email protected]> Co-authored-by: Hannah <[email protected]>
1 parent 987725c commit 947d615

19 files changed

+491
-11
lines changed

.changeset/hot-worms-type.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"gradio": minor
3+
---
4+
5+
feat:Sign in with Hugging Face (OAuth support)

.github/workflows/backend.yml

+3-2
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@ jobs:
128128
cache-dependency-path: |
129129
client/python/requirements.txt
130130
requirements.txt
131+
requirements-oauth.txt
131132
test/requirements.txt
132133
- name: Create env
133134
run: |
@@ -138,7 +139,7 @@ jobs:
138139
with:
139140
path: |
140141
venv/*
141-
key: gradio-lib-${{ runner.os }}-pip-${{ hashFiles('client/python/requirements.txt') }}-${{ hashFiles('requirements.txt') }}-${{ hashFiles('test/requirements.txt') }}
142+
key: gradio-lib-${{ runner.os }}-pip-${{ hashFiles('client/python/requirements.txt') }}-${{ hashFiles('requirements.txt') }}-${{ hashFiles('requirements-oauth.txt') }}-${{ hashFiles('test/requirements.txt') }}
142143
- uses: actions/cache@v3
143144
id: frontend-cache
144145
with:
@@ -198,7 +199,7 @@ jobs:
198199
if: steps.cache.outputs.cache-hit != 'true' && runner.os == 'Windows'
199200
run: |
200201
venv\Scripts\activate
201-
python -m pip install -e . -r test/requirements.txt
202+
python -m pip install -e . -r test/requirements.txt -r requirements-oauth.txt
202203
- name: Run tests (Windows)
203204
if: runner.os == 'Windows'
204205
run: |

gradio/__init__.py

+3
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@
3939
Json,
4040
Label,
4141
LinePlot,
42+
LoginButton,
43+
LogoutButton,
4244
Markdown,
4345
Model3D,
4446
Number,
@@ -82,6 +84,7 @@
8284
from gradio.ipython_ext import load_ipython_extension
8385
from gradio.layouts import Accordion, Box, Column, Group, Row, Tab, TabItem, Tabs
8486
from gradio.mix import Parallel, Series
87+
from gradio.oauth import OAuthProfile
8588
from gradio.routes import Request, mount_gradio_app
8689
from gradio.templates import (
8790
Files,

gradio/blocks.py

+8
Original file line numberDiff line numberDiff line change
@@ -918,6 +918,14 @@ def __repr__(self):
918918
repr += f"\n |-{block}"
919919
return repr
920920

921+
@property
922+
def expects_oauth(self):
923+
"""Return whether the app expects user to authenticate via OAuth."""
924+
return any(
925+
isinstance(block, (components.LoginButton, components.LogoutButton))
926+
for block in self.blocks.values()
927+
)
928+
921929
def render(self):
922930
if Context.root_block is not None:
923931
if self._id in Context.root_block.blocks:

gradio/components/__init__.py

+4
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@
3333
from gradio.components.json_component import JSON
3434
from gradio.components.label import Label
3535
from gradio.components.line_plot import LinePlot
36+
from gradio.components.login_button import LoginButton
37+
from gradio.components.logout_button import LogoutButton
3638
from gradio.components.markdown import Markdown
3739
from gradio.components.model3d import Model3D
3840
from gradio.components.number import Number
@@ -87,6 +89,8 @@
8789
"Json",
8890
"Label",
8991
"LinePlot",
92+
"LoginButton",
93+
"LogoutButton",
9094
"Markdown",
9195
"Textbox",
9296
"Dropdown",

gradio/components/login_button.py

+95
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
"""Predefined button to sign in with Hugging Face in a Gradio Space."""
2+
from __future__ import annotations
3+
4+
import warnings
5+
from typing import Any, Literal
6+
7+
from gradio_client.documentation import document, set_documentation_group
8+
9+
from gradio.components import Button
10+
from gradio.context import Context
11+
from gradio.routes import Request
12+
13+
set_documentation_group("component")
14+
15+
16+
@document()
17+
class LoginButton(Button):
18+
"""
19+
Button that redirects the user to Sign with Hugging Face using OAuth.
20+
"""
21+
22+
is_template = True
23+
24+
def __init__(
25+
self,
26+
*,
27+
value: str = "Sign in with Hugging Face",
28+
variant: Literal["primary", "secondary", "stop"] = "secondary",
29+
size: Literal["sm", "lg"] | None = None,
30+
icon: str
31+
| None = "https://huggingface.co/front/assets/huggingface_logo-noborder.svg",
32+
link: str | None = None,
33+
visible: bool = True,
34+
interactive: bool = True,
35+
elem_id: str | None = None,
36+
elem_classes: list[str] | str | None = None,
37+
scale: int | None = 0,
38+
min_width: int | None = None,
39+
**kwargs,
40+
):
41+
super().__init__(
42+
value,
43+
variant=variant,
44+
size=size,
45+
icon=icon,
46+
link=link,
47+
visible=visible,
48+
interactive=interactive,
49+
elem_id=elem_id,
50+
elem_classes=elem_classes,
51+
scale=scale,
52+
min_width=min_width,
53+
**kwargs,
54+
)
55+
if Context.root_block is not None:
56+
self.activate()
57+
else:
58+
warnings.warn(
59+
"LoginButton created outside of a Blocks context. May not work unless you call its `activate()` method manually."
60+
)
61+
62+
def activate(self):
63+
# Taken from https://cmgdo.com/external-link-in-gradio-button/
64+
# Taking `self` as input to check if user is logged in
65+
# ('self' value will be either "Sign in with Hugging Face" or "Signed in as ...")
66+
self.click(fn=None, inputs=[self], outputs=None, _js=_js_open_if_not_logged_in)
67+
68+
self.attach_load_event(self._check_login_status, None)
69+
70+
def _check_login_status(self, request: Request) -> dict[str, Any]:
71+
# Each time the page is refreshed or loaded, check if the user is logged in and adapt label
72+
session = getattr(request, "session", None) or getattr(
73+
request.request, "session", None
74+
)
75+
if session is None or "oauth_profile" not in session:
76+
return self.update("Sign in with Hugging Face", interactive=True)
77+
else:
78+
username = session["oauth_profile"]["preferred_username"]
79+
return self.update(f"Signed in as {username}", interactive=False)
80+
81+
82+
# JS code to redirects to /login/huggingface if user is not logged in.
83+
# If the app is opened in an iframe, open the login page in a new tab.
84+
# Otherwise, redirects locally. Taken from https://stackoverflow.com/a/61596084.
85+
_js_open_if_not_logged_in = """
86+
(buttonValue) => {
87+
if (!buttonValue.includes("Signed in")) {
88+
if ( window !== window.parent ) {
89+
window.open('/login/huggingface', '_blank');
90+
} else {
91+
window.location.assign('/login/huggingface');
92+
}
93+
}
94+
}
95+
"""

gradio/components/logout_button.py

+52
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
"""Predefined button to sign out from Hugging Face in a Gradio Space."""
2+
from __future__ import annotations
3+
4+
from typing import Literal
5+
6+
from gradio_client.documentation import document, set_documentation_group
7+
8+
from gradio.components import Button
9+
10+
set_documentation_group("component")
11+
12+
13+
@document()
14+
class LogoutButton(Button):
15+
"""
16+
Button to log out a user from a Space.
17+
"""
18+
19+
is_template = True
20+
21+
def __init__(
22+
self,
23+
*,
24+
value: str = "Logout",
25+
variant: Literal["primary", "secondary", "stop"] = "secondary",
26+
size: Literal["sm", "lg"] | None = None,
27+
icon: str
28+
| None = "https://huggingface.co/front/assets/huggingface_logo-noborder.svg",
29+
# Link to logout page (which will delete the session cookie and redirect to landing page).
30+
link: str | None = "/logout",
31+
visible: bool = True,
32+
interactive: bool = True,
33+
elem_id: str | None = None,
34+
elem_classes: list[str] | str | None = None,
35+
scale: int | None = 0,
36+
min_width: int | None = None,
37+
**kwargs,
38+
):
39+
super().__init__(
40+
value,
41+
variant=variant,
42+
size=size,
43+
icon=icon,
44+
link=link,
45+
visible=visible,
46+
interactive=interactive,
47+
elem_id=elem_id,
48+
elem_classes=elem_classes,
49+
scale=scale,
50+
min_width=min_width,
51+
**kwargs,
52+
)

gradio/helpers.py

+27-2
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
import threading
1414
import warnings
1515
from pathlib import Path
16-
from typing import TYPE_CHECKING, Any, Callable, Iterable, Literal
16+
from typing import TYPE_CHECKING, Any, Callable, Iterable, Literal, Optional
1717

1818
import matplotlib.pyplot as plt
1919
import numpy as np
@@ -23,8 +23,9 @@
2323
from gradio_client.documentation import document, set_documentation_group
2424
from matplotlib import animation
2525

26-
from gradio import components, processing_utils, routes, utils
26+
from gradio import components, oauth, processing_utils, routes, utils
2727
from gradio.context import Context
28+
from gradio.exceptions import Error
2829
from gradio.flagging import CSVLogger
2930

3031
if TYPE_CHECKING: # Only import for type checking (to avoid circular imports).
@@ -690,6 +691,30 @@ def special_args(
690691
elif type_hint == routes.Request:
691692
if inputs is not None:
692693
inputs.insert(i, request)
694+
elif (
695+
type_hint == Optional[oauth.OAuthProfile]
696+
or type_hint == oauth.OAuthProfile
697+
# Note: "OAuthProfile | None" is equals to Optional[OAuthProfile] in Python
698+
# => it is automatically handled as well by the above condition
699+
# (adding explicit "OAuthProfile | None" would break in Python3.9)
700+
):
701+
if inputs is not None:
702+
# Retrieve session from gr.Request, if it exists (i.e. if user is logged in)
703+
session = (
704+
# request.session (if fastapi.Request obj i.e. direct call)
705+
getattr(request, "session", {})
706+
or
707+
# or request.request.session (if gr.Request obj i.e. websocket call)
708+
getattr(getattr(request, "request", None), "session", {})
709+
)
710+
oauth_profile = (
711+
session["oauth_profile"] if "oauth_profile" in session else None
712+
)
713+
if type_hint == oauth.OAuthProfile and oauth_profile is None:
714+
raise Error(
715+
"This action requires a logged in user. Please sign in and retry."
716+
)
717+
inputs.insert(i, oauth_profile)
693718
elif (
694719
type_hint
695720
and inspect.isclass(type_hint)

0 commit comments

Comments
 (0)