Problem
I currently have JWT dependency named jwt which makes sure it passes JWT authentication stage before hitting the endpoint like this:
sample_endpoint.py:
from fastapi import APIRouter, Depends, Request
from JWTBearer import JWTBearer
from jwt import jwks
router = APIRouter()
jwt = JWTBearer(jwks)
#router.get("/test_jwt", dependencies=[Depends(jwt)])
async def test_endpoint(request: Request):
return True
Below is the JWT dependency which authenticate users using JWT (source: https://medium.com/datadriveninvestor/jwt-authentication-with-fastapi-and-aws-cognito-1333f7f2729e):
JWTBearer.py
from typing import Dict, Optional, List
from fastapi import HTTPException
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from jose import jwt, jwk, JWTError
from jose.utils import base64url_decode
from pydantic import BaseModel
from starlette.requests import Request
from starlette.status import HTTP_403_FORBIDDEN
JWK = Dict[str, str]
class JWKS(BaseModel):
keys: List[JWK]
class JWTAuthorizationCredentials(BaseModel):
jwt_token: str
header: Dict[str, str]
claims: Dict[str, str]
signature: str
message: str
class JWTBearer(HTTPBearer):
def __init__(self, jwks: JWKS, auto_error: bool = True):
super().__init__(auto_error=auto_error)
self.kid_to_jwk = {jwk["kid"]: jwk for jwk in jwks.keys}
def verify_jwk_token(self, jwt_credentials: JWTAuthorizationCredentials) -> bool:
try:
public_key = self.kid_to_jwk[jwt_credentials.header["kid"]]
except KeyError:
raise HTTPException(
status_code=HTTP_403_FORBIDDEN, detail="JWK public key not found"
)
key = jwk.construct(public_key)
decoded_signature = base64url_decode(jwt_credentials.signature.encode())
return key.verify(jwt_credentials.message.encode(), decoded_signature)
async def __call__(self, request: Request) -> Optional[JWTAuthorizationCredentials]:
credentials: HTTPAuthorizationCredentials = await super().__call__(request)
if credentials:
if not credentials.scheme == "Bearer":
raise HTTPException(
status_code=HTTP_403_FORBIDDEN, detail="Wrong authentication method"
)
jwt_token = credentials.credentials
message, signature = jwt_token.rsplit(".", 1)
try:
jwt_credentials = JWTAuthorizationCredentials(
jwt_token=jwt_token,
header=jwt.get_unverified_header(jwt_token),
claims=jwt.get_unverified_claims(jwt_token),
signature=signature,
message=message,
)
except JWTError:
raise HTTPException(status_code=HTTP_403_FORBIDDEN, detail="JWK invalid")
if not self.verify_jwk_token(jwt_credentials):
raise HTTPException(status_code=HTTP_403_FORBIDDEN, detail="JWK invalid")
return jwt_credentials
jwt.py:
import os
import requests
from dotenv import load_dotenv
from fastapi import Depends, HTTPException
from starlette.status import HTTP_403_FORBIDDEN
from app.JWTBearer import JWKS, JWTBearer, JWTAuthorizationCredentials
load_dotenv() # Automatically load environment variables from a '.env' file.
jwks = JWKS.parse_obj(
requests.get(
f"https://cognito-idp.{os.environ.get('COGNITO_REGION')}.amazonaws.com/"
f"{os.environ.get('COGNITO_POOL_ID')}/.well-known/jwks.json"
).json()
)
jwt = JWTBearer(jwks)
async def get_current_user(
credentials: JWTAuthorizationCredentials = Depends(auth)
) -> str:
try:
return credentials.claims["username"]
except KeyError:
HTTPException(status_code=HTTP_403_FORBIDDEN, detail="Username missing")
api_key_dependency.py (very simplified right now, it will be changed):
from fastapi import Security, FastAPI, HTTPException
from fastapi.security.api_key import APIKeyHeader
from starlette.status import HTTP_403_FORBIDDEN
async def get_api_key(
api_key_header: str = Security(api_key_header)
):
API_KEY = ... getting API KEY logic ...
if api_key_header == API_KEY:
return True
else:
raise HTTPException(
status_code=HTTP_403_FORBIDDEN, detail="Could not validate credentials"
)
Question
Depending on the situation, I would like to first check if it has API Key in the header, and if its present, use that to authenticate. Otherwise, I would like to use jwt dependency for authentication. I want to make sure that if either api-key authentication or jwt authentication passes, the user is authenticated. Would this be possible in FastAPI (i.e. having multiple dependencies and if one of them passes, authentication passed). Thank you!
Sorry, got lost with things to do
The endpoint has a unique dependency, call it check from the file check_auth
ENDPOINT
from fastapi import APIRouter, Depends, Request
from check_auth import check
from JWTBearer import JWTBearer
from jwt import jwks
router = APIRouter()
jwt = JWTBearer(jwks)
#router.get("/test_jwt", dependencies=[Depends(check)])
async def test_endpoint(request: Request):
return True
The function check will depend on two separate dependencies, one for api-key and one for JWT. If both or one of these passes, the authentication passes. Otherwise, we raise exception as shown below.
DEPENDENCY
def key_auth(api_key=Header(None)):
if not api_key:
return None
... verification logic goes here ...
def jwt(authorization=Header(None)):
if not authorization:
return None
... verification logic goes here ...
async def check(key_result=Depends(jwt_auth), jwt_result=Depends(key_auth)):
if not (key_result or jwt_result):
raise Exception
This worked for me (JWT or APIkey Auth). If both or one of the authentication method passes, the authentication passes.
def jwt_auth(auth: HTTPAuthorizationCredentials = Depends(HTTPBearer(auto_error=False))):
if not auth:
return None
## validation logic
return True
def key_auth(apikey_header=Depends(APIKeyHeader(name='X-API-Key', auto_error=False))):
if not apikey_header:
return None
## validation logic
return True
async def jwt_or_key_auth(jwt_result=Depends(jwt_auth), key_result=Depends(key_auth)):
if not (key_result or jwt_result):
raise HTTPException(status_code=401, detail="Not authenticated")
#app.get("/", dependencies=[Depends(jwt_or_key_auth)])
async def root():
return {"message": "Hello World"}
Related
I would like to create a middleware that authorizes every request based on its url, headers and body (OPA). To do that I created a custom request and custom route class. It is the advised way if one wants to manipulate the request body. Firstly, I tried to override the .json() method and do the authorization there, however, not every request handler calls the .json() method, so that is not a possible solution. Then I decided to create a custom method on the request that will do the authorization and call it from a custom middleware. The issue is that the middleware receives a plain request, which does not have the .is_authorized method, rather than the subclassed request.
Minimal example:
import json
from typing import Callable, Any
from fastapi import FastAPI, Request, Response
from fastapi.routing import APIRoute
from starlette.middleware import Middleware
from starlette.middleware.base import BaseHTTPMiddleware
from fastapi.responses import JSONResponse
from pydantic import BaseModel
def some_authz_func(request: Request, body: Any) -> bool:
print(vars(request), body)
return True
class MessageModel(BaseModel):
message: str
class AuthzMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request, call_next):
await request.is_authorized()
response = await call_next(request)
return response
class AuthzRequest(Request):
async def is_authorized(self) -> bool:
try:
json_ = await super().json()
except json.decoder.JSONDecodeError:
json_ = None
return some_authz_func(self, json_)
class AuthzRoute(APIRoute):
def get_route_handler(self) -> Callable:
original_route_handler = super().get_route_handler()
async def custom_route_handler(request: Request) -> Response:
request = AuthzRequest(request.scope, request.receive)
return await original_route_handler(request)
return custom_route_handler
app = FastAPI(route_class=AuthzRoute, middleware=[Middleware(AuthzMiddleware)])
#app.get('/')
async def no_body():
return JSONResponse(content={'no-body': True})
#app.post('/')
async def some_body(body: MessageModel):
print(body)
return JSONResponse(content={'body': True})
Raised exception
File ".virtualenvs/tempenv-416e126962124/lib/python3.8/site-packages/starlette/middleware/errors.py", line 162, in __call__
await self.app(scope, receive, _send)
File ".virtualenvs/tempenv-416e126962124/lib/python3.8/site-packages/starlette/middleware/base.py", line 68, in __call__
response = await self.dispatch_func(request, call_next)
File "main.py", line 23, in dispatch
await request.is_authorized()
AttributeError: 'Request' object has no attribute 'is_authorized'
Dependencies
$ pip freeze
anyio==3.6.1
click==8.1.3
fastapi==0.79.0
h11==0.13.0
idna==3.3
pydantic==1.9.1
sniffio==1.2.0
starlette==0.19.1
typing_extensions==4.3.0
uvicorn==0.18.2
It can be solved by using dependency injection and applying it to the app object (Thanks #MatsLindh). The dependency function can take a Request object and get the ulr, headers and body from it.
from fastapi import FastAPI, Request, Depends
async def some_authz_func(request: Request):
try:
json_ = await request.json()
except json.decoder.JSONDecodeError:
json_ = None
print(vars(request), json_)
app = FastAPI(dependencies=[Depends(some_authz_func)])
Whole working example
import json
from fastapi import FastAPI, Request, Depends
from fastapi.responses import JSONResponse
from pydantic import BaseModel
async def some_authz_func(request: Request):
try:
json_ = await request.json()
except json.decoder.JSONDecodeError:
json_ = None
print(vars(request), json_)
app = FastAPI(dependencies=[Depends(some_authz_func)])
class MessageModel(BaseModel):
message: str
#app.get('/')
async def no_body():
return JSONResponse(content={'no-body': True})
#app.post('/')
async def some_body(body: MessageModel):
print(body)
return JSONResponse(content={'body': True})
Im trying to build a new wrapper to make my google drive api client thread safe and multi threading.
So I have this code, 2 files:
APIConnector.py:
import logging
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Any, Callable, List, Optional
import google.auth
import httplib2
from google_auth_httplib2 import AuthorizedHttp
from googleapiclient.http import HttpRequest
import APIConnector
#dataclass
class APIConnector:
factory: Callable[[], AuthorizedHttp]
pool: List[AuthorizedHttp] = field(default_factory=[])
#classmethod
def new(
cls,
credentials: google.auth.Credentials,
initial_size: int = 5,
timeout_seconds: int = 10,
) -> APIConnector:
factory = lambda: AuthorizedHttp(
credentials,
http=httplib2.Http(timeout=timeout_seconds)
)
pool: List[AuthorizedHttp] = []
for i in range(initial_size):
pool.append(factory())
return cls(factory, pool=pool)
def execute(self, req: HttpRequest) -> Any:
http: Optional[AuthorizedHttp] = None
try:
http = self._provision_http()
return req.execute(http=http)
finally:
if http:
self.pool.append(http)
def _provision_http(self) -> AuthorizedHttp:
# This function can run in parallel in multiple threads.
try:
return self.pool.pop()
except IndexError:
logging.info("Pool exhausted. Creating new transport")
return self.factory()
and GSuiteUserManager.py
from __future__ import annotations
from dataclasses import dataclass
import googleapiclient
from googleapiclient.discovery import build
from google.auth.credentials import Credentials
import GSuiteUserManager
from ngus_crawlers.googledrive.APIConnector import APIConnector
#dataclass
class GSuiteUserManager:
api: APIConnector
users: googleapiclient.discovery.Resource
domain: str
#classmethod
def new(cls, domain, credentials) -> GSuiteUserManager:
api = APIConnector.new(Credentials)
service = googleapiclient.discovery.build(
"admin",
"directory_v1",
credentials=credentials,
cache_discovery=False,
)
users = service.users()
return cls(api=api, users=users, domain=domain)
def list(self) -> dict:
return self.api.execute(
self.users.list(domain=self.domain)
)
def get(self, email: str) -> dict:
pass
my question is so,
how do I make this function to use the new code:
def connect_logic(config: dict[str]) -> googleapiclient.discovery.Resource:
logging.info("Connecting to google drive...")
credentials: Credentials = Credentials.from_authorized_user_info(
info=config[GD_CONFIG][TOKEN_CONFIG], scopes=config[GD_CONFIG][SCOPES])
service: googleapiclient.discovery.Resource = build("drive", "v3", credentials=credentials)
logging.info("Successfully logged into google drive")
return service
or even, how do I make line like this to use the new code:
raw_data = service.files().get(fileId=file_id, fields='*').execute()
Huge thanks to everyone who helps!
I make FastAPI application I face to structural problem.
For example, let's say there is exist this simple application
from fastapi import FastAPI, Header
from fastapi.responses import JSONResponse
from fastapi.testclient import TestClient
from pydantic import BaseModel, Field
app = FastAPI()
class PydanticSchema(BaseModel):
test_int: int = Field(..., ge=0)
class CustomException(Exception):
def __init__(self, status_code, msg):
self.status_code = status_code
self.msg = msg
#app.exception_handler(CustomException)
async def handle_custom_exception(request, exc: CustomException):
return JSONResponse(
status_code=exc.status_code, content={"error": exc.msg}
)
#app.post("/")
def index(
some_form: PydanticSchema,
some_header: str = Header("", alias="X-Header", max_length=3),
):
is_special_case = some_form.test_int == 42
if is_special_case:
raise CustomException(418, "Very special case")
return "ok"
And here is the problem.
I have THREE independent producers of errors: My manual raise, Pydantic Model, FastApi.
if __name__ == "__main__":
client = TestClient(app)
cases = (
({"X-Header": "abc"}, {"test_int": 0}),
({"X-Header": "abcdef"}, {"test_int": 0}),
({"X-Header": "abc"}, {"test_int": -1}),
({"X-Header": "abc"}, {"test_int": 42}),
)
for case in cases:
r = client.post("/", headers=case[0], json=case[1])
print(f"#####\n{case=}:\n{r.status_code=}\n{r.text=}")
My goal is to have consistent error responses
So I have to add exception handler for my CustomException,
I have to add error_msg_templates to PydanticModel.Config
I have to add incredibly stupid handler for FastApi exception and Pydantic exception to reformat them into my custom response. Becasuse fastapi overrides custom messages from pydantic templates.
Like so:
# ---snip---
from fastapi.exceptions import RequestValidationError
# ---snip---
class PydanticSchema(BaseModel):
test_int: int = Field(..., ge=0)
class Config:
error_msg_templates = {"value_error.number.not_ge": "Custom GE error"}
# ---snip---
#app.exception_handler(RequestValidationError)
async def fastapi_error_handler(request, exc: RequestValidationError):
errors = exc.errors()
error_wrapper = exc.raw_errors[0]
validation_error = error_wrapper.exc
from pydantic import error_wrappers as ew
if isinstance(validation_error, ew.ValidationError):
errors = validation_error.errors()
first_error = errors[0]
msg = first_error.get("msg")
# [!] Demonstration
etype = first_error.get("type")
if etype == "value_error.any_str.max_length":
msg = "Custom MAX length error"
return JSONResponse(status_code=400, content={"error": msg})
# ---snip---
It is unmaintainable! But custom error message is required - I can not rely ond default pydantic\fastapi error response schemas.
Does anyone one faced to this problem and have solved it gracefully?
Why not FastAPI's Exception_handler argument? You can view the status code of the response when this error occurs, then add an exception handler. Exception handlers have access to the Request & Exception objects, which are the request that caused the exception, and the exception raised, respectively.
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
from fastapi.exceptions import RequestValidationError
def val_err(request: Request, exception: Exception):
if not isinstance(exception, RequestValidationError):
return
errors = exc.errors()
error_wrapper = exc.raw_errors[0]
validation_error = error_wrapper.exc
from pydantic import error_wrappers as ew
if isinstance(validation_error, ew.ValidationError):
errors = validation_error.errors()
first_error = errors[0]
msg = first_error.get("msg")
# [!] Demonstration
etype = first_error.get("type")
if etype == "value_error.any_str.max_length":
msg = "Custom MAX length error"
return JSONResponse(status_code=400, content={"error": msg})
exception_handlers = {422: val_err}
app = FastAPI(exception_handlers=exception_handlers)
You can refer to my other answer which dissects the code and explains it in a bit more detail.
I'd like to handle errors using AioHttp and Connexion in my python web apis in the same way Flask does through #app.errorhandler(Exception)
In another words, let's say my services raises SomethingAlreadyExists and I want to return 409 Conflict, rather than add the code below in all my apis:
try:
myservice.create_something(..)
except SomethingAlreadyExists as error: # Repeated code -> DRY
return json_response({"message": str(error)}, status=409)
I'd like to just call the myservice.create_something(..) in the API layer and the error handle would return the 409 for SomethingAlreadyExists exceptions or 404 for SomethingNotFound.
Note:
In Flask land it would be something like below:
import connexion
def create_api_app(version='api'):
connexion_app = connexion.FlaskApp(__name__, specification_dir='../api/')
connexion_app.add_api('openapi.yaml', validate_responses=True)
app = connexion_app.app
# It intercepts the specific exception and returns the respective status_code
#app.errorhandler(InvalidValueException)
def bad_request_handler(error):
return 'Bad Request: {}'.format(str(error)), 400
#app.errorhandler(NotFoundException)
def not_found_handler(error):
return 'Not found: {}'.format(str(error)), 404
#app.errorhandler(AlreadyExistsException)
def conflict_handler(error):
return 'Conflict: {}'.format(str(error)), 409
# my_service.py
def get_model(i):
model = get_model_or_none(id)
if btask is None:
raise NotFoundException(f"Model id:{id} not found.")
...
# api.py
def get_model(id):
model = my_service.get_model(id)
# handle errors not required ;)
return btask.to_dict()
I'd like to do the same in my AioHttp connexion app:
from connexion import AioHttpApp
def create_app():
connexion_app = AioHttpApp(__name__, port=8000, specification_dir="../", only_one_api=True)
connexion_app.add_api("openapi.yaml", pass_context_arg_name="request")
# Do something here.. like
# web.Application(middlewares=[handle_already_exists_errors]) --> doesn't work
# OR
# connex_app.add_error_handler(
# AlreadyExistsException, handle_already_exists_errors) --> doesn't work too
return connexion_app
Cheers and I'll appreciate any help!
Roger
I was digging into the connexion and aiohttp code and figured out a way to do it using middlewares:
import json
from aiohttp.web import middleware
from connexion.lifecycle import ConnexionResponse
from connexion import AioHttpApp
from .exceptions import NotFoundException, AlreadyExistsException
def create_app():
connexion_app = AioHttpApp(
__name__, port=8000, specification_dir="../", only_one_api=True
)
connexion_app.add_api("openapi.yaml", pass_context_arg_name="request")
connexion_app.app.middlewares.append(errors_handler_middleware)
return connexion_app
#middleware
async def errors_handler_middleware(request, handler):
""" Handle standard errors returning response following the connexion style messages"""
try:
response = await handler(request)
return response
except NotFoundException as error:
return json_error(message=str(error), title='Not Found', status_code=404)
except AlreadyExistsException as error:
return json_error(message=str(error), title='Conflict', status_code=409)
def json_error(message, title, status_code):
return ConnexionResponse(
body=json.dumps({
'title': title,
'detail': message,
'status': status_code,
}).encode('utf-8'),
status_code=status_code,
content_type='application/json'
)
This is driving me absolutely crazy and preventing me from being able to do local dev/test.
I have a flask app that uses authlib (client capabilities only). When a user hits my home page, my flask backend redirects them to /login which in turn redirects to Google Auth. Google Auth then posts them back to my app's /auth endpoint.
For months, I have been experiencing ad-hoc issues with authlib.integrations.base_client.errors.MismatchingStateError: mismatching_state: CSRF Warning! State not equal in request and response. It feels like a cookie problem and most of the time, I just open a new browser window or incognito or try to clear cache and eventually, it sort of works.
However, I am now running the exact same application inside of a docker container and at one stage this was working. I have no idea what I have changed but whenever I browse to localhost/ or 127.0.0.1/ and go through the auth process (clearing cookies each time to ensure i'm not auto-logged in), I am constantly redirected back to localhost/auth?state=blah blah blah and I experience this issue:
authlib.integrations.base_client.errors.MismatchingStateError: mismatching_state: CSRF Warning! State not equal in request and response.
I think the relevant part of my code is:
#app.route("/", defaults={"path": ""})
#app.route("/<path:path>")
def catch_all(path: str) -> Union[flask.Response, werkzeug.Response]:
if flask.session.get("user"):
return app.send_static_file("index.html")
return flask.redirect("/login")
#app.route("/auth")
def auth() -> Union[Tuple[str, int], werkzeug.Response]:
token = oauth.google.authorize_access_token()
user = oauth.google.parse_id_token(token)
flask.session["user"] = user
return flask.redirect("/")
#app.route("/login")
def login() -> werkzeug.Response:
return oauth.google.authorize_redirect(flask.url_for("auth", _external=True))
I would hugely appreciate any help.
When I run locally, I start with:
export FLASK_APP=foo && flask run
When I run inside docker container, i start with:
.venv/bin/gunicorn -b :8080 --workers 16 foo
Issue was that SECRET_KEY was being populated using os.random which yielded different values for different workers and thus, couldn't access the session cookie.
#adamcunnington here is how you can debug it:
#app.route("/auth")
def auth() -> Union[Tuple[str, int], werkzeug.Response]:
# Check these two values
print(flask.request.args.get('state'), flask.session.get('_google_authlib_state_'))
token = oauth.google.authorize_access_token()
user = oauth.google.parse_id_token(token)
flask.session["user"] = user
return flask.redirect("/")
Check the values in request.args and session to see what's going on.
Maybe it is because Flask session not persistent across requests in Flask app with Gunicorn on Heroku
How I Fix My Issue
install old version of authlib it work fine with fastapi and flask
Authlib==0.14.3
For Fastapi
uvicorn==0.11.8
starlette==0.13.6
Authlib==0.14.3
fastapi==0.61.1
Imporantt if using local host for Google auth make sure get https certifcate
install chocolatey and setup https check this tutorial
https://dev.to/rajshirolkar/fastapi-over-https-for-development-on-windows-2p7d
ssl_keyfile="./localhost+2-key.pem" ,
ssl_certfile= "./localhost+2.pem"
--- My Code ---
from typing import Optional
from fastapi import FastAPI, Depends, HTTPException
from fastapi.openapi.docs import get_swagger_ui_html
from fastapi.openapi.utils import get_openapi
from starlette.config import Config
from starlette.requests import Request
from starlette.middleware.sessions import SessionMiddleware
from starlette.responses import HTMLResponse, JSONResponse, RedirectResponse
from authlib.integrations.starlette_client import OAuth
# Initialize FastAPI
app = FastAPI(docs_url=None, redoc_url=None)
app.add_middleware(SessionMiddleware, secret_key='!secret')
#app.get('/')
async def home(request: Request):
# Try to get the user
user = request.session.get('user')
if user is not None:
email = user['email']
html = (
f'<pre>Email: {email}</pre><br>'
'documentation<br>'
'logout'
)
return HTMLResponse(html)
# Show the login link
return HTMLResponse('login')
# --- Google OAuth ---
# Initialize our OAuth instance from the client ID and client secret specified in our .env file
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('/login', tags=['authentication']) # Tag it as "authentication" for our docs
async def login(request: Request):
# Redirect Google OAuth back to our application
redirect_uri = request.url_for('auth')
print(redirect_uri)
return await oauth.google.authorize_redirect(request, redirect_uri)
#app.route('/auth/google')
async def auth(request: Request):
# Perform Google OAuth
token = await oauth.google.authorize_access_token(request)
user = await oauth.google.parse_id_token(request, token)
# Save the user
request.session['user'] = dict(user)
return RedirectResponse(url='/')
#app.get('/logout', tags=['authentication']) # Tag it as "authentication" for our docs
async def logout(request: Request):
# Remove the user
request.session.pop('user', None)
return RedirectResponse(url='/')
# --- Dependencies ---
# Try to get the logged in user
async def get_user(request: Request) -> Optional[dict]:
user = request.session.get('user')
if user is not None:
return user
else:
raise HTTPException(status_code=403, detail='Could not validate credentials.')
return None
# --- Documentation ---
#app.route('/openapi.json')
async def get_open_api_endpoint(request: Request, user: Optional[dict] = Depends(get_user)): # This dependency protects our endpoint!
response = JSONResponse(get_openapi(title='FastAPI', version=1, routes=app.routes))
return response
#app.get('/docs', tags=['documentation']) # Tag it as "documentation" for our docs
async def get_documentation(request: Request, user: Optional[dict] = Depends(get_user)): # This dependency protects our endpoint!
response = get_swagger_ui_html(openapi_url='/openapi.json', title='Documentation')
return response
if __name__ == '__main__':
import uvicorn
uvicorn.run(app, port=8000,
log_level='debug',
ssl_keyfile="./localhost+2-key.pem" ,
ssl_certfile= "./localhost+2.pem"
)
.env file
GOOGLE_CLIENT_ID=""
GOOGLE_CLIENT_SECRET=""
Google Console Setup