FastAPI ResourceProtector integration
Is your feature request related to a problem? Please describe.
Authlib already supports ResourceProtector integrations with Flask and Django, as documented here and here respectively, as annotations on their frameworks' routes.
FastAPI has a structure for authentication and authorization utilizing its dependencies. In its docs, it even uses them as part of its example implementation of OAuth, such as seen here.
It would be nice if authlib supported a similar structure for FastAPI routes as it currently does for Flask and Django. In this case, rather than being an annotation, the ResourceProtector should return a callable that can be used as a FastAPI dependency.
Describe the solution you'd like
I've implemented and am currently using a fairly straightforward implementation for FastAPI, based off the existing ones for Flask and Django. Rather than making the ResourceProtector a callable itself, it acts as a dependency factory to reuse validators while making the dependency's optional and claims parameters configurable. This allows following FastAPI's parameterized dependency structure.
class ResourceProtector(_ResourceProtector):
def acquire_token(self, request, scopes=None, **kwargs):
kwargs['scopes'] = scopes
return self.validate_request(request=request, **kwargs)
@asynccontextmanager
async def acquire(self, request: Request, **kwargs):
try:
yield self.acquire_token(request=request, **kwargs)
except OAuth2Error as error:
raise return_error_response(error)
def dependency(self, optional=False, **kwargs):
def wrapper(request: Request):
try:
token = self.acquire_token(request, **kwargs)
return token
except MissingAuthorizationError as error:
if optional:
return None
raise return_error_response(error)
except OAuth2Error as error:
raise return_error_response(error)
return wrapper
def return_error_response(error: OAuth2Error):
status = error.status_code
body = dict(error.get_body())
headers = dict(error.get_headers())
return HTTPException(status_code=status, detail=body, headers=headers)
The ResourceProtector can be used in a similar fashion to the existing implementations, but passing it as a callable FastAPI dependency rather than as an annotation. E.g.:
require_oauth = ResourceProtector()
# Some validator that implements get_jwks as documented by authlib
validator = JWTBearerTokenValidator()
require_oauth.register_token_validator(validator)
# Create a parameterized dependency
oauth_dependency = require_oauth.dependency(optional=True)
@app.get('/token')
def get_token(token: Annotated[str, Depends(oauth_dependency)]):
return {'token': token}
It also supports the context manager option as described in authlib's docs, without the dependency factory:
@app.get('/token')
def get_token(request: Request):
with token as require_oauth.acquire(request):
return {'token': token}
Describe alternatives you've considered
At the moment, this implementation is built into an application codebase rather than as part of authlib's provided integrations.
Additional context
I'm happy to do a pull request for this if this is wanted. I was initially concerned about how to test resource protectors but it doesn't seem there are existing test cases for the Flask or Django implementations either, so I presume that a test case for this is not required?
If there's interest in this, I've also got something along similar lines here (docs) that uses joserfc internally. Re: testing, there's integration and unit tests you could crib from if you wanted
What I have is a little bit geared toward our own use cases at SKAO, but could be factored out and made generic.
I haven't a chance to look at it in too much depth but the use of FastAPI's Security (wrapped as Requires) might be more correct of a solution for FastAPI if not for its limitations on exclusively scope-based claims. Implementing a wrapper out-of-tree seems more than what authlib should be responsible for, but I'll leave that for the maintainers to decide.
I am interested in the unit tests and will give it a look once I have some more time to.
I think applying FastAPI's native Security features is desirable because it gives you some nice annotations in the generated OpenAPI spec and enables the "authenticate" button if using interactive SwaggerUI.
I'm not sure what you mean by "exclusively scope-based claims" FastAPI has special first-party support for declaring oauth2 scopes as part of a security requirement, but you can create your own subclass and add whatever else you want. (e.g. we're adding roles for role-based access control)
It's tricky to decide what (if anything) a library like authlib should provide because the authorization rules inevitably end up tangled up with the business rules of a specific app. If I read you right, I think you're suggesting something more like a base dependency that would validate the token but then return it to the app (or another wrapping Depends()) to apply specific authorization checks.
I'm not sure what you mean by "exclusively scope-based claims" FastAPI has special first-party support for declaring oauth2 scopes as part of a security requirement, but you can create your own subclass and add whatever else you want. (e.g. we're adding roles for role-based access control)
That's what I meant, the Security class only supports scope-based claims and doesn't support other claims such as roles. The more I think about it, the more it's probably fine to just create an authlib implementation of Security that adds support for other JWT claims.
It's tricky to decide what (if anything) a library like authlib should provide because the authorization rules inevitably end up tangled up with the business rules of a specific app. If I read you right, I think you're suggesting something more like a base dependency that would validate the token but then return it to the app (or another wrapping Depends()) to apply specific authorization checks.
I think either are fine. My current implementation just utilizes existing structure with ResourceProtectors and Validators to both validate and authorize a token but also return the token (in a similar way to the existing Flask and Django implementations of ResourceProtector).