Search code examples
pythonoauth-2.0jwtfastapibasic-authentication

FastAPI - Supporting Either Basic Auth Or JWT Auth to Access Endpoint


Referencing this link: fastapi-supporting-multiple-authentication-dependencies

I think this is the closest to what I need, but somehow I can’t get either of the dependency to work because fastapi is enforcing both dependencies before it grants access to endpoint.

Snippet for custom depedency:

def basic_logged_user(credentials: Annotated[HTTPBasicCredentials, Depends(security)]):
    current_username_bytes = credentials.username.encode("utf8")
    correct_username_bytes = settings.SESSION_LOGIN_USER.encode("utf8")
    is_correct_username = secrets.compare_digest(
        current_username_bytes, correct_username_bytes
    )
    current_password_bytes = credentials.password.encode("utf8")
    correct_password_bytes = settings.SESSION_LOGIN_PASS.encode("utf8")
    is_correct_password = secrets.compare_digest(
        current_password_bytes, correct_password_bytes
    )

    if not (is_correct_username and is_correct_password):
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Invalid Credentials",
            headers={"WWW-Authenticate": "Basic"},
        )
    return credentials.username


def jwt_logged_user(token: str = Depends(utils.OAuth2_scheme), 
                    db: Session = Depends(db_session)):
    credential_exception = HTTPException(status_code=status.HTTP_401_UNAUTHORIZED,
                                         detail="Incorrect username or password",
                                         headers={"WWW-Authenticate": "Bearer"})

    token = utils.verify_token(token, credential_exception)
    user = db.query(User).filter(User.username == token.username).first()

    return user


# custom auth
def auth_user(jwt_auth: HTTPBearer = Depends(jwt_logged_user), 
                    basic_auth: HTTPBasic = Depends(basic_logged_user)):
    if not (jwt_auth or basic_auth):
        raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, 
                            detail='Invalid Credentials')


#endpoint
@router.get("/")
async def get_users(db: Session = Depends(db_session), logged_user: str = Depends(auth_user)):
    query_users = db.query(User).all()

    return query_users

I expect it to grant me access to endpoint when I provide correct credential for JWT auth or Basic Auth, but it still forces me to put in credential for both. How can I achieve this effect of providing any one of the 2 auth but not both.


Solution

  • The idea is to make all of this security dependencies not to raise exceptions on user authentication error at the dependency resolving stage.

    For HTTPBasic pass auto_error=False:

    security = HTTPBasic(auto_error=False)
    

    And then in basic_logged_user you should check that

    def basic_logged_user(credentials: Annotated[Optional[HTTPBasicCredentials], Depends(security)]):
        if credentials is None:
            return None
        ...
        # Do not raise exception, but return None instead
    

    You need to find the way how to do the same with your second authentication scheme (utils.OAuth2_scheme) - not to raise HTTP_401_UNAUTHORIZED, but return None instead.

    Then your auth_user will work as you expect. It will raise HTTP_401_UNAUTHORIZED only if both schemes return None.