Skip to content

Commit e16e45c

Browse files
abidlabsgradio-pr-botaliabd
authored
Add docs for auth and rate limiting (#11773)
* docs * changes * add demo * add changeset * Update demo/rate_limit/run.py * notebook * add changeset * Update gradio/components/login_button.py Co-authored-by: Ali Abdalla <[email protected]> --------- Co-authored-by: gradio-pr-bot <[email protected]> Co-authored-by: Ali Abdalla <[email protected]>
1 parent 513d21e commit e16e45c

File tree

5 files changed

+143
-6
lines changed

5 files changed

+143
-6
lines changed

.changeset/proud-boxes-tie.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"gradio": patch
3+
---
4+
5+
feat:Add docs for auth and rate limiting

demo/rate_limit/run.ipynb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"cells": [{"cell_type": "markdown", "id": "302934307671667531413257853548643485645", "metadata": {}, "source": ["# Gradio Demo: rate_limit"]}, {"cell_type": "code", "execution_count": null, "id": "272996653310673477252411125948039410165", "metadata": {}, "outputs": [], "source": ["!pip install -q gradio "]}, {"cell_type": "code", "execution_count": null, "id": "288918539441861185822528903084949547379", "metadata": {}, "outputs": [], "source": ["import gradio as gr\n", "from datetime import datetime, timedelta\n", "from collections import defaultdict\n", "import threading\n", "\n", "rate_limit_data = defaultdict(list)\n", "lock = threading.Lock()\n", "\n", "UNAUTH_RATE_LIMIT = 3\n", "AUTH_RATE_LIMIT = 30\n", "RATE_LIMIT_WINDOW = 60\n", "\n", "def clean_old_entries(user_id):\n", " \"\"\"Remove entries older than the rate limit window\"\"\"\n", " current_time = datetime.now()\n", " cutoff_time = current_time - timedelta(seconds=RATE_LIMIT_WINDOW)\n", " rate_limit_data[user_id] = [\n", " timestamp for timestamp in rate_limit_data[user_id]\n", " if timestamp > cutoff_time\n", " ]\n", "\n", "def get_user_identifier(profile: gr.OAuthProfile | None, request: gr.Request) -> tuple[str, bool]:\n", " \"\"\"Get user identifier and whether they're authenticated\"\"\"\n", " if profile is not None:\n", " return profile.username, True\n", " else:\n", " if request:\n", " return f\"ip_{request.client.host}\", False\n", " return \"ip_unknown\", False\n", "\n", "def check_rate_limit(user_id: str, is_authenticated: bool) -> tuple[bool, int, int]:\n", " \"\"\"\n", " Check if user has exceeded rate limit\n", " Returns: (can_proceed, clicks_used, max_clicks)\n", " \"\"\"\n", " with lock:\n", " clean_old_entries(user_id)\n", " \n", " max_clicks = AUTH_RATE_LIMIT if is_authenticated else UNAUTH_RATE_LIMIT\n", " clicks_used = len(rate_limit_data[user_id])\n", " \n", " can_proceed = clicks_used < max_clicks\n", " \n", " return can_proceed, clicks_used, max_clicks\n", "\n", "def add_click(user_id: str):\n", " \"\"\"Add a click timestamp for the user\"\"\"\n", " with lock:\n", " rate_limit_data[user_id].append(datetime.now())\n", "\n", "def update_status(profile: gr.OAuthProfile | None, request: gr.Request) -> str:\n", " \"\"\"Update the status message showing current rate limit info\"\"\"\n", " user_id, is_authenticated = get_user_identifier(profile, request)\n", " _, clicks_used, max_clicks = check_rate_limit(user_id, is_authenticated)\n", " \n", " if is_authenticated:\n", " return f\"\u2705 You are logged in as '{profile.username}'. You have clicked {clicks_used} times this minute. You have {max_clicks} total clicks per minute.\" # type: ignore\n", " else:\n", " return f\"\u26a0\ufe0f You are not logged in. You have clicked {clicks_used} times this minute. You have {max_clicks} total clicks per minute.\"\n", "\n", "def run_action(profile: gr.OAuthProfile | None, request: gr.Request) -> tuple[str, str]:\n", " \"\"\"Handle the run button click with rate limiting\"\"\"\n", " user_id, is_authenticated = get_user_identifier(profile, request)\n", " can_proceed, clicks_used, max_clicks = check_rate_limit(user_id, is_authenticated)\n", " \n", " if not can_proceed:\n", " result = f\"\u274c Rate limit exceeded! You've used all {max_clicks} clicks for this minute. Please wait before trying again.\"\n", " status = update_status(profile, request)\n", " return result, status\n", " \n", " add_click(user_id)\n", " \n", " _, new_clicks_used, _ = check_rate_limit(user_id, is_authenticated)\n", " \n", " result = f\"\u2705 Action executed successfully! (Click #{new_clicks_used})\"\n", " status = update_status(profile, request)\n", " \n", " return result, status\n", "\n", "with gr.Blocks(title=\"Rate Limiting Demo\") as demo:\n", " gr.Markdown(\"# Rate Limiting Demo App\")\n", " gr.Markdown(\"This app demonstrates rate limiting based on authentication status.\")\n", " \n", " gr.LoginButton()\n", " \n", " status_text = gr.Markdown(\"Loading status...\")\n", " \n", " with gr.Row():\n", " run_btn = gr.Button(\"\ud83d\ude80 Run Action\", variant=\"primary\", scale=1)\n", " \n", " result_text = gr.Markdown(\"\")\n", " \n", " demo.load(update_status, inputs=None, outputs=status_text)\n", " \n", " run_btn.click(\n", " run_action,\n", " inputs=None,\n", " outputs=[result_text, status_text]\n", " )\n", " \n", " gr.Markdown(\"---\")\n", " gr.Markdown(\"\"\"\n", " ### Rate Limits:\n", " - **Not logged in:** 3 clicks per minute (based on IP address)\n", " - **Logged in:** 30 clicks per minute (based on HF username)\n", " \n", " ### How it works:\n", " - Click the **Login** button to authenticate with Hugging Face\n", " - Click the **Run Action** button to test the rate limiting\n", " - The system tracks your clicks over a rolling 1-minute window\n", " \"\"\")\n", "\n", "if __name__ == \"__main__\":\n", " demo.launch()\n"]}], "metadata": {}, "nbformat": 4, "nbformat_minor": 5}

