Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Google Authentication #2790

Closed
yuvalkirstain opened this issue Dec 11, 2022 · 72 comments · Fixed by #7557
Closed

Google Authentication #2790

yuvalkirstain opened this issue Dec 11, 2022 · 72 comments · Fixed by #7557
Labels
enhancement New feature or request

Comments

@yuvalkirstain
Copy link

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
Copy link
Author

yuvalkirstain commented Dec 11, 2022

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")

@abidlabs abidlabs added the enhancement New feature or request label Dec 12, 2022
@davidbernat
Copy link

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.

@abidlabs abidlabs added new component Involves creating a new component enhancement New feature or request and removed enhancement New feature or request labels Feb 9, 2023
@abidlabs abidlabs added this to the Gradio 4.0 milestone Feb 21, 2023
@zinoubm
Copy link

zinoubm commented Mar 6, 2023

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
Copy link

zinoubm commented Mar 6, 2023

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.

@AGronowski
Copy link

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!

@jerpint
Copy link

jerpint commented May 2, 2023

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

@zinoubm
Copy link

zinoubm commented May 4, 2023

@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.

@jerpint
Copy link

jerpint commented May 4, 2023

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

@zinoubm
Copy link

zinoubm commented May 4, 2023

@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
Copy link

zinoubm commented May 4, 2023

@davidbernat
Copy link

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

@kambleakash0
Copy link

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.

@davidbernat
Copy link

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.

@kambleakash0
Copy link

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

@dhruv-anand-aintech
Copy link

@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.

@kambleakash0
Copy link

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

@AGronowski
Copy link

@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.

@abidlabs abidlabs removed this from the 4.0 milestone May 25, 2023
@ambiSk
Copy link

ambiSk commented Jul 7, 2023

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
Copy link

ambiSk commented Jul 10, 2023

What if we use some Middleware from FastAPI?

@davidbernat
Copy link

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.

@ambiSk
Copy link

ambiSk commented Jul 11, 2023

@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

@abidlabs
Copy link
Member

Thanks @EshamAaqib yes that works. I do something similar here: https://huggingface.co/spaces/gradio/oauth-example/blob/6c746b5828864cb057ed5d9a9fe725d91d6723fa/app.py#L53

gr.route_utils.get_root_url gets the root url and adds https if running on https (but I guess better to be explicit about this since get_root_url is an internal functions and could change)

@pseudotensor
Copy link
Contributor

pseudotensor commented Mar 18, 2024

@abidlabs Does the code in gradio have this fixed so handles generally? Or have to tweak this wrapper code every time for either http or https cases?

@pseudotensor
Copy link
Contributor

@abidlabs For this google auth solution, seems to be better to avoid having to mount things and be just a part of gradio. Not like google is a random thing, everyone uses it for auth. So better to be in gradio as option not as external mount. That way no issues with rest of usage, like options in .launch

@abidlabs
Copy link
Member

@abidlabs For this google auth solution, seems to be better to avoid having to mount things and be just a part of gradio. Not like google is a random thing, everyone uses it for auth. So better to be in gradio as option not as external mount. That way no issues with rest of usage, like options in .launch

Not really possible, because you need to define other routes to handle the login page, auth page, etc. I considered other options, but its better to provide a template and then let users define these routes so that they can modify them as needed rather than hide them behind a layer of abstraction.

@pseudotensor
Copy link
Contributor

What about all the launch parameters? Can can one pass all of those through to gradio?

@abidlabs
Copy link
Member

Depends which ones. server_port and server_name are passed to uvicorn directly. auth and auth_message (which you wouldn't use if you were using auth_dependency) would be passed into gr.mount_gradio_app.

If there's something else you need, let us know and we can add support

@pseudotensor
Copy link
Contributor

These are the ones we set that I don't know how to pass through:

                    ssl_verify
                    favicon_path
                    prevent_thread_lock
                    allowed_paths
                    blocked_paths
                    )

@abidlabs
Copy link
Member

ssl_verify shouldn't need to be set since I don't believe it has any effect when the gradio app is mounted within a fastapi app. I'll expose the other parameters in gr.mount_gradio_app

@abidlabs
Copy link
Member

abidlabs commented Mar 18, 2024

Also out of curiosity, why do you need prevent_thread_lock? It might be difficult to include in the mounted context

@pseudotensor
Copy link
Contributor

In general for testing, I launch server then the test in single process, so I don't want gradio to block.

@pseudotensor
Copy link
Contributor

FYI, I'd also pass show_error if was option, but less critical.

@pseudotensor
Copy link
Contributor

I'd guess if uvicorn doesn't support prevent_thread_lock like behavior, not really possible to do that anyways.

@abidlabs
Copy link
Member

It doesn't really make sense to pass in prevent_thread_lock since you should be running the entire fastapi app in a separate thread if you want to be able to terminate it easily and use a signal to shut it down. The Gradio app never really "launches".

Everything else, I have added here, if you want to give it a spin: #7734

@pseudotensor
Copy link
Contributor

Any thoughts on "workers"?

fastapi/fastapi#1495 (comment)

I hit:

