-
Notifications
You must be signed in to change notification settings - Fork 37
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Support multiple logout views (multiple use of 'oidc_logout' decorator). #126
Conversation
281b4c3
to
c821ab4
Compare
c821ab4
to
f9f6d7d
Compare
I left a review highlighting the problem. |
@infohash I don't see any review? Was it not published because this PR was still in draft stage? |
src/flask_pyoidc/flask_pyoidc.py
Outdated
@@ -269,8 +249,9 @@ def _logout(self): | |||
if client.provider_end_session_endpoint: | |||
flask.session['end_session_state'] = rndstr() | |||
|
|||
post_logout_uri = url_for(view_func.__name__, _external=True) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This will fail for logout view functions that are mounted under blueprints. Name of view functions under a blueprint are prefixed by the name of the associated blueprint. In large-scaled flask apps, this is very common. A user should have an option available to provide view function name that can be resolved by url_for
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You're right - I opted to leave this behavior for the draft as that's how it was before.
I've just pushed a change now to make it more general, with tests that show it works regardless of which endpoint the view function is mounted under.
The benefit as I see it with this PR is that it will automatically handle the most common cases (including setting the correct post_logout_redirect_uris
in the registration request) without any changes from the user side.
src/flask_pyoidc/flask_pyoidc.py
Outdated
return [] | ||
def _get_url_for_view_function(self, view_func): | ||
for endpoint, f in current_app.view_functions.items(): | ||
if f == view_func: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This if condition will never be true if you are using flask extensions that wrap view functions to generate OpenAPI documentation (Swagger). For example if you use flask-restx
, the view function object becomes <function Logout.get at 0x000001DF5DB27760>
and is registered with flask app as:
>>> app.view_functions
{'apiv2.logout_logout': <function View.as_view.<locals>.view at 0x000001DF5DB40670>
The reason that registered object is different here is because flask-restx
uses Flask Pluggable Views to register the views. See Views.as_view()
. This also means that even if you are not using any other flask extensions, implementing views with Pluggable Views will produce the same problem.
In an enterprise environment, OpenAPI compliance is a standard so all view functions are likely to be wrapped with extensions that automatically generate swagger documentation for APIs. We must not have conflicts with the use of other flask-extensions.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That is due to the ordering of the decorators: @auth.oidc_logout
needs to be the second outermost decorator, right after the Flask @app.route(...)
:
Nvm, I didn't check the pluggable views reference.
Either way I found a way to greatly simplify it: flask.request.endpoint
can be used to get the current endpoint being hit when doing the logout.
And for the registration, the docs already says:
If the logout view is mounted under a custom endpoint (other than the default, which is
the name of the view function), or if using Blueprints, you
must specify the full URL in the Flask-pyoidc configuration usingpost_logout_redirect_uris
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have done it in the lastest commit of #124. I'm using flask.request.url
instead.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Our approach is exactly same 😂. I have left some reviews to address the changes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hehe! :)
Thanks a ton for the detailed review, I've addressed the comments! 🙇
bd7ed99
to
9d08533
Compare
try: | ||
return url_for(self._logout_view.__name__, _external=True) | ||
return [url_for(view.__name__, _external=True) for view in self._logout_views] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This raises unhandled exception if post_logout_redirect_uris
is not provided in ClientRegistrationInfo
and the client is registered outside of request context.
...
auth = OIDCAuthentication({'default': provider_config})
client = auth.clients['default']
with app.app_context():
auth._register_client(client)
RuntimeError: Application was not able to create a URL adapter for request independent URL generation. You might be able to fix this by setting the SERVER_NAME config variable.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd say this is an expected error, and the given error message points to the solution as well: with SERVER_NAME
in the Flask config it works as expected:
app.config.update({'SERVER_NAME': 'foobar', ...})
auth.init_app(app)
client = auth.clients[PROVIDER_NAME1]
with app.app_context():
auth._register_client(client)
This is standard Flask behavior, that it needs to either have a request context or know the SERVER_NAME
to be able to figure out the URL for and endpoint.
|
||
def _get_url_for_logout_view(self): | ||
if not self._logout_view: | ||
return None |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To prevent RuntimeError
exception as shown below, return empty list here if self._logout_view
is empty.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This will already return an empty list if self._logout_views
is empty.
post_logout_redirect_uris = client._provider_configuration._client_registration_info.get( | ||
'post_logout_redirect_uris') | ||
if not post_logout_redirect_uris: | ||
# If not passed, try to resolve it by using logout view function. | ||
_default_post_logout_redirect_uris = default_post_logout_redirect_uris() | ||
# Set this as an attribute of ClientRegistrationInfo. | ||
client._provider_configuration._client_registration_info[ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don't remove all comments, let it be for future devs so that they can understand the reason of writing this line of code.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I found the comments in this case redundant when looking over them - they don't explain any additional info that is not already in the code itself.
Or am I missing something?
with self.app.test_request_context('/'): | ||
assert authn._get_url_for_logout_view() == f'http://{self.CLIENT_DOMAIN}/logout' | ||
|
||
def test_get_url_for_logout_view_should_raise_build_error_if_mounted_under_custom_endpoint(self): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Keep this test case, removing this has decreased the test coverage.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The method _get_url_for_logout_view
no longer exists so this test didn't make sense after these changes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It does.
Flask-pyoidc/src/flask_pyoidc/flask_pyoidc.py
Lines 91 to 96 in 9d08533
def _get_urls_for_logout_views(self): | |
try: | |
return [url_for(view.__name__, _external=True) for view in self._logout_views] | |
except BuildError: | |
logger.error('could not build url for logout view, it might be mounted under a custom endpoint') | |
raise |
You can mark rest of the conversations as resolved. 🙂
@@ -88,42 +88,22 @@ def init_app(self, app): | |||
for (name, configuration) in self._provider_configurations.items() | |||
} | |||
|
|||
def _get_post_logout_redirect_uri(self, client): | |||
if client.post_logout_redirect_uris: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As this method is no longer required, add a test case to cover post_logout_redirect_uris
property of PyoidcFacade
. Take it from my PR.
@property
def post_logout_redirect_uris(self):
return self._client.registration_response.get('post_logout_redirect_uris')
def test_property_post_logout_redirect_uris(self):
post_logout_redirect_uris = ['https://client.example.com/logout']
client_metadata = self.CLIENT_METADATA.copy(
post_logout_redirect_uris=post_logout_redirect_uris)
facade = PyoidcFacade(ProviderConfiguration(provider_metadata=self.PROVIDER_METADATA,
client_metadata=client_metadata),
REDIRECT_URI)
assert facade.post_logout_redirect_uris == post_logout_redirect_uris
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed! 👍
Test coverage is at 99.20% 🙂. Striving for 100% is good when the code base is not large yet. This will benefit us in future PRs. You can set test coverage threshold to 98% now. How do you bump versions? Should we keep our version scheme consistent with Python? Like keeping it |
@infohash I don't think 100% coverage is necessary. And actually I'd like to refactor the tests sometime™️ for less test code to maintain - that's higher priority than increasing already high coverage. 🙂 As for the versioning: this extension is using semantic versioning, so not tied to Python versioning at all. |
@zamzterz I have delegated all requests to underlying pyoidc library but test cases are failing because |
Note: Not tested yet