Skip to content
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

Add authorization_code and password based OAuth login handlers #5547

Merged
merged 9 commits into from
Oct 11, 2023

Conversation

philippjfr
Copy link
Member

@philippjfr philippjfr commented Sep 27, 2023

So far our OAuth login handlers all used authorization flows that required a client_secret. Our new workflow works by using the code challenge auth flow, which redirects the user to a log in page along with a SHA encoded code challenge that is then verified against the verification code that is stored in a cookie.

We also add a handler for the password based grant types, which is simple to set up for development but should generally not be used.

  • Add docs
  • Add tests

@codecov
Copy link

codecov bot commented Sep 27, 2023

Codecov Report

Merging #5547 (3d1c417) into main (e85bb5e) will decrease coverage by 1.12%.
Report is 2 commits behind head on main.
The diff coverage is 19.04%.

@@            Coverage Diff             @@
##             main    #5547      +/-   ##
==========================================
- Coverage   83.44%   82.33%   -1.12%     
==========================================
  Files         275      275              
  Lines       39395    39486      +91     
==========================================
- Hits        32873    32510     -363     
- Misses       6522     6976     +454     
Flag Coverage Δ
ui-tests 38.81% <17.85%> (-1.90%) ⬇️
unitexamples-tests 73.34% <19.04%> (-0.14%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files Coverage Δ
panel/io/server.py 70.90% <18.18%> (-4.66%) ⬇️
panel/command/serve.py 15.18% <5.88%> (-25.05%) ⬇️
panel/auth.py 32.80% <20.71%> (-14.22%) ⬇️

... and 15 files with indirect coverage changes

📣 We’re building smart automated test selection to slash your CI/CD build times. Learn more

@philippjfr philippjfr changed the title Add implementation for password grant based OAuth Add authorization_code and password based OAuth login handlers Sep 27, 2023
@MarcSkovMadsen
Copy link
Collaborator

If its easy to make it mandatory for specific pages or optional the it would be really valuable.

@philippjfr
Copy link
Member Author

@cdeil Would love it if you guys could try this one out on your end.

@@ -12,6 +12,9 @@ The first step in configuring a OAuth is to specify a specific OAuth provider. P
* `gitlab`: GitLab
* `google`: Google
* `okta`: Okta
* `generic`: Generic OAuth Provider with configurable endpoints
* `password`: Generic password grant based OAuth Provider with configurable endpoints
* `code`: Generic code challenge grant based OAuth Provider with configurable endpoints

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

code might be somewhat ambiguous name, since there is also a device code flow. What about auth_code? Right, @armaaar ?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree. There is also an OAuth flow called Device Code flow: https://oauth.net/2/grant-types/device-code/
This name might cause confusion

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense to me, thanks!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe link from the docs here or elsewhere to the API docs to make it easy to explore the details where reading this?

With search it's possible to find e.g. panel.auth.GenericLoginHandler and other relevant classes, but at least for me having a link to click when reading here would be nice to have.

- `TOKEN_URL`: The token endpoint of the authentication server, may also be provided using the `PANEL_OAUTH_TOKEN_URL` environment variable.
- `USER_URL`: The user information endpoint of the authentication server, may also be provided using the `PANEL_OAUTH_USER_URL` environment variable.

The difference between these three providers is the authentication flow they perform. The `generic` provider uses the standard authentication flow, which requests authorization using the client secret, while the `password` based workflow lets the user log in via a form served on the server (only recommended for testing and development), and the `code` uses a code challenge based auth flow.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is nothing call "standard authentication flow", I guess you mean client credentials flow: https://oauth.net/2/grant-types/client-credentials/

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably going to split this guide up into separate pages for the different grant types to clarify this.

panel/auth.py Show resolved Hide resolved
panel/auth.py Show resolved Hide resolved
panel/io/server.py Outdated Show resolved Hide resolved
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We get this error

Uncaught exception GET /auth (::1)
HTTPServerRequest(protocol='http', host='localhost:5005', method='GET', uri='/auth', version='HTTP/1.1', remote_ip='::1')
Traceback (most recent call last):
  File "***/lib/python3.10/site-packages/tornado/web.py", line 1784, in _execute
    result = method(*self.path_args, **self.path_kwargs)
  File "***/lib/python3.10/site-packages/tornado/web.py", line 3276, in wrapper
    if not self.current_user:
  File "***/lib/python3.10/site-packages/tornado/web.py", line 1420, in current_user
    self._current_user = self.get_current_user()
  File "***/lib/python3.10/site-packages/bokeh/server/views/auth_request_handler.py", line 71, in get_current_user
    return self.application.auth_provider.get_user(self)
  File "***/panel/auth.py", line 1038, in get_user
    return request_handler.get_secure_cookie("user", max_age_days=config.oauth_expiry)
  File "***/lib/python3.10/site-packages/tornado/web.py", line 836, in get_signed_cookie
    self.require_setting("cookie_secret", "secure cookies")
  File "***/lib/python3.10/site-packages/tornado/web.py", line 1669, in require_setting
    raise Exception(
Exception: You must define the 'cookie_secret' setting in your application to use secure cookies
500 GET /auth (::1) 22.92ms

I thought you create this by default. Do we need to always supply it? if so, shouldn't it be documented that this is required?

@armaaar
Copy link

armaaar commented Sep 28, 2023

The solution doesn't work for us. It results in many redirects:

Screenshot 2023-09-28 at 10 54 43 AM

Here is the final URL: http://localhost:5005/auth?next=%2Fauth%3Fnext%3D%252Fauth%253Fnext%253D%25252Fauth%25253Fnext%25253D%2525252Fauth%2525253Fnext%2525253D%252525252Fauth%252525253Fnext%252525253D%25252525252Fauth%25252525253Fnext%25252525253D%2525252525252Fauth%2525252525253Fnext%2525252525253D%252525252525252Fauth%252525252525253Fnext%252525252525253D%25252525252525252Fauth%25252525252525253Fnext%25252525252525253D%2525252525252525252Fauth%2525252525252525253Fnext%2525252525252525253D%252525252525252525252Fauth%252525252525252525253Fnext%252525252525252525253D%25252525252525252525252Fauth%25252525252525252525253Fnext%25252525252525252525253D%2525252525252525252525252Fauth%2525252525252525252525253Fnext%2525252525252525252525253D%252525252525252525252525252Fauth%252525252525252525252525253Fnext%252525252525252525252525253D%25252525252525252525252525252Fauth%25252525252525252525252525253Fnext%25252525252525252525252525253D%2525252525252525252525252525252Fauth%2525252525252525252525252525253Fnext%2525252525252525252525252525253D%252525252525252525252525252525252Fauth%252525252525252525252525252525253Fnext%252525252525252525252525252525253D%25252525252525252525252525252525252Fauth%25252525252525252525252525252525253Fnext%25252525252525252525252525252525253D%2525252525252525252525252525252525252Fauth%2525252525252525252525252525252525253Fnext%2525252525252525252525252525252525253D%252525252525252525252525252525252525252Fauth%252525252525252525252525252525252525253Fnext%252525252525252525252525252525252525253D%25252525252525252525252525252525252525252Fauth%25252525252525252525252525252525252525253Fnext%25252525252525252525252525252525252525253D%2525252525252525252525252525252525252525252Fauth%2525252525252525252525252525252525252525253Fnext%2525252525252525252525252525252525252525253D%252525252525252525252525252525252525252525252Fauth%252525252525252525252525252525252525252525253Fnext%252525252525252525252525252525252525252525253D%25252525252525252525252525252525252525252525252Fauth%25252525252525252525252525252525252525252525253Fnext%25252525252525252525252525252525252525252525253D%2525252525252525252525252525252525252525252525252Fauth%2525252525252525252525252525252525252525252525253Fnext%2525252525252525252525252525252525252525252525253D%252525252525252525252525252525252525252525252525252Fauth%252525252525252525252525252525252525252525252525253Fnext%252525252525252525252525252525252525252525252525253D%25252525252525252525252525252525252525252525252525252Fauth%25252525252525252525252525252525252525252525252525253Fnext%25252525252525252525252525252525252525252525252525253D%2525252525252525252525252525252525252525252525252525252Fauth%2525252525252525252525252525252525252525252525252525253Fnext%2525252525252525252525252525252525252525252525252525253D%252525252525252525252525252525252525252525252525252525252Fauth%252525252525252525252525252525252525252525252525252525253Fnext%252525252525252525252525252525252525252525252525252525253D%25252525252525252525252525252525252525252525252525252525252Fauth%25252525252525252525252525252525252525252525252525252525253Fnext%25252525252525252525252525252525252525252525252525252525253D%2525252525252525252525252525252525252525252525252525252525252Fauth%2525252525252525252525252525252525252525252525252525252525253Fnext%2525252525252525252525252525252525252525252525252525252525253D%252525252525252525252525252525252525252525252525252525252525252Fauth%252525252525252525252525252525252525252525252525252525252525253Fnext%252525252525252525252525252525252525252525252525252525252525253D%25252525252525252525252525252525252525252525252525252525252525252Fauth%25252525252525252525252525252525252525252525252525252525252525253Fnext%25252525252525252525252525252525252525252525252525252525252525253D%2525252525252525252525252525252525252525252525252525252525252525252Fauth%2525252525252525252525252525252525252525252525252525252525252525253Fnext%2525252525252525252525252525252525252525252525252525252525252525253D%252525252525252525252525252525252525252525252525252525252525252525252Fauth%252525252525252525252525252525252525252525252525252525252525252525253Fnext%252525252525252525252525252525252525252525252525252525252525252525253D%25252525252525252525252525252525252525252525252525252525252525252525252Fauth%25252525252525252525252525252525252525252525252525252525252525252525253Fnext%25252525252525252525252525252525252525252525252525252525252525252525253D%2525252525252525252525252525252525252525252525252525252525252525252525252Fauth%2525252525252525252525252525252525252525252525252525252525252525252525253Fnext%2525252525252525252525252525252525252525252525252525252525252525252525253D%252525252525252525252525252525252525252525252525252525252525252525252525252F

@armaaar
Copy link

armaaar commented Sep 28, 2023

We have a huge problem with not being able to logout.
Currently there is no logout handler for OAuthProvider. That means that once a user login, they can't logout.
This is not the only problem, the current LogoutHandler that is used in BasicAuthProvider can't be used in our use case as well. To fully understand why, take this into consideration:

  1. In order to fully logout, we end our session and revoke our token from the identity server. If not, we will always be logged in.
  2. The LogoutHandler only removes cookies, but doesn't end the session or revoke the token
  3. Panel would always redirect to the Auth provider portal if not authenticated

Now if we assume that there is a logout button, have a look into this scenario:

  1. You visit Panel app, you visit panel login page
  2. you are redirected to auth portal
  3. you login in portal, and get redirected back to panel app
  4. you logout, cookies are cleared, you are redirected to login page
  5. you are redirected to auth portal again
  6. Since you are still logged in in the portal, you are immediately logged in again and redirected to panel app
  7. you are not logged out as intended

Just clearing cookies, and not allowing viewing any page without being authenticated, leads to always being logged in after the first login attempt. without the ability to ever logout. This is unacceptable for us. It also prevents people with multiple accounts to view anything as they are locked to the first account they logged in with

Comment on lines +474 to +484
user_headers = dict(self._API_BASE_HEADERS)
if self._access_token_header:
user_url = self._OAUTH_USER_URL
user_headers['Authorization'] = self._access_token_header.format(
body['access_token']
)
else:
user_url = '{}{}'.format(self._OAUTH_USER_URL, body['access_token'])

user_response = await http.fetch(user_url, headers=user_headers)
user = decode_response_body(user_response)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In our use case, The user url is a custom URL which uses one of the decoded properties of the access token as one of its path parameters. We can't supply a fixed url.
We actually don't care about the user info so much, either Make this optional, or provide a way (like a hook) to set the user info ourselves

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll have to retest this but when I tried it with your standard endpoint I got back a valid response containing my user information.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Multiple users from our end return a 403 forbidden response

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you set openid as one of the scopes?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I couldn't reach that point due to the infinite redirects bug

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@philippjfr
Copy link
Member Author

Appreciate the review comments, all reasonable requests.

@philippjfr
Copy link
Member Author

Going to merge this PR and then continue improving logout and access_token refresh handling as well as the docs in another PR.

@philippjfr philippjfr merged commit da4a0f6 into main Oct 11, 2023
12 of 15 checks passed
@philippjfr philippjfr deleted the password_grant_oauth branch October 11, 2023 14:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants