fastapi icon indicating copy to clipboard operation
fastapi copied to clipboard

Add JWT token mechanism with fastapi authlib-oauth implementation for external SSO

Open tapajyotideb opened this issue 1 year ago • 0 comments

First Check

  • [X] I added a very descriptive title to this issue.
  • [X] I used the GitHub search to find a similar issue and didn't find it.
  • [X] I searched the FastAPI documentation, with the integrated search.
  • [X] I already searched in Google "How to X in FastAPI" and didn't find any information.
  • [X] I already read and followed all the tutorial in the docs and didn't find an answer.
  • [X] I already checked if it is not related to FastAPI but to Pydantic.
  • [X] I already checked if it is not related to FastAPI but to Swagger UI.
  • [X] I already checked if it is not related to FastAPI but to ReDoc.

Commit to Help

  • [X] I commit to help with one of those options 👆

Example Code

from authlib.integrations.starlette_client import OAuth
oauth = OAuth()

CONF_URL = "https://localhost:8080/.well-known/openid-configuration"
oauth.register(
    name="cad",
    server_metadata_url=CONF_URL,
    client_id=settings.CLIENT_ID,
    client_secret=settings.CLIENT_SECRET,
    client_kwargs={"scope": "openid email profile authorization_group"},
)
oauth2_scheme = OAuth2PasswordBearer(tokenUrl='/auth')

def create_access_token(*, data: dict, expires_delta: datetime.timedelta = None):
    to_encode = data.copy()
    if expires_delta:
        expire = datetime.datetime.utcnow() + expires_delta
    else:
        expire = datetime.datetime.utcnow() + datetime.timedelta(minutes=15)
    to_encode.update({'exp': expire})
    encoded_jwt = jwt.encode(to_encode, "abcd", algorithm="HS256")
    return encoded_jwt


def create_token(id):
    access_token_expires = datetime.timedelta(minutes=120)
    access_token = create_access_token(data={'sub': id}, expires_delta=access_token_expires)
    return access_token


@app.middleware("http")
async def authorize(request: Request, call_next):
    if not (request.scope["path"].startswith("/login") or request.scope["path"].startswith("/auth")):
        if not is_session_okay(request.session):
            return RedirectResponse(url="/login")
    return await call_next(request)


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


@app.get("/auth")
async def auth(request: Request):
    try:
        token = await oauth.cad.authorize_access_token(request)
    except OAuthError as error:
        return HTMLResponse(f"<h1>{error.error}</h1>")
    user = await oauth.cad.parse_id_token(request, token)
    access_token = create_token(user['sub'])
    return {"access_token": access_token, "token_type": "bearer"}


async def get_current_user(token: str = Depends(oauth2_scheme)):
    cred_exception = HTTPException(
        status_code=status.HTTP_401_UNAUTHORIZED,
        detail='Could not validate credentials',
        headers={'WWW-Authenticate': 'Bearer'},
    )
    try:
        payload = jwt.decode(token, os.getenv("CLIENT_SECRET"), algorithms=["HS256"])
        id: str = payload.get('sub')
        if id is None:
            raise cred_exception
    except jwt.JWTError:
        raise cred_exception
    return id


@app.get("/", tags=["Web-UI"])
def index():
    frontend_root = "./ui"
    return FileResponse(str(frontend_root) + "/index.html", media_type="text/html")

Description

I have this main.py file where I am configuring authentication with an external-sso or local-oidc-server. I am creating a jwt token here at /auth endpoint. After the token is generated, now I am unable to redirect it to base path("/"). How can I achieve that. If I try to access the / path, i get redirected to auth endpoint with the bearer token displayed.

In the "auth" endpoint after the access token is created, I even tried to do RedirectResponse to "/" like this

response = RedirectResponse(url='/', status_code=status.HTTP_302_FOUND)
response.set_cookie(key='access_token', value=access_token)
return response

but i get into a infinite loop where after sso authentication is successful it again does the same thing.

Console log:

INFO:     127.0.0.1:61886 - "GET /auth?code=fkZa_Z9LrA6paUeHXS02HH_hLCU-y3TVAvQBBADJ&state=2Yi3OwaTweWmCuQtkEeuAEduvxAEET HTTP/1.1" 302 Found
INFO:     127.0.0.1:61886 - "GET / HTTP/1.1" 307 Temporary Redirect
INFO:     127.0.0.1:61886 - "GET /login HTTP/1.1" 302 Found

INFO:     127.0.0.1:61886 - "GET /auth?code=fkZa_Z9LrA6paUeHXS02HH_hLCU-y3TVAvQBBADJ&state=2Yi3OwaTweWmCuQtkEeuAEduvxAEET HTTP/1.1" 302 Found
INFO:     127.0.0.1:61886 - "GET / HTTP/1.1" 307 Temporary Redirect
INFO:     127.0.0.1:61886 - "GET /login HTTP/1.1" 302 Found

...........

Operating System

Windows

Operating System Details

No response

FastAPI Version

0.63.0

Python Version

Python 3.7.11

Additional Context

No response

tapajyotideb avatar Aug 01 '22 10:08 tapajyotideb