Replies: 8 comments 2 replies
-
This is a good question, and it isn't clear to me either. The advanced dependencies page in the docs would seem to suggest a pattern where you create a callable class or higher-order-function initialised from a list of scopes with a dependency on Security, and use that to check scope, e.g. class ScopeChecker:
def __init__(self, scopes):
self._scopes = scopes
def __call__(self, request: Request, token: str = Security(oauth2_scheme)):
token = validate_jwt_token(token) # proper validation goes here
if not token_has_required_scopes(token, self._scopes):
raise HTTPException(403, detail='Forbidden')
return token but that either leads to a rather ugly syntax, or you have to create a global dep object for each set of scopes (!): # either
@app.get('/')
async def root(request: Request, user: UserToken = Depends(ScopeChecker(['user']))):
# ...
# or
check_is_user = ScopeChecker(['user'])
@app.get('/')
async def root(request: Request, user: UserToken = Depends(check_is_user)):
# ... Based on my (cursory) reading of class ScopedTo(Depends):
"""Dependency on particular scope.
"""
def __init__(self, *scopes) -> None:
super().__init__(self.__call__)
self._scopes = scopes
def __call__(self, request: Request, token: str = Security(oauth2_scheme)) -> TokenData:
"""Check scopes and return the current user.
"""
token = validate_jwt_token(token) # proper validation goes here
if not token_has_required_scopes(token, self._scopes):
raise HTTPException(403, detail='Forbidden')
return token which can then be used like: @app.get('/')
async def root(request: Request, user: UserToken = ScopedTo('user', 'root:read')):
# ... But I'm not convinced that this is the best solution. I would welcome any clarification on this issue! |
Beta Was this translation helpful? Give feedback.
-
Sorry for the delay guys. You're right, currently, there's no way to access the scopes defined in a I'm thinking about implementing it as an additional parameter that you can declare in the security function, I imagine a class So, you would define your dependency something like: async def get_current_user(scopes: Scopes, token: str = Security(oauth2_scheme)):
for scope in scopes.scopes:
... It has to be a predefined class instead of a plain list to be able to recognize it and solve it while solving the dependencies. But I think that would be the simplest/cleanest way to get the scopes inside your dependencies without breaking or compromising anything else. Would that sound good for you? |
Beta Was this translation helpful? Give feedback.
-
This implementation is almost exactly the same as what we implemented for our application. I'd suggest something like (if possible): async def get_current_user(scopes: Scopes(Security(oauth2_scheme))):
for scope in scopes.scopes:
... This way you don't have an unused parameter |
Beta Was this translation helpful? Give feedback.
-
Here it is! 🎉 🍰 🚀 https://fastapi.tiangolo.com/tutorial/security/oauth2-scopes/ @mattlaue the problem is that you probably still want to read the token, at least at some point, to verify that it's valid. But the new Those dependencies and path operations can declare their own required scopes. And the sub-dependency (that is probably reading the token and verifying it directly) can access all those required scopes from the dependants. So, you can have a central point that checks and verifies all the scopes, and then in different path operations you can have @app.get("/somepath1/")
def some_path_1(current_user: User = Security(get_current_user, scopes=["some", "scopes"])):
...
@app.get("/somepath2/")
def some_path_2(current_user: User = Security(get_current_user, scopes=["something", "different"])):
...
def other_dependency(current_user: User = Security(get_current_user, scopes=["subdependency", "required"])):
...
@app.get("/somepath3/")
def some_path_3(sub_dep = Depends(other_dependency), current_user: User = Security(get_current_user, scopes=["more"])):
... |
Beta Was this translation helpful? Give feedback.
-
I assume this is solved now, so I'll close this issue. But feel free to add more comments or create new issues. |
Beta Was this translation helpful? Give feedback.
-
Is there any mechanism to overwrite security scopes?
let's say, the url3's scopes only require
|
Beta Was this translation helpful? Give feedback.
-
Is there a way to extend the SecurityScopes to support other parameters like The current use-case is creating a custom security that allow me to control based on function checking mechanics. @router.get("/me", response_model=User)
async def get_all_user(
current_user: Annotated[
User,
Security(get_current_user, scopes=["me", "permissions"], permissions=["manage_user"])
]
):
# Return all users
pass Where async def get_current_user(
security_scopes: SecurityScopes,
token: Annotated[str, Depends(oauth2_scheme)],
company_id: Annotated[int, Depends(company_id_auth)],
db: Session = Depends(get_db),
):
...
if security_scopes.scopes:
... # cross check the scopes against the token
if security_scopes.permissions: # Exception! permissions is not part of SecurityScopes
... # cross check the permissions against the token
... # Own validation
user = get_user(db, username=token_data.username)
... # Cross validation
return user Since async def get_all_user(
current_user: Annotated[
User,
Security(get_current_user, scopes=["me", "permissions"], extra={"permissions": ["manage_user"]})
]
): I need to change my approach to utilize scope as permission system in additional of authorizing resources. |
Beta Was this translation helpful? Give feedback.
-
I worked around this problem by creating a class which extends "str", so you can pass through every additional data u want class ScopePredicate(str):
def __init__(self, scopes: list[str]):
self.scopes = scopes
self.any_other_field = ..
def __new__(cls, scopes: list[str]):
value = f" {cls.__name__.lower()} ".join(tuple(sorted(set(scopes))))
member = str.__new__(cls, value)
member._value_ = value
return member
class And(ScopePredicate):
pass
class Or(ScopePredicate):
pass
def get_current_user(
security_scopes: SecurityScopes
) -> UserData:
for scope in security_scopes.scopes:
if isinstance(scope, Or):
# access any other field scope.any_other_field
pass
elif isinstance(scope, And):
pass
return UserData()
Annotated[
UserData,
Security(get_current_user, scopes=[Or(["entitlement-a", "entitlement-b"])]),
] |
Beta Was this translation helpful? Give feedback.
-
How can the scope parameter of the Security object be accessed?
Security accepts scopes as a parameter but the callable doesn't seem to be able to access it. The use case is essentially what is mentioned in the documentation - require certain scopes to be present to access an endpoint, or generate a 403 error.
What if you don't need the security parameter in the callback?
In the above use case, I'd like to require one of a set of scopes to be present but which one isn't really important. Using Security requires that a parameter be added like:
In callable(), test for the scopes and throw an HTTPException as needed. The problem is that arg isn't needed, so its ugly to pass it to the function. A much cleaner implementation would be to use a decorator - similar to the Starlette requires - like:
Is this possible with the FastAPI design? If so, how does the decorate get passed a list of scopes (from the request)?
Beta Was this translation helpful? Give feedback.
All reactions