zamzterz/Flask-pyoidc

Provide a way to check for access token, but not require it

Opened this issue · 3 comments

santtu commented

I have a case where an API can be accessed both with or without authentication, i.e., reduced access and capability without authentication, but with full features with authentication. This seems to be tricky to get done with the current token_auth though. It will check that

  1. Authorization header exists, if not, 401
  2. The introspection succeeds, if not, 403

I looked at #161 but it doesn't seem to address this. What I'd like is a way to programmatically check if token is supplied and it is valid or not. The introspect_token looked promising, but it will balk on a missing Authorization header.

I settled for the time being to wrap the token_auth decorator so I can intercept 401/403 if they occur and then bypass to the actual view function, but this is quite a kludge.

Since #161 is deprecating introspect_token, it doesn't seem to be a good choice anyway. Would it be possible to expose functionalities of the decorators directly or have a required=False keyword option for them?

The reason I intend to deprecate introspect_token as a public method is because JWT can be verified in memory and using token introspection is just an unnecessary I/O for your web service unless you are using opaque tokens. Using token introspection for JWT nullifies the benefit of "no backend call is required". That method misleads the user into using it for verification of JWT which although is convenient but is not required.

As I understand, you want to allow everyone to access the API but say your premium users will get to use the access token to access the complete functionality of your API while freemium (unauthenticated) users without access token will get reduced capabilities like having API rate limits. In my opinion, a more appropriate way is to issue access token to everyone but use scopes to limit their capabilities. E.g. Check if the access token has a scope for your premium users. Is there a reason why you can't issue access token to everyone? Is your API public for anyone on the internet to use?

token_auth is more about authorizing the caller coming from browserless user-agents. It's not meant to accommodate public users because managing access of public users does not seem to be in scope of this extension which is strictly about OIDC and to its base Oauth2 as well. Your use case although it's practical falls more towards managing user access with and without JWT and I am sure flask-jwt-extended can help you better with this.

If you don't have a problem with using private methods, you can do this after #161:

from oic.oic import AccessTokenResponse

auth = OIDCAuthentication({'default': provider_config})
auth.init_app(app)

client = auth.clients['default']

@app.get('/')
def index():
    if access_token := auth._parse_authorization_header():
        if client._validate_token_response(token=AccessTokenResponse().from_jwt(
            access_token, keyjar=client._client.keyjar), scopes=['premium'], audience=False):
            ...  # full functionality access
            return ...
    ....  # free access
    return ...

@zamzterz #161 also fixes #158 , can you merge it asap.

santtu commented

I need to have the same API available for both unauthenticated and authenticated (= authorized) users. I've considered providing the two versions from different endpoints (/api/anon/ vs. /api/auth/), requiring authentication for all and providing the minimal anonymous information via other API (but that'd require specialcasing in application)... In the end, I settled on having an API answering for "am I authenticated or not?" and allow the client to decide then to fetch OIDC token if it wants to proceed with higher privileges.

I'll take a look at flask-jwt-extended, but in the end, what I really want to know is "given this OIDC setup, is this token valid?" simple question that flask-pyoidc already answers. It just doesn't let me really query the token status easily, just to enforce its validity. I also considered using the internal interfaces, but instead used a decorator-decorating-another-decorator kludge as it appears to be more future-proof (using the public interface).

I wonder if around https://github.com/zamzterz/Flask-pyoidc/blob/main/src/flask_pyoidc/flask_pyoidc.py#L449 a g.current_token_identity = None default and then if required=False is passed to the decorator, fall through to the view function? This would allow the view function to check g.current_token_identity (and of course UserSession validity if oidc_auth is used) and decide what to do based on that.

Reflecting on flask-login, it has login_required decorator, but the use of that decorator isn't necessary to access the user information (current_user is valid even if no login decorator is used), so it offers the option for view functions to behave differently for authenticated and unauthenticated users. I'd like the same kind of flexibility for token_auth (and oidc_auth potentially as well).

I have it working now (not in a pretty way, but it works), just thought it might be something others would need too.

flask-login is about user management so it makes sense to have current_user for both authenticated and unauthenticated users. flask-pyoidc is about identity management and access is managed through access token and ID token claims which is why I doubt circumventing token_auth & oidc_auth by using a flag variable required can be considered in scope of this extension. In SSO, using scopes and roles is the most appropriate way to manage access privileges and because it's already a recommended approach, we cannot promote custom access management design that is not offered by OIDC & Oauth2.