demo/rate_limit/run.py

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import gradio as gr
2+
from datetime import datetime, timedelta
3+
from collections import defaultdict
4+
import threading
5+
6+
rate_limit_data = defaultdict(list)
7+
lock = threading.Lock()
8+
9+
UNAUTH_RATE_LIMIT = 3
10+
AUTH_RATE_LIMIT = 30
11+
RATE_LIMIT_WINDOW = 60
12+
13+
def clean_old_entries(user_id):
14+
"""Remove entries older than the rate limit window"""
15+
current_time = datetime.now()
16+
cutoff_time = current_time - timedelta(seconds=RATE_LIMIT_WINDOW)
17+
rate_limit_data[user_id] = [
18+
timestamp for timestamp in rate_limit_data[user_id]
19+
if timestamp > cutoff_time
20+
]
21+
22+
def get_user_identifier(profile: gr.OAuthProfile | None, request: gr.Request) -> tuple[str, bool]:
23+
"""Get user identifier and whether they're authenticated"""
24+
if profile is not None:
25+
return profile.username, True
26+
else:
27+
if request:
28+
return f"ip_{request.client.host}", False
29+
return "ip_unknown", False
30+
31+
def check_rate_limit(user_id: str, is_authenticated: bool) -> tuple[bool, int, int]:
32+
"""
33+
Check if user has exceeded rate limit
34+
Returns: (can_proceed, clicks_used, max_clicks)
35+
"""
36+
with lock:
37+
clean_old_entries(user_id)
38+
39+
max_clicks = AUTH_RATE_LIMIT if is_authenticated else UNAUTH_RATE_LIMIT
40+
clicks_used = len(rate_limit_data[user_id])
41+
42+
can_proceed = clicks_used < max_clicks
43+
44+
return can_proceed, clicks_used, max_clicks
45+
46+
def add_click(user_id: str):
47+
"""Add a click timestamp for the user"""
48+
with lock:
49+
rate_limit_data[user_id].append(datetime.now())
50+
51+
def update_status(profile: gr.OAuthProfile | None, request: gr.Request) -> str:
52+
"""Update the status message showing current rate limit info"""
53+
user_id, is_authenticated = get_user_identifier(profile, request)
54+
_, clicks_used, max_clicks = check_rate_limit(user_id, is_authenticated)
55+
56+
if is_authenticated:
57+
return f"✅ You are logged in as '{profile.username}'. You have clicked {clicks_used} times this minute. You have {max_clicks} total clicks per minute." # type: ignore
58+
else:
59+
return f"⚠️ You are not logged in. You have clicked {clicks_used} times this minute. You have {max_clicks} total clicks per minute."
60+
61+
def run_action(profile: gr.OAuthProfile | None, request: gr.Request) -> tuple[str, str]:
62+
"""Handle the run button click with rate limiting"""
63+
user_id, is_authenticated = get_user_identifier(profile, request)
64+
can_proceed, clicks_used, max_clicks = check_rate_limit(user_id, is_authenticated)
65+
66+
if not can_proceed:
67+
result = f"❌ Rate limit exceeded! You've used all {max_clicks} clicks for this minute. Please wait before trying again."
68+
status = update_status(profile, request)
69+
return result, status
70+
71+
add_click(user_id)
72+
73+
_, new_clicks_used, _ = check_rate_limit(user_id, is_authenticated)
74+
75+
result = f"✅ Action executed successfully! (Click #{new_clicks_used})"
76+
status = update_status(profile, request)
77+
78+
return result, status
79+
80+
with gr.Blocks(title="Rate Limiting Demo") as demo:
81+
gr.Markdown("# Rate Limiting Demo App")
82+
gr.Markdown("This app demonstrates rate limiting based on authentication status.")
83+
84+
gr.LoginButton()
85+
86+
status_text = gr.Markdown("Loading status...")
87+
88+
with gr.Row():
89+
run_btn = gr.Button("🚀 Run Action", variant="primary", scale=1)
90+
91+
result_text = gr.Markdown("")
92+
93+
demo.load(update_status, inputs=None, outputs=status_text)
94+
95+
run_btn.click(
96+
run_action,
97+
inputs=None,
98+
outputs=[result_text, status_text]
99+
)
100+
101+
gr.Markdown("---")
102+
gr.Markdown("""
103+
### Rate Limits:
104+
- **Not logged in:** 3 clicks per minute (based on IP address)
105+
- **Logged in:** 30 clicks per minute (based on HF username)
106+
107+
### How it works:
108+
- Click the **Login** button to authenticate with Hugging Face
109+
- Click the **Run Action** button to test the rate limiting
110+
- The system tracks your clicks over a rolling 1-minute window
111+
""")
112+
113+
if __name__ == "__main__":
114+
demo.launch()

gradio/components/login_button.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,18 @@
2222
@document()
2323
class LoginButton(Button):
2424
"""
25-
Creates a button that redirects the user to Sign with Hugging Face using OAuth. If
26-
created inside of a Blocks context, it will add an event to check if the user is logged in
27-
and update the button text accordingly. If created outside of a Blocks context, call the
28-
`LoginButton.activate()` method to add the event.
25+
Creates a "Sign In" button that redirects the user to sign in with Hugging Face OAuth.
26+
Once the user is signed in, the button will act as a logout button, and you can
27+
retrieve a signed-in user's profile by adding a parameter of type `gr.OAuthProfile`
28+
to any Gradio function. This will only work if this Gradio app is running in a
29+
Hugging Face Space. Permissions for the OAuth app can be configured in the Spaces
30+
README file, as described here: <a href="https://huggingface.co/docs/hub/en/spaces-oauth" target="_blank">Spaces OAuth Docs</a>.
31+
For local development, instead of OAuth, the local Hugging Face account that is
32+
logged in (via `hf auth login`) will be available through the `gr.OAuthProfile`
33+
object.
34+
35+
Demos: login_with_huggingface
36+
Guides: sharing-your-app
2937
"""
3038

3139
is_template = True

guides/04_additional-features/07_sharing-your-app.md

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,10 @@ In this Guide, we dive more deeply into the various aspects of sharing a Gradio
1010
6. [Accessing network requests](#accessing-the-network-request-directly)
1111
7. [Mounting within FastAPI](#mounting-within-another-fast-api-app)
1212
8. [Authentication](#authentication)
13-
9. [Analytics](#analytics)
14-
10. [Progressive Web Apps (PWAs)](#progressive-web-app-pwa)
13+
9. [MCP Servers](#mcp-servers)
14+
10. [Rate Limits](#rate-limits)
15+
11. [Analytics](#analytics)
16+
12. [Progressive Web Apps (PWAs)](#progressive-web-app-pwa)
1517

1618
## Sharing Demos
1719

@@ -421,6 +423,13 @@ if __name__ == '__main__':
421423

422424
There are actually two separate Gradio apps in this example! One that simply displays a log in button (this demo is accessible to any user), while the other main demo is only accessible to users that are logged in. You can try this example out on [this Space](https://huggingface.co/spaces/gradio/oauth-example).
423425

426+
## MCP Servers
427+
428+
Gradio apps can function as MCP (Model Context Protocol) servers, allowing LLMs to use your app's functions as tools. By simply setting `mcp_server=True` in the `.launch()` method, Gradio automatically converts your app's functions into MCP tools that can be called by MCP clients like Claude Desktop, Cursor, or Cline. The server exposes tools based on your function names, docstrings, and type hints, and can handle file uploads, authentication headers, and progress updates. You can also create MCP-only functions using `gr.api` and expose resources and prompts using decorators. For a comprehensive guide on building MCP servers with Gradio, see [Building an MCP Server with Gradio](https://www.gradio.app/guides/building-mcp-server-with-gradio).
429+
430+
## Rate Limits
431+
432+
When publishing your app publicly, and making it available via API or via MCP server, you might want to set rate limits to prevent users from abusing your app. You can identify users using their IP address (using the `gr.Request` object [as discussed above](#accessing-the-network-request-directly)) or, if they are logged in via Hugging Face OAuth, using their username. To see a complete example of how to set rate limits, please see [this Gradio app](https://github.com/gradio-app/gradio/blob/main/demo/rate_limit/run.py).
424433

425434
## Analytics
426435

0 commit comments

Comments
 (0)