dorinclisu / fastapi-auth0

FastAPI authentication and authorization using auth0.com
MIT License
230 stars 37 forks source link

Optional authentication #10

Closed samedii closed 3 years ago

samedii commented 3 years ago

Is this already possible? Is it bad practice? If not, how difficult do you think it would it be to implement?

If you can point me in the right direction then I can try to create a PR

E.g. something along the lines of

@router.get("/secure", dependencies=[Depends(auth.implicit_scheme)])
def get_secure(
    user: Optional[Auth0User] = None
):
    return {"message": f"{user}"}
zekiblue commented 3 years ago

Optional authentication is against common practices. Why exactly you need something like this?

samedii commented 3 years ago

I see. I have a rest api where I want to return a list of public objects A and if the user is authorized I would have liked to show any private objects A belonging to that user in that list too. I guess I have to duplicate a lot of endpoints instead then or do you know of a better solution?

zekiblue commented 3 years ago

public and secure endpoints needs to be seperated. You can create handler to deal with endpoint. If auth user is supplied to handler it will return puplic + owned objects. If auth user is not provided it will only return public.

samedii commented 3 years ago

Okay, thanks :+1:

dorinclisu commented 3 years ago

It is already supported, although you have to be extra careful.

By default auto_error = True so you will set it to false. In case of a failed authentication / authorization, auth.get_user returns None instead of raising Auth0UnauthenticatedException that triggers the typical 401 response.

auth = Auth0(...)
dangerous_auth = Auth0(..., auto_error=False)

@router.get("/secure", dependencies=[Depends(auth.implicit_scheme)])
def get_secure(
    user: Optional[Auth0User] = Security(dangerous_auth.get_user)
):
    if user is None:
       return {"message": "public user"}

    return {"message": f"{user}"}

Note that you can have multiple Auth0 objects in the same app, so if you have some endpoints that always need authentication (no public mixup), I recommend using the regular auth and leave dangerous_auth only for those public endpoints.

samedii commented 3 years ago

Are you sure that it works? I tried to do the same but I get 403 Error: Forbidden

{
  "detail": "Not authenticated"
}

This is my code:

import os
from fastapi import APIRouter, Depends, Security
from fastapi_auth0 import Auth0, Auth0User
from typing import Optional

auth = Auth0(
    domain=os.environ["AUTH0_DOMAIN"],
    api_audience=os.environ["AUTH0_API_AUDIENCE"],
)
guest_auth = Auth0(
    domain=os.environ["AUTH0_DOMAIN"],
    api_audience=os.environ["AUTH0_API_AUDIENCE"],
    auto_error=False,
)

router = APIRouter()

@router.get("/secure", dependencies=[Depends(auth.implicit_scheme)])
def get_secure(
    user: Optional[Auth0User] = Security(guest_auth.get_user)
):
    if user is None:
       return {"message": "public user"}

    return {"message": f"{user}"}

And I try to do this:

curl -X 'GET' \
  'http://localhost:8000/secure' \
  -H 'accept: application/json'

I also tried switching out Depends(auth.implicit_scheme) for Depends(guest_auth.implicit_scheme)

Note that my API is a "third party" application in case that matters

samedii commented 3 years ago

The issue seems to arise in get_user. The auto_error is not passed through. Example where the default is auto_error=True

    async def get_user(self,
        security_scopes: SecurityScopes,
        creds: HTTPAuthorizationCredentials = Depends(Auth0HTTPBearer(auto_error=False))
    ) -> Optional[Auth0User]:

        if creds is None:
            return None

I don't see any pretty solution for this but I'm not used to the "Depends" pattern. Do you see a nice solution to setting auto_error dynamically in the default value?

dorinclisu commented 3 years ago

Released fix as 0.2.1