gradio icon indicating copy to clipboard operation
gradio copied to clipboard

Google Authentication

Open yuvalkirstain opened this issue 1 year ago • 28 comments

In some apps, we want to filter harmful users or bots. I think that having a component that enables google authentication (rather than a username, passowrd authentication) in gradio can be very helpful.

Describe the solution you'd like
I'd like to have an authentication component that receives the details of the user so it can decide if the user may access the app or not.

yuvalkirstain avatar Dec 11 '22 09:12 yuvalkirstain

This code is partial (and unfortunately non-elegant) solution. It:

  1. creates a new app that mounts the gradio app
  2. the new app is in charge of the google authentication.

A key aspect that is missed here, is that in the case that the user goes straight ahead to the gradio endpoint, there is no redirection to login and the user is stuck there. Is there a way by any chance to redirect from within the gradio demo back to \login?

import json
from authlib.integrations.base_client import OAuthError
from fastapi import FastAPI
from starlette.middleware.sessions import SessionMiddleware
from starlette.responses import HTMLResponse, RedirectResponse
from starlette.requests import Request
import gradio as gr
from authlib.integrations.starlette_client import OAuth
from starlette.config import Config

# CODE FOR NEW APP

app = FastAPI()
app.add_middleware(SessionMiddleware, secret_key="!secret")

config = Config('.env')
oauth = OAuth(config)

CONF_URL = 'https://accounts.google.com/.well-known/openid-configuration'
oauth.register(
    name='google',
    server_metadata_url=CONF_URL,
    client_kwargs={
        'scope': 'openid email profile'
    }
)


@app.get('/')
async def homepage(request: Request):
    user = request.session.get('user')
    if user:
        data = json.dumps(user)
        html = (
            f'<pre>{data}</pre>'
            '<a href="/logout">logout</a>'
            '<br>'
            '<a href="/gradio">demo</a>'
        )
        return HTMLResponse(html)
    return HTMLResponse('<a href="/login">login</a>')


@app.get('/login')
async def login(request: Request):
    redirect_uri = request.url_for('auth')
    return await oauth.google.authorize_redirect(request, redirect_uri)


@app.get('/auth')
async def auth(request: Request):
    print(f"before request user {request.session.get('user')}")
    try:
        token = await oauth.google.authorize_access_token(request)
    except OAuthError as error:
        return HTMLResponse(f'<h1>{error.error}</h1>')
    user = token.get('userinfo')
    if user:
        request.session['user'] = dict(user)
    print(f"after request user {request.session.get('user')}")
    return RedirectResponse(url='/')


@app.get('/logout')
async def logout(request: Request):
    request.session.pop('user', None)
    return RedirectResponse(url='/')

# CODE FOR MOUNTED GRADIO APP

def update(name, request: gr.Request):
    return f"Welcome to Gradio, {name}!\n{request.request.session.get('user')}"


def make_demo_visible(request: gr.Request):
    if request.request.session.get('user'):
        return gr.update(visible=True), gr.update(visible=True), gr.update(visible=True), gr.update(visible=False)
    return gr.update(visible=False), gr.update(visible=False), gr.update(visible=False), gr.update(value="Looks like you are not logged in. Please login at the main app.")


with gr.Blocks() as demo:
    start_btn = gr.Button("Press Here to initialize the demo!")

    with gr.Row():
        inp = gr.Textbox(placeholder="What is your name?", visible=False)
        out = gr.Textbox(visible=False)

    btn = gr.Button("Run", visible=False)

    start_btn.click(make_demo_visible, outputs=[inp, out, btn, start_btn])
    btn.click(fn=update, inputs=inp, outputs=out)

gradio_app = gr.mount_gradio_app(app, demo, "/gradio")

yuvalkirstain avatar Dec 11 '22 12:12 yuvalkirstain

Google Firebase offers an excellent solution to this use case. I have a useful API on the backend server data processing that handles inbound authorization requests from a Google Firebase front-end. (I would be happy to offer my code to anyone who asks.) Google Firebase front-end support is excellent. I have a React UI from previous repositories and directing users to that landing page; require them to log in with the Firebase token or via Google Login, then redirects the user to the primary front-end of the app (the Gradio UI in this case). I have not installed that into my current Gradio UI only because I am only now developing the app via Gradio, though the software has served me excellently for several years now. It would even be relatively easy to embed an email and UUID into an existing Gradio demo which communicates first with the backend for Google Firebase; which then, upon returning confirmation of the email-UUID match makes numerous, otherwise invisible and inactive Gradio UI blocks appear. I think that might even be easier in a first pass to do this because there is no need to mount a second service for the front-end. This solution is similar to @yuvalkirstain with Google Firebase handling the auth.

