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

Add Google OAuth Login flow. #459

Merged
merged 11 commits into from
Jul 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .env
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,11 @@ DUCKDB_USE_VIEWS=0
# HF_USERNAME=
# The default repo to deploy to for a staging demo. Can be overridden by a command line flag.
# HF_STAGING_DEMO_REPO='HF_ORG/HF_REPO_NAME'

# For Google-login. This is generated from the Google Cloud Console for a web client.
# See: https://developers.google.com/identity/protocols/oauth2
GOOGLE_CLIENT_ID='279475920249-i8llm8vbos1vj5m1qocir8narb3r0enu.apps.googleusercontent.com'
# The client secret of the above client.
# GOOGLE_CLIENT_SECRET=
# A random string for oauth sessions.
# LILAC_OAUTH_SECRET_KEY=
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,25 @@ To run the docker image locally:
docker run -p 5432:5432 lilac_blueprint
```

#### Authentication

Authentication is done via Google login. A Google Client token should be created
from the Google API Console. Details can be found [here](https://developers.google.com/identity/protocols/oauth2).

By default, the Lilac google client is used. The secret can be found in Google
Cloud console, and should be defined under `GOOGLE_CLIENT_SECRET` in .env.local.

For the session middleware, a random string should be created and defined as `LILAC_OAUTH_SECRET_KEY` in .env.local.

You can generate a random secret key with:

```py
import string
import random
key = ''.join(random.choices(string.ascii_uppercase + string.digits, k=64))
print(f"LILAC_OAUTH_SECRET_KEY='{key}'")
```

### Configuration

To use various API's, API keys need to be provided. Create a file named `.env.local` in the root, and add variables that are listed in `.env` with your own values.
Expand Down
4 changes: 4 additions & 0 deletions mypy.ini
Original file line number Diff line number Diff line change
Expand Up @@ -116,3 +116,7 @@ follow_imports = skip
[mypy-langdetect.*]
ignore_missing_imports = True
follow_imports = skip

[mypy-authlib.*]
ignore_missing_imports = True
follow_imports = skip
74 changes: 72 additions & 2 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

92 changes: 48 additions & 44 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,30 +9,33 @@ version = "0.0.1"
[tool.poetry.dependencies]

### Required dependencies. ###
dask = "^2023.3.2"
datasets = "^2.12.0"
distributed = "^2023.3.2.1"
duckdb = "^0.8.1"
fastapi = "^0.98.0"
gcsfs = "^2023.4.0"
google-cloud-storage = "^2.5.0"
gunicorn = "^20.1.0"
joblib = "^1.3.1"
authlib = "^1.2.1"
dask = "^2023.3.2"
datasets = "^2.12.0"
distributed = "^2023.3.2.1"
duckdb = "^0.8.1"
fastapi = "^0.98.0"
gcsfs = "^2023.4.0"
google-cloud-storage = "^2.5.0"
gunicorn = "^20.1.0"
httpx = "^0.24.1"
itsdangerous = "^2.1.2"
joblib = "^1.3.1"
openai-function-call = "^0.0.5" # Wraps OpenAI functions with Pydantic models.
orjson = "^3.8.10" # Fast JSON serialization: https://fastapi.tiangolo.com/advanced/custom-response/#use-orjsonresponse
pillow = "^9.3.0" # Image processing.
psutil = "^5.9.5"
pyarrow = "^9.0.0"
pydantic = "^1.10.11"
python = "~3.9"
python-dotenv = "^1.0.0"
requests = "^2.28.1"
scikit-learn = "^1.3.0"
tenacity = "^8.2.2"
tqdm = "^4.65.0"
types-psutil = "^5.9.5.12"
typing-extensions = "^4.7.1"
uvicorn = {extras = ["standard"], version = "^0.22.0"}
psutil = "^5.9.5"
pyarrow = "^9.0.0"
pydantic = "^1.10.11"
python = "~3.9"
python-dotenv = "^1.0.0"
requests = "^2.28.1"
scikit-learn = "^1.3.0"
tenacity = "^8.2.2"
tqdm = "^4.65.0"
types-psutil = "^5.9.5.12"
typing-extensions = "^4.7.1"
uvicorn = {extras = ["standard"], version = "^0.22.0"}

### Optional dependencies. ###

Expand Down Expand Up @@ -61,6 +64,7 @@ regex = "^2023.6.3"
# For language detection.
langdetect = {version = "^1.0.9", optional = true}


[tool.poetry.extras]
all = [
"cohere",
Expand Down Expand Up @@ -97,29 +101,29 @@ optional = true

[tool.poetry.group.dev.dependencies]
bokeh = ">=2.4.2,<3" # Required for Dask monitoring.
click = "^8.1.3"
google-api-python-client-stubs = "^1.13.0"
httpx = "^0.24.0"
huggingface-hub = "^0.15.1"
isort = "^5.12.0"
matplotlib = "^3.7.1"
mypy = "^1.0.0"
notebook = "^6.5.4"
pytest = "^7.1.3"
pytest-asyncio = "^0.20.2"
pytest-cov = "^4.0.0"
pytest-mock = "^3.10.0"
ruff = "^0.0.219"
setuptools = "^65.5.0"
toml = "^0.10.2"
types-Pillow = "^9.3.0.4"
types-cachetools = "^5.3.0.5"
types-regex = "^2023.6.3.0"
types-requests = "^2.28.11.5"
types-tqdm = "^4.65.0.0"
watchdog = {extras = ["watchmedo"], version = "^3.0.0"}
wheel = "^0.37.1"
yapf = "^0.32.0"
click = "^8.1.3"
google-api-python-client-stubs = "^1.13.0"
httpx = "^0.24.0"
huggingface-hub = "^0.15.1"
isort = "^5.12.0"
matplotlib = "^3.7.1"
mypy = "^1.0.0"
notebook = "^6.5.4"
pytest = "^7.1.3"
pytest-asyncio = "^0.20.2"
pytest-cov = "^4.0.0"
pytest-mock = "^3.10.0"
ruff = "^0.0.219"
setuptools = "^65.5.0"
toml = "^0.10.2"
types-Pillow = "^9.3.0.4"
types-cachetools = "^5.3.0.5"
types-regex = "^2023.6.3.0"
types-requests = "^2.28.11.5"
types-tqdm = "^4.65.0.0"
watchdog = {extras = ["watchmedo"], version = "^3.0.0"}
wheel = "^0.37.1"
yapf = "^0.32.0"

[tool.poetry.scripts]
deploy-hf = "scripts.deploy_hf:main"
Expand Down
17 changes: 17 additions & 0 deletions src/auth.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
"""Authentication and ACL configuration."""

from typing import Optional

from pydantic import BaseModel

from .config import CONFIG
Expand Down Expand Up @@ -32,6 +34,21 @@ class UserAccess(BaseModel):
concept: ConceptUserAccess


class UserInfo(BaseModel):
"""User information."""
email: str
name: str
given_name: str
family_name: str


class AuthenticationInfo(BaseModel):
"""Authentication information for the user."""
user: Optional[UserInfo]
access: UserAccess
auth_enabled: bool


def get_user_access() -> UserAccess:
"""Get the user access."""
auth_enabled = CONFIG.get('LILAC_AUTH_ENABLED', False)
Expand Down
62 changes: 62 additions & 0 deletions src/router_google_login.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
"""Router for Google OAuth2 login."""

from urllib.parse import urlparse, urlunparse

from authlib.integrations.starlette_client import OAuth, OAuthError
from fastapi import APIRouter, Request, Response
from fastapi.responses import HTMLResponse
from starlette.config import Config
from starlette.responses import RedirectResponse

from .config import CONFIG
from .router_utils import RouteErrorHandler

router = APIRouter(route_class=RouteErrorHandler)

GOOGLE_CLIENT_ID = CONFIG.get('GOOGLE_CLIENT_ID', None)
GOOGLE_CLIENT_SECRET = CONFIG.get('GOOGLE_CLIENT_SECRET', None)
LILAC_AUTH_ENABLED = CONFIG.get('LILAC_AUTH_ENABLED', False)
if LILAC_AUTH_ENABLED:
if GOOGLE_CLIENT_ID is None or GOOGLE_CLIENT_SECRET is None:
raise ValueError(
'Missing `GOOGLE_CLIENT_ID` or `GOOGLE_CLIENT_SECRET` when `LILAC_AUTH_ENABLED=true`')
SECRET_KEY = CONFIG.get('LILAC_OAUTH_SECRET_KEY', None)
if not SECRET_KEY:
raise ValueError('Missing `LILAC_OAUTH_SECRET_KEY` when `LILAC_AUTH_ENABLED=true`')

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


@router.get('/login')
async def login(request: Request, origin_url: str) -> RedirectResponse:
"""Redirects to Google OAuth login page."""
auth_path = urlunparse(urlparse(origin_url)._replace(path='/google/auth'))
return await oauth.google.authorize_redirect(request, auth_path)


@router.get('/auth')
async def auth(request: Request) -> Response:
"""Handles the Google OAuth callback."""
try:
token = await oauth.google.authorize_access_token(request)
except OAuthError as error:
return HTMLResponse(f'<h1>{error}</h1>')
request.session['user'] = token['userinfo']
return RedirectResponse(url='/')


@router.get('/logout')
def logout(request: Request) -> RedirectResponse:
"""Logs the user out."""
request.session.pop('user', None)
return RedirectResponse(url='/')
Loading