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

Token exchange support for ManagedIdentityCredential #19902

Merged
merged 10 commits into from
Aug 6, 2021

Conversation

chlowell
Copy link
Member

With this change, ManagedIdentityCredential authenticates via a token exchange when these variables are set:

  • AZURE_CLIENT_ID
  • AZURE_TENANT_ID
  • TOKEN_FILE_PATH: path to a file containing the JWT to be exchanged
    • the credential caches this file's content for 5 minutes i.e. it reads the file only after 5 minutes have passed since the last time it read the file
  • optional: AZURE_AUTHORITY_HOST

I implemented this atop a new credential class, ClientAssertionCredential, which authenticates with a JWT assertion. It's quite similar to CertificateCredential but instead of constructing a JWT for the "client_assertion" field, this new credential gets one from a callback. I kept ClientAssertionCredential internal because there's no need to make it public today; we may find one in the future.

Closes #19304

@chlowell chlowell requested review from mccoyp and schaabs as code owners July 21, 2021 19:55
@ghost ghost added the Azure.Identity label Jul 21, 2021
# type: (**Any) -> None
super(TokenExchangeCredential, self).__init__()
self._available = all(os.environ.get(var) for var in EnvironmentVariables.TOKEN_EXCHANGE_VARS)
if self._available:
Copy link
Member

Choose a reason for hiding this comment

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

Do you think it would be valuable to do any logging in the case where the credential isn't (or is) available? Users will find out if it's unavailable when a token request is made, but EnvironmentCredential logs availability status (though that's part of the DefaultAzureCredential chain, which might have been the motivation to log there)

Copy link
Member Author

Choose a reason for hiding this comment

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

Summarizing offline conversation: it's moot because ManagedIdentityCredential is the public API of this credential and already logs the interesting bit (what platform it believes it's running on) and will only instantiate this credential when its required environment variables are set. TokenExchangeCredential can therefore assume its prerequisites are met, and stop trying to raise a useful error message when that isn't the case. Commit to that effect forthcoming.

return self

async def close(self) -> None:
await self._credential.close()
Copy link
Member

Choose a reason for hiding this comment

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

Should we do a check for self._credential here (and if we shouldn't, should we do that check in the sync credential)? Also, is there a reason that async credentials follow a close() -> self._x.close() pattern instead of close() -> self.__aexit__() -> self._x.__aexit__() pattern like sync credentials do?

Copy link
Member Author

Choose a reason for hiding this comment

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

We should check that, thanks for noticing! There's no good reason for the sync vs. async difference. I prefer close -> exit today but it doesn't really matter.



class TokenExchangeCredential(ClientAssertionCredential, TokenFileMixin):
def __init__(self, token_file_path, **kwargs):
Copy link
Member

Choose a reason for hiding this comment

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

Should this require a tenant_id, client_id, and get_assertion since ClientAssertionCredential does?

Copy link
Member Author

Choose a reason for hiding this comment

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

Sure, that will make it easier for future generations to understand how this works.

Comment on lines +36 to +42
async def __aenter__(self):
await self._client.__aenter__()
return self

async def close(self) -> None:
"""Close the credential's transport session."""
await self._client.close()
Copy link
Member

Choose a reason for hiding this comment

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

Are you keeping the async context management API since it's more relevant for async 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.

That and self._client here already implements the API. I removed it from the sync credential because I don't want to block this PR on #19746 or paste part of that PR into this one.

@chlowell chlowell merged commit fff4d58 into Azure:main Aug 6, 2021
@chlowell chlowell deleted the token-exchange branch August 6, 2021 20:38
ghost pushed a commit to Azure/azure-sdk-for-js that referenced this pull request Sep 8, 2021
This is a simplified version of what @chlowell did on this PR: Azure/azure-sdk-for-python#19902

This is based on what I understood. I’ll make sure to circle back with Charles before I get this PR out of draft.

Fixes #15800
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Support exchanging k8s token to AAD token
2 participants