MushroomMaula / fastapi_login

FastAPI-Login tries to provide similar functionality as Flask-Login does.
https://pypi.org/project/fastapi-login
MIT License
639 stars 58 forks source link

Allow blacklist jwt #102

Closed unaor closed 8 months ago

unaor commented 1 year ago

I want to allow user to login and use the application from one device only. I noticed that the current implementation of the user_loaderonly receives one param the decoded username, making it impossible to access the actual token and compare it

i'm suggesting the following change to give access to the token

line 229: user = await self._load_user(user_identifier, token)

line 236: async def _load_user(self, identifier: typing.Any ,token: str):

if inspect.iscoroutinefunction(self._user_callback):
            user = await self._user_callback(identifier. token)
        else:
            user = self._user_callback(identifier, token)

        return user

now my custom implementation looks like this

@manager.user_loader(db=get_db())
def query_user(username: str, token:str,  db: SessionLocal):
    if blacklisted_jwt[token]:
        return None
    if isinstance(db, types.GeneratorType):
        db = next(get_db())
    user: UserModel = db.query(UserModel).filter(UserModel.username==username).first()
    return user
MushroomMaula commented 1 year ago

I am not quite sure, I understand the issue. You want to allow access to exactly one user? In that case, couldn't you also blacklist based on the username? I.e.:

@manager.user_loader(db=get_db())
def query_user(username: str, token:str,  db: SessionLocal):
    if username != "myusername":
        return None
    if isinstance(db, types.GeneratorType):
        db = next(get_db())
    user: UserModel = db.query(UserModel).filter(UserModel.username==username).first()
    return user
unaor commented 1 year ago

I want to allow access to many users of course, but I would like to prevent the same user to login from his phone for example, and from his computer on the same time. for example

token_a =login("some_random_user", "secret_password") now token_a lives on the device of the some_random_user

however if the same user would go to another device and do token_b =login("some_random_user", "secret_password") than i would like to blacklist token_aand thus basically force log him out from the first device, but allow him to use on the second device with token_b

MushroomMaula commented 1 year ago

I see. I am afraid that is not currently possible and I would rather not change the call pattern of the user loader in order to keep backwards compatibility. You could in theory write your own dependency / subclass the login manager to implement this behavior there:

def BlacklistManager(request: Request):
     token = manager._get_token(request)  # Careful might raise errors if no token is present
    if token in blacklist:
        # Raise not authenticated
    else:
        # Return user
        return manager(request)

I agree however that this depends on the "private" _get_token function, which should probably be public anyway. I probably should change the API in that way.

MushroomMaula commented 1 year ago

See also #87 and #82.

unaor commented 1 year ago

thanks that is quite good workaround.

i'm using it like this wdyt

async def blacklistManager(request: Request): <- need it async for the token to resolve
    token = await manager._get_token(request)
    if token in blacklist:
        raise InvalidCredentialsException
    return manager(request)

and example usage

@router.get("/", response_model=list[UserSchema])
async def read_users(skip: int = 0, limit: int = 100, db: Session = Depends(get_db), requesting_user: UserModel=Depends(blacklistManager)):
    requesting_user = await requesting_user <- only way to make it actually authenticate
    users = db.query(UserModel).offset(skip).limit(limit).all()
    return users