davidbernat avatar Dec 12 '22 17:12 davidbernat

Hi @davidbernat, inroder to request permissions from google oauth. my app needs to redirect to a url provided by google api. The problem is that gradio doesn't offer a redirect feature.

zinoubm avatar Mar 06 '23 21:03 zinoubm

Hey @yuvalkirstain , Can you give me a way to redirect the user to a given url, I'll be really happy if you can help me.

zinoubm avatar Mar 06 '23 21:03 zinoubm

Google Firebase offers an excellent solution to this use case. I have a useful API on the backend server data processing that handles inbound authorization requests from a Google Firebase front-end. (I would be happy to offer my code to anyone who asks.) Google Firebase front-end support is excellent. I have a React UI from previous repositories and directing users to that landing page; require them to log in with the Firebase token or via Google Login, then redirects the user to the primary front-end of the app (the Gradio UI in this case). I have not installed that into my current Gradio UI only because I am only now developing the app via Gradio, though the software has served me excellently for several years now. It would even be relatively easy to embed an email and UUID into an existing Gradio demo which communicates first with the backend for Google Firebase; which then, upon returning confirmation of the email-UUID match makes numerous, otherwise invisible and inactive Gradio UI blocks appear. I think that might even be easier in a first pass to do this because there is no need to mount a second service for the front-end. This solution is similar to @yuvalkirstain with Google Firebase handling the auth.

Hey @davidbernat , I'm trying to implement exactly this, add authentication to a gradio app, pass in a UUID and make blocks visible after comparing the UUID with what's in a database. If you'd be willing to share any code or give any advice, that would be really helpful!

AGronowski avatar Apr 10 '23 21:04 AGronowski

Has anyone made any progress on this? I would like to also have firebase authenticate users and redirect to my app once authenticated

jerpint avatar May 02 '23 15:05 jerpint

@jerpint I found that gradio is built on top of fastapi, so for me I mounted gradio app on top of a fastapi app that handle authentication.

zinoubm avatar May 04 '23 00:05 zinoubm

@zinoubm i was thinking of doing something similar, do you have a working example?

jerpint avatar May 04 '23 00:05 jerpint

@jerpint Sorry about that, It was for a client so I can't share it. But I believe the docs have some useful resources.

zinoubm avatar May 04 '23 01:05 zinoubm

@jerpint , You may find this helpful https://gradio.app/sharing-your-app/#mounting-within-another-fastapi-app

zinoubm avatar May 04 '23 01:05 zinoubm

I solved this problem. Please mark this issue as complete. Starlight LLC. Thanks.

davidbernat avatar May 04 '23 01:05 davidbernat

Solved? Where can I see this enhancement @davidbernat?

I just came across a case where I will need to integrate Google login to a Gradio app and stumbled upon this discussion just now.

kambleakash0 avatar May 04 '23 18:05 kambleakash0

Unfortunately until Google and Apple shift toward more open models of research and data in their AI division we have decided to close our doors to each company here at Starlight LLC and Starlight.AI LLC. You may feel free to reach out to me directly, though I wish to not discuss this project at this time as Google has shifted its backend priorities anyway, and legal action is already in discussion.

davidbernat avatar May 04 '23 19:05 davidbernat

So there's no other way than to wrap it in a FastAPI app and then implement auth for that FastAPI app?

kambleakash0 avatar May 05 '23 07:05 kambleakash0

@davidbernat you've not solved anything as far as I can see. Please keep this issue open until the functionality is implemented in Gradio, or there is a simple process we can follow to make it work well.

dhruv-anand-aintech avatar May 05 '23 08:05 dhruv-anand-aintech

@yuvalkirstain Have you hosted this somewhere so it'll be helpful for us to take a look?

kambleakash0 avatar May 05 '23 08:05 kambleakash0

@zinoubm I know you can't share your code, but could you explain how you access the gr.Request to get the user information? I tried the above solution by @yuvalkirstain but it only works when the queue is disabled. Enabling the queue makes request.request be None and the solution no longer works.

AGronowski avatar May 25 '23 08:05 AGronowski

Any final update on this thread? How to enable Google SSO for Gradio Apps, I tried the mount_gradio_app function based on @yuvalkirstain suggestion, but still when I directly go to the pathway, it overrides the login and directly make it accessible. Someone provide alternative approach for Google SSO

ambiSk avatar Jul 07 '23 05:07 ambiSk

What if we use some Middleware from FastAPI?

ambiSk avatar Jul 10 '23 07:07 ambiSk

The issue is not the API. All that is required is an HTML element that can create API calls via JS or, better yet, a hook into a Python function. Unfortunately, my understanding is that Gradio is not providing those yet, and I created my own.

davidbernat avatar Jul 11 '23 15:07 davidbernat

@yuvalkirstain @jerpint @kambleakash0 @dhruv-anand-aintech I think I found a solution to enforce authentication on gradio app, using a custom middleware helps with this, here's the code of @yuvalkirstain with the middleware:

import json
from authlib.integrations.base_client import OAuthError
from fastapi import FastAPI
from starlette.middleware.sessions import SessionMiddleware
from starlette.responses import HTMLResponse, RedirectResponse
from starlette.requests import Request
import gradio as gr
from authlib.integrations.starlette_client import OAuth
from starlette.config import Config

# CODE FOR NEW APP

app = FastAPI()

config = Config('.env')
oauth = OAuth(config)

CONF_URL = 'https://accounts.google.com/.well-known/openid-configuration'
oauth.register(
    name='google',
    server_metadata_url=CONF_URL,
    client_kwargs={
        'scope': 'openid email profile'
    }
)

# The Middleware that enforces authentication on /gradio app
@app.middleware("http")
async def check_authentication(request: Request, call_next):
    if request.url.path.startswith('/login') or request.url.path.startswith('/auth'):
        # Skip authentication check for login and authentication routes
        return await call_next(request)
        
    if request.url.path=='/gradio/api/predict' or request.url.path=='/gradio/reset':
        return await call_next(request)
    
    user = request.session.get("user")
    if not user:
        
        # User is not logged in, redirect to login page
        return RedirectResponse(url="/login")

    return await call_next(request)

@app.get('/')
async def homepage(request: Request):
    user = request.session.get('user')
    if user:
        data = json.dumps(user)
        html = (
            f'<pre>{data}</pre>'
            '<a href="/logout">logout</a>'
            '<br>'
            '<a href="/gradio">demo</a>'
        )
        return HTMLResponse(html)
    return HTMLResponse('<a href="/login">login</a>')


@app.get('/login')
async def login(request: Request):
    redirect_uri = request.url_for('auth')
    return await oauth.google.authorize_redirect(request, redirect_uri)


@app.get('/auth')
async def auth(request: Request):
    print(f"before request user {request.session.get('user')}")
    try:
        token = await oauth.google.authorize_access_token(request)
    except OAuthError as error:
        return HTMLResponse(f'<h1>{error.error}</h1>')
    user = token.get('userinfo')
    if user:
        request.session['user'] = dict(user)
    print(f"after request user {request.session.get('user')}")
    return RedirectResponse(url='/')


@app.get('/logout')
async def logout(request: Request):
    request.session.pop('user', None)
    return RedirectResponse(url='/')

# CODE FOR MOUNTED GRADIO APP

def update(name, request: gr.Request):
    return f"Welcome to Gradio, {name}!\n{request.request.session.get('user')}"


def make_demo_visible(request: gr.Request):
    if request.request.session.get('user'):
        return gr.update(visible=True), gr.update(visible=True), gr.update(visible=True), gr.update(visible=False)
    return gr.update(visible=False), gr.update(visible=False), gr.update(visible=False), gr.update(value="Looks like you are not logged in. Please login at the main app.")


with gr.Blocks() as demo:
    start_btn = gr.Button("Press Here to initialize the demo!")

    with gr.Row():
        inp = gr.Textbox(placeholder="What is your name?", visible=False)
        out = gr.Textbox(visible=False)

    btn = gr.Button("Run", visible=False)

    start_btn.click(make_demo_visible, outputs=[inp, out, btn, start_btn])
    btn.click(fn=update, inputs=inp, outputs=out)

gradio_app = gr.mount_gradio_app(app, demo, "/gradio")
app.add_middleware(SessionMiddleware, secret_key="!secret")

In the middleware you'll find these lines of code:

    if request.url.path=='/gradio/api/predict' or request.url.path=='/gradio/reset':
        return await call_next(request)

I included this because I found that when using this middleware, the fastapi makes POST request to /gradio/api/predict and /gradio/reset which messes up with gradio functionality, instead of making POST request over those, it has to make POST request on /api/predict and /reset to make gradio app function. I think developers need to work on routes.py, so that if someone makes multiple pathways to multiple gradio app they don't need to write condition statement for each app.

Please provide feedback on this solution and let me know if there's a better solution than this

ambiSk avatar Jul 11 '23 16:07 ambiSk

@ambiSk Sorry for naive question, but how do you run the above block. When I run it, nothing stops and it just completes the code without effect.

pseudotensor avatar Aug 04 '23 03:08 pseudotensor

@pseudotensor I understand you are not aware of how to launch a FastAPI app. Install uvicorn in your python environment using:

pip installl -U uvicorn

Then do either of the following:

  • add this code line at the end of above script:
if __name__ == "__main__":
    uvicorn.run(app)

When you run this file on CLI, you'll get domain to your tool

  • Lets say the filename of this app is main.py, instead of adding the code from above, just run following command in your CLI. Make sure your CLI has opened in the directory where main.py is:
uvicorn main:app 

ambiSk avatar Aug 04 '23 05:08 ambiSk

Hey! We've now made it possible for Gradio users to create their own custom components -- meaning that you can write some Python and JavaScript (Svelte), and publish it as a Gradio component. You can use it in your own Gradio apps, or share it so that anyone can use it in their Gradio apps. Here are some examples of custom Gradio components:

You can see the source code for those components by clicking the "Files" icon and then clicking "src". The complete source code for the backend and frontend is visible. In particular, its very fast if you want to build off an existing component. We've put together a Guide: https://www.gradio.app/guides/five-minute-guide, and we're happy to help. Hopefully this will help address this issue.

abidlabs avatar Nov 07 '23 00:11 abidlabs

@ambiSk

I can't get that to work as end-to-end example. So I have this code block as main.py:

import json

import uvicorn
from authlib.integrations.base_client import OAuthError
from fastapi import FastAPI
from starlette.middleware.sessions import SessionMiddleware
from starlette.responses import HTMLResponse, RedirectResponse
from starlette.requests import Request
import gradio as gr
from authlib.integrations.starlette_client import OAuth
from starlette.config import Config

# CODE FOR NEW APP

app = FastAPI()

config = Config('.env')
oauth = OAuth(config)

CONF_URL = 'https://accounts.google.com/.well-known/openid-configuration'
oauth.register(
    name='google',
    server_metadata_url=CONF_URL,
    client_kwargs={
        'scope': 'openid email profile'
    }
)

# The Middleware that enforces authentication on /gradio app
@app.middleware("http")
async def check_authentication(request: Request, call_next):
    if request.url.path.startswith('/login') or request.url.path.startswith('/auth'):
        # Skip authentication check for login and authentication routes
        return await call_next(request)

    if request.url.path=='/gradio/api/predict' or request.url.path=='/gradio/reset':
        return await call_next(request)

    user = request.session.get("user")
    if not user:

        # User is not logged in, redirect to login page
        return RedirectResponse(url="/login")

    return await call_next(request)

@app.get('/')
async def homepage(request: Request):
    user = request.session.get('user')
    if user:
        data = json.dumps(user)
        html = (
            f'<pre>{data}</pre>'
            '<a href="/logout">logout</a>'
            '<br>'
            '<a href="/gradio">demo</a>'
        )
        return HTMLResponse(html)
    return HTMLResponse('<a href="/login">login</a>')


@app.get('/login')
async def login(request: Request):
    redirect_uri = request.url_for('auth')
    return await oauth.google.authorize_redirect(request, redirect_uri)


@app.get('/auth')
async def auth(request: Request):
    print(f"before request user {request.session.get('user')}")
    try:
        token = await oauth.google.authorize_access_token(request)
    except OAuthError as error:
        return HTMLResponse(f'<h1>{error.error}</h1>')
    user = token.get('userinfo')
    if user:
        request.session['user'] = dict(user)
    print(f"after request user {request.session.get('user')}")
    return RedirectResponse(url='/')


@app.get('/logout')
async def logout(request: Request):
    request.session.pop('user', None)
    return RedirectResponse(url='/')

# CODE FOR MOUNTED GRADIO APP

def update(name, request: gr.Request):
    return f"Welcome to Gradio, {name}!\n{request.request.session.get('user')}"


def make_demo_visible(request: gr.Request):
    if request.request.session.get('user'):
        return gr.update(visible=True), gr.update(visible=True), gr.update(visible=True), gr.update(visible=False)
    return gr.update(visible=False), gr.update(visible=False), gr.update(visible=False), gr.update(value="Looks like you are not logged in. Please login at the main app.")


with gr.Blocks() as demo:
    start_btn = gr.Button("Press Here to initialize the demo!")

    with gr.Row():
        inp = gr.Textbox(placeholder="What is your name?", visible=False)
        out = gr.Textbox(visible=False)

    btn = gr.Button("Run", visible=False)

    start_btn.click(make_demo_visible, outputs=[inp, out, btn, start_btn])
    btn.click(fn=update, inputs=inp, outputs=out)

gradio_app = gr.mount_gradio_app(app, demo, "/gradio")
app.add_middleware(SessionMiddleware, secret_key="!secret")

and I run:

uvicorn main:app 

As soon as I go to http://127.0.0.1:8000 it shows:

Sign in with Google
Access blocked: Authorization Error

[email protected]
The OAuth client was not found.
If you are a developer of this app, see [error details](https://accounts.google.com/).
Error 401: invalid_client

I removed my email as XXXX.

Do I need to fill .env file with something about who's authorized? I'm confused.

pseudotensor avatar Nov 26 '23 23:11 pseudotensor

@pseudotensor Did you register your app in Google authentication service(I am not familiar to Google SSO, this is just a convenient reference to the product, not the product name)? Because the error message seems you passed a wrong client_id to Google, and Google refused to proceed.

thiner avatar Nov 27 '23 02:11 thiner

Thanks, that helped. Now I'm stuck with my own app hanging after reaching loading (visual loading of gradio app with spinning candy looking thing) and unable to find style css files etc.

pseudotensor avatar Nov 27 '23 09:11 pseudotensor

@abidlabs I am not able to understand how the custom component support will solve the OP's auth issue. I am in a similar situation as the OP. Perhaps it will help if you can post a hello world gradio code that uses firebase google auth and posts a custom message for the user.

delip avatar Feb 17 '24 22:02 delip

Sorry for the late response @delip -- will work on something for this

abidlabs avatar Feb 28 '24 00:02 abidlabs

For anyone who'd like to test this feature, I have a PR ready here: https://github.com/gradio-app/gradio/pull/7557

You can install this Gradio from this PR by doing:

pip install -q https://gradio-builds.s3.amazonaws.com/abf7bda7be2e2375e09514fde0ea8c86491900af/gradio-4.19.2-py3-none-any.whl

And you can add e.g. Google OAuth to your Gradio app like this:

import os
from authlib.integrations.starlette_client import OAuth, OAuthError
from fastapi import FastAPI, Depends, Request
from starlette.config import Config
from starlette.responses import RedirectResponse
from starlette.middleware.sessions import SessionMiddleware
import uvicorn
import gradio as gr

app = FastAPI()

# OAuth settings
GOOGLE_CLIENT_ID = "..."
GOOGLE_CLIENT_SECRET = "..."
SECRET_KEY = "..."

# Set up OAuth
config_data = {'GOOGLE_CLIENT_ID': GOOGLE_CLIENT_ID, 'GOOGLE_CLIENT_SECRET': GOOGLE_CLIENT_SECRET}
starlette_config = Config(environ=config_data)
oauth = OAuth(starlette_config)
oauth.register(
    name='google',
    server_metadata_url='https://accounts.google.com/.well-known/openid-configuration',
    client_kwargs={'scope': 'openid email profile'},
)

SECRET_KEY = os.environ.get('SECRET_KEY') or "a_very_secret_key"
app.add_middleware(SessionMiddleware, secret_key=SECRET_KEY)

# Dependency to get the current user
def get_user(request: Request):
    user = request.session.get('user')
    if user:
        return user['name']
    return None

@app.get('/')
def public(user: dict = Depends(get_user)):
    if user:
        return RedirectResponse(url='/gradio')
    else:
        return RedirectResponse(url='/login-demo')

@app.route('/logout')
async def logout(request: Request):
    request.session.pop('user', None)
    return RedirectResponse(url='/')

@app.route('/login')
async def login(request: Request):
    redirect_uri = request.url_for('auth')
    return await oauth.google.authorize_redirect(request, redirect_uri)

@app.route('/auth')
async def auth(request: Request):
    try:
        access_token = await oauth.google.authorize_access_token(request)
    except OAuthError:
        return RedirectResponse(url='/')
    request.session['user'] = dict(access_token)["userinfo"]
    return RedirectResponse(url='/')

with gr.Blocks() as login_demo:
    gr.Button("Login", link="/login")

app = gr.mount_gradio_app(app, login_demo, path="/login-demo")

def greet(request: gr.Request):
    return f"Welcome to Gradio, {request.username}"

with gr.Blocks() as main_demo:
    m = gr.Markdown("Welcome to Gradio!")
    gr.Button("Logout", link="/logout")
    main_demo.load(greet, None, m)

app = gr.mount_gradio_app(app, main_demo, path="/gradio", auth_dependency=get_user)


if __name__ == '__main__':
    uvicorn.run(app)

abidlabs avatar Feb 28 '24 23:02 abidlabs