This repository was archived by the owner on Apr 26, 2024. It is now read-only.
-
-
Notifications
You must be signed in to change notification settings - Fork 2.1k
Support PKCE for OAuth 2.0 #14750
Merged
Merged
Support PKCE for OAuth 2.0 #14750
Changes from all commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
32475f0
Support OIDC code challenges.
clokep 4aad25e
Newsfragment
clokep 5e910b8
Changes from review.
clokep 1e82afd
Fix outdated comment.
clokep 45d3bba
Clarify comments.
clokep 1ebd224
Clarify comment.
clokep 8682a71
Merge branch 'develop' into clokep/oidc-code-challenges
clokep d504e47
Support disabling PKCE.
clokep 08df5d4
Merge branch 'develop' into clokep/oidc-code-challenges
clokep File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| Support [RFC7636](https://datatracker.ietf.org/doc/html/rfc7636) Proof Key for Code Exchange for OAuth single sign-on. | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -36,6 +36,7 @@ | |
| from authlib.jose.errors import InvalidClaimError, JoseError, MissingClaimError | ||
| from authlib.oauth2.auth import ClientAuth | ||
| from authlib.oauth2.rfc6749.parameters import prepare_grant_uri | ||
| from authlib.oauth2.rfc7636.challenge import create_s256_code_challenge | ||
| from authlib.oidc.core import CodeIDToken, UserInfo | ||
| from authlib.oidc.discovery import OpenIDProviderMetadata, get_well_known_url | ||
| from jinja2 import Environment, Template | ||
|
|
@@ -475,6 +476,16 @@ def _validate_metadata(self, m: OpenIDProviderMetadata) -> None: | |
| ) | ||
| ) | ||
|
|
||
| # If PKCE support is advertised ensure the wanted method is available. | ||
| if m.get("code_challenge_methods_supported") is not None: | ||
| m.validate_code_challenge_methods_supported() | ||
| if "S256" not in m["code_challenge_methods_supported"]: | ||
| raise ValueError( | ||
| '"S256" not in "code_challenge_methods_supported" ({supported!r})'.format( | ||
| supported=m["code_challenge_methods_supported"], | ||
| ) | ||
| ) | ||
|
|
||
| if m.get("response_types_supported") is not None: | ||
| m.validate_response_types_supported() | ||
|
|
||
|
|
@@ -602,6 +613,11 @@ async def _load_metadata(self) -> OpenIDProviderMetadata: | |
| if self._config.jwks_uri: | ||
| metadata["jwks_uri"] = self._config.jwks_uri | ||
|
|
||
| if self._config.pkce_method == "always": | ||
| metadata["code_challenge_methods_supported"] = ["S256"] | ||
| elif self._config.pkce_method == "never": | ||
| metadata.pop("code_challenge_methods_supported", None) | ||
|
|
||
| self._validate_metadata(metadata) | ||
|
|
||
| return metadata | ||
|
|
@@ -653,7 +669,7 @@ async def _load_jwks(self) -> JWKS: | |
|
|
||
| return jwk_set | ||
|
|
||
| async def _exchange_code(self, code: str) -> Token: | ||
| async def _exchange_code(self, code: str, code_verifier: str) -> Token: | ||
| """Exchange an authorization code for a token. | ||
|
|
||
| This calls the ``token_endpoint`` with the authorization code we | ||
|
|
@@ -666,6 +682,7 @@ async def _exchange_code(self, code: str) -> Token: | |
|
|
||
| Args: | ||
| code: The authorization code we got from the callback. | ||
| code_verifier: The PKCE code verifier to send, blank if unused. | ||
|
|
||
| Returns: | ||
| A dict containing various tokens. | ||
|
|
@@ -696,6 +713,8 @@ async def _exchange_code(self, code: str) -> Token: | |
| "code": code, | ||
| "redirect_uri": self._callback_url, | ||
| } | ||
| if code_verifier: | ||
| args["code_verifier"] = code_verifier | ||
| body = urlencode(args, True) | ||
|
|
||
| # Fill the body/headers with credentials | ||
|
|
@@ -914,11 +933,14 @@ async def handle_redirect_request( | |
| - ``scope``: the list of scopes set in ``oidc_config.scopes`` | ||
| - ``state``: a random string | ||
| - ``nonce``: a random string | ||
clokep marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| - ``code_challenge``: a RFC7636 code challenge (if PKCE is supported) | ||
|
|
||
| In addition generating a redirect URL, we are setting a cookie with | ||
| a signed macaroon token containing the state, the nonce and the | ||
| client_redirect_url params. Those are then checked when the client | ||
| comes back from the provider. | ||
| In addition to generating a redirect URL, we are setting a cookie with | ||
| a signed macaroon token containing the state, the nonce, the | ||
| client_redirect_url, and (optionally) the code_verifier params. The state, | ||
| nonce, and client_redirect_url are then checked when the client comes back | ||
| from the provider. The code_verifier is passed back to the server during | ||
| the token exchange and compared to the code_challenge sent in this request. | ||
|
|
||
| Args: | ||
| request: the incoming request from the browser. | ||
|
|
@@ -935,17 +957,33 @@ async def handle_redirect_request( | |
|
|
||
clokep marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| state = generate_token() | ||
| nonce = generate_token() | ||
| code_verifier = "" | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I wondered why you weren't making that optional, but then I remembered we rely on macaroons for those... :'(
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeahhhh, I figured this was the cleanest way to do it. |
||
|
|
||
| if not client_redirect_url: | ||
| client_redirect_url = b"" | ||
|
|
||
| metadata = await self.load_metadata() | ||
|
|
||
| # Automatically enable PKCE if it is supported. | ||
| extra_grant_values = {} | ||
| if metadata.get("code_challenge_methods_supported"): | ||
| code_verifier = generate_token(48) | ||
|
|
||
| # Note that we verified the server supports S256 earlier (in | ||
| # OidcProvider._validate_metadata). | ||
| extra_grant_values = { | ||
| "code_challenge_method": "S256", | ||
| "code_challenge": create_s256_code_challenge(code_verifier), | ||
| } | ||
|
|
||
| cookie = self._macaroon_generaton.generate_oidc_session_token( | ||
| state=state, | ||
| session_data=OidcSessionData( | ||
| idp_id=self.idp_id, | ||
| nonce=nonce, | ||
| client_redirect_url=client_redirect_url.decode(), | ||
| ui_auth_session_id=ui_auth_session_id or "", | ||
| code_verifier=code_verifier, | ||
| ), | ||
| ) | ||
|
|
||
|
|
@@ -966,7 +1004,6 @@ async def handle_redirect_request( | |
| ) | ||
| ) | ||
|
|
||
| metadata = await self.load_metadata() | ||
| authorization_endpoint = metadata.get("authorization_endpoint") | ||
| return prepare_grant_uri( | ||
| authorization_endpoint, | ||
|
|
@@ -976,6 +1013,7 @@ async def handle_redirect_request( | |
| scope=self._scopes, | ||
| state=state, | ||
| nonce=nonce, | ||
| **extra_grant_values, | ||
| ) | ||
|
|
||
| async def handle_oidc_callback( | ||
|
|
@@ -1003,7 +1041,9 @@ async def handle_oidc_callback( | |
| # Exchange the code with the provider | ||
| try: | ||
| logger.debug("Exchanging OAuth2 code for a token") | ||
| token = await self._exchange_code(code) | ||
| token = await self._exchange_code( | ||
| code, code_verifier=session_data.code_verifier | ||
| ) | ||
| except OidcError as e: | ||
| logger.warning("Could not exchange OAuth2 code: %s", e) | ||
| self._sso_handler.render_error(request, e.error, e.error_description) | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.