WARNING:  You must pass the application as an import string to enable 'reload' or 'workers'.

It seems like it may just be a naming issue, but I'm not expert.

@pseudotensor
Copy link
Contributor

@abidlabs Next question, how do to demo.load() with this mounting way? i.e. I have demo.load() run some login stuff to setup user for given user that logged in via normal auth using gr.Request.

But with the google auth way, demo.load() and demo.queue() are never done AFAIK.

Which gets to the next question, which is what about demo.queue? API access with auth was done recently in gradio 4, but how about through google auth? What about concurrency limit controls?

@abidlabs
Copy link
Member

Hi @pseudotensor you can still do demo.load() and demo.queue(). The only method you do not call is demo.launch().

@pseudotensor
Copy link
Contributor

Hi @abidlabs , hmm, it's not doing what's in demo.load(), however. I'll double check. and make repro if there is issue. Thanks!

@pseudotensor
Copy link
Contributor

@abidlabs Ok, demo.load() issue was just user mistake.

Last thing I'm wondering is how to identify a user. gr. requests has username but nothing else that seems unique, and that username is just the user's google full name, but that can be same for different people. Is there way to get the email that was used?

@pseudotensor
Copy link
Contributor

@abidlabs I looked into the starlette request object in your code, but nothing has email or any other things except cookie (which doesn't match in persistent way).

Seems required to get email not just user from google to ensure authentication is proper.

In non-google case, we have user + password to disentangle and verify, but here name is not enough.

@pseudotensor
Copy link
Contributor

@abidlabs Ok, I figured out. The requests session also has email, picture, etc. So I make user name name + email + picture url and then process that in main gradio app as required. name+email is for auth, pic just for showing user.

@abidlabs
Copy link
Member

Yes exactly, you can pass whatever attributes you want as a dict in username -- it doesn't have to be a string actually. Maybe I should clarify that actually.

@pseudotensor
Copy link
Contributor

Ya that would be useful to know. At moment I did some ugly string appending and splitting to handle.

@pseudotensor
Copy link
Contributor

@abidlabs QQ

Can the login demo part (before actual demo) show both the normal auth login in addition to the google button to login via google? I'd like to be able to support either option instead of only one.

I'm not quite sure how to do that.

Thanks!

@abidlabs
Copy link
Member

Hi @pseudotensor no we don't support both the regular auth and auth_dependency and don't really have plans to add this as it could lead to a confusing security model.

That being said, auth_dependency is very flexible, and it should be possible for you to create a separate login page that looks like the basic gradio auth page, and then it sets a username in the session that you can retrieve via the auth_dependency.

@chadbr
Copy link

chadbr commented Apr 1, 2024

@abidlabs - sorry for the beginner questions...

I've got the google auth working as shown in your demo code, etc.. (thanks!)

But in the category of "no good deed goes unpunished" --

Do you have any idea what it would take to integrate Firebase / Identity Platform authentication?

All of their samples show some form of HTML being integrated into an application - is that something I could achieve with Custom components? https://www.gradio.app/custom-components/gallery

Or do you think the existing Google auth support would work with some python tweaking?

Firebase Authentication
https://firebase.google.com/docs/auth
Identity Platform | Google Cloud
https://cloud.google.com/security/products/identity-platform?hl=en

@abidlabs
Copy link
Member

abidlabs commented Apr 1, 2024

All of their samples show some form of HTML being integrated into an application - is that something I could achieve with Custom components?

Can you share an example of this? If you need to add custom HTML or JS to your Gradio application, you don't necessarily need to build a custom component, you might be able to do this with https://www.gradio.app/guides/custom-CSS-and-JS

@chadbr
Copy link

chadbr commented Apr 4, 2024

Sorry for the slow reply...

https://cloud.google.com/identity-platform/docs/sign-in-user-email#sign_in_the_user

Shows this basic flow:

import { initializeApp } from 'firebase/app';

const firebaseConfig = {
    apiKey: "API_KEY",
    authDomain: "AUTH_DOMAIN"
};

const app = initializeApp(firebaseConfig);

and further down, a more complete solution... notice calls to login function:

import { initializeApp } from 'firebase/app';
import {
  onAuthStateChanged,
  signInWithEmailAndPassword,
  getAuth
} from 'firebase/auth';

const firebaseConfig = {
  apiKey: "API_KEY",
  authDomain: "AUTH_DOMAIN"
};
const app = initializeApp(firebaseConfig);
const auth = getAuth(app, {/* extra options */ });

document.addEventListener("DOMContentLoaded", () => {
  onAuthStateChanged(auth, (user) => {
      if (user) {
          document.getElementById("message").innerHTML = "Welcome, " + user.email;
      }
      else {
          document.getElementById("message").innerHTML = "No user signed in.";
      }
  });
  signIn();
});

function signIn() {
  const email = "EMAIL_ID";
  const password = "PASSWORD";
  signInWithEmailAndPassword(auth, email, password)
      .catch((error) => {
          document.getElementById("message").innerHTML = error.message;
      });
}

Thanks for the pointer here - I'll have a look - https://www.gradio.app/guides/custom-CSS-and-JS

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet