-
Notifications
You must be signed in to change notification settings - Fork 3.2k
[Key Vault] Add support for multi-tenant authentication #21290
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
Changes from 14 commits
bc7c6e9
0b999e6
9c4f089
e50dfb1
c3f17b9
788bd24
757368b
f4a7203
697f1f0
9b951a8
0c583c5
c323d13
e7be78f
d47cb3f
241de3f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -13,67 +13,67 @@ | |
| requirements can change. For example, a vault may move to a new tenant. In such a case the policy will attempt the | ||
| protocol again. | ||
| """ | ||
|
|
||
| import time | ||
| from typing import TYPE_CHECKING | ||
|
|
||
| from azure.core.pipeline.policies import AsyncHTTPPolicy | ||
| from azure.core.pipeline.policies import AsyncBearerTokenCredentialPolicy | ||
|
|
||
| from . import HttpChallengeCache | ||
| from .challenge_auth_policy import _enforce_tls, _get_challenge_request, _update_challenge, ChallengeAuthPolicyBase | ||
| from . import http_challenge_cache as ChallengeCache | ||
| from .challenge_auth_policy import _enforce_tls, _get_challenge_scope, _update_challenge | ||
|
|
||
| if TYPE_CHECKING: | ||
| from typing import Any | ||
| from typing import Any, Optional | ||
| from azure.core.credentials import AccessToken | ||
| from azure.core.credentials_async import AsyncTokenCredential | ||
| from azure.core.pipeline import PipelineRequest | ||
| from azure.core.pipeline.transport import AsyncHttpResponse | ||
| from . import HttpChallenge | ||
| from azure.core.pipeline import PipelineRequest, PipelineResponse | ||
|
|
||
|
|
||
| class AsyncChallengeAuthPolicy(ChallengeAuthPolicyBase, AsyncHTTPPolicy): | ||
| class AsyncChallengeAuthPolicy(AsyncBearerTokenCredentialPolicy): | ||
| """policy for handling HTTP authentication challenges""" | ||
|
|
||
| def __init__(self, credential: "AsyncTokenCredential", **kwargs: "Any") -> None: | ||
| def __init__(self, credential: "AsyncTokenCredential", *scopes: str, **kwargs: "Any") -> None: | ||
|
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. I added an |
||
| super().__init__(credential, *scopes, **kwargs) | ||
| self._credential = credential | ||
| super(AsyncChallengeAuthPolicy, self).__init__(**kwargs) | ||
| self._token = None # type: Optional[AccessToken] | ||
|
|
||
| async def send(self, request: "PipelineRequest") -> "AsyncHttpResponse": | ||
| async def on_request(self, request: "PipelineRequest") -> None: | ||
| _enforce_tls(request) | ||
|
|
||
| challenge = HttpChallengeCache.get_challenge_for_url(request.http_request.url) | ||
| if not challenge: | ||
| challenge_request = _get_challenge_request(request) | ||
| challenger = await self.next.send(challenge_request) | ||
| try: | ||
| challenge = _update_challenge(request, challenger) | ||
| except ValueError: | ||
| # didn't receive the expected challenge -> nothing more this policy can do | ||
| return challenger | ||
|
|
||
| await self._handle_challenge(request, challenge) | ||
| response = await self.next.send(request) | ||
|
|
||
| if response.http_response.status_code == 401: | ||
| # any cached token must be invalid | ||
| self._token = None | ||
|
|
||
| # cached challenge could be outdated; maybe this response has a new one? | ||
| try: | ||
| challenge = _update_challenge(request, response) | ||
| except ValueError: | ||
| # 401 with no legible challenge -> nothing more this policy can do | ||
| return response | ||
|
|
||
| await self._handle_challenge(request, challenge) | ||
| response = await self.next.send(request) | ||
|
|
||
| return response | ||
|
|
||
| async def _handle_challenge(self, request: "PipelineRequest", challenge: "HttpChallenge") -> None: | ||
| """authenticate according to challenge, add Authorization header to request""" | ||
|
|
||
| if self._need_new_token: | ||
| # azure-identity credentials require an AADv2 scope but the challenge may specify an AADv1 resource | ||
| scope = challenge.get_scope() or challenge.get_resource() + "/.default" | ||
| self._token = await self._credential.get_token(scope) | ||
|
|
||
| # ignore mypy's warning because although self._token is Optional, get_token raises when it fails to get a token | ||
| request.http_request.headers["Authorization"] = "Bearer {}".format(self._token.token) # type: ignore | ||
| challenge = ChallengeCache.get_challenge_for_url(request.http_request.url) | ||
| if challenge: | ||
| # Note that if the vault has moved to a new tenant since our last request for it, this request will fail. | ||
| if self._need_new_token: | ||
| scope = _get_challenge_scope(challenge) | ||
| self._token = await self._credential.get_token(scope, tenant_id=challenge.tenant_id) | ||
|
|
||
| # ignore mypy's warning -- although self._token is Optional, get_token raises when it fails to get a token | ||
| request.http_request.headers["Authorization"] = "Bearer {}".format(self._token.token) # type: ignore | ||
| return | ||
|
|
||
| # else: discover authentication information by eliciting a challenge from Key Vault. Remove any request data, | ||
| # saving it for later. Key Vault will reject the request as unauthorized and respond with a challenge. | ||
| # on_challenge will parse that challenge, reattach any body removed here, authorize the request, and tell | ||
| # super to send it again. | ||
| if request.http_request.body: | ||
| request.context["key_vault_request_data"] = request.http_request.body | ||
| request.http_request.set_json_body(None) | ||
| request.http_request.headers["Content-Length"] = "0" | ||
|
|
||
|
|
||
| async def on_challenge(self, request: "PipelineRequest", response: "PipelineResponse") -> bool: | ||
| try: | ||
| challenge = _update_challenge(request, response) | ||
| scope = _get_challenge_scope(challenge) | ||
| except ValueError: | ||
| return False | ||
|
|
||
| body = request.context.pop("key_vault_request_data", None) | ||
| request.http_request.set_text_body(body) # no-op when text is None | ||
| await self.authorize_request(request, scope, tenant_id=challenge.tenant_id) | ||
|
|
||
| return True | ||
|
|
||
| @property | ||
| def _need_new_token(self) -> bool: | ||
| # pylint:disable=invalid-overridden-method | ||
|
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. I debated internally a bit over adding this override as a property. I suppressed this warning because the situation in |
||
| return not self._token or self._token.expires_on - time.time() < 300 | ||
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.
Could you give more context about this change?
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.
Just updated the PR description with more context!