Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions sdk/identity/azure-identity/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Azure Identity client library for Python

# Getting started

# Key concepts

# Examples
Shortest path to an access token:
```py
from azure.identity import ClientSecretCredential

credential = ClientSecretCredential(client_id, secret, tenant_id)

# all credentials implement get_token
token = credential.get_token(scopes=["https://vault.azure.net/.default"])
```

# Troubleshooting

# Next steps

# Contributing
7 changes: 7 additions & 0 deletions sdk/identity/azure-identity/azure/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# -------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See LICENSE.txt in the project root for
# license information.
# --------------------------------------------------------------------------
# pylint:disable=missing-docstring
__path__ = __import__("pkgutil").extend_path(__path__, __name__) # type: ignore
16 changes: 16 additions & 0 deletions sdk/identity/azure-identity/azure/identity/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# -------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See LICENSE.txt in the project root for
# license information.
# --------------------------------------------------------------------------
from .exceptions import AuthenticationError
from .credentials import ClientSecretCredential, TokenCredentialChain

__all__ = ["AuthenticationError", "ClientSecretCredential", "TokenCredentialChain"]

try:
from .aio import AsyncClientSecretCredential, AsyncTokenCredentialChain

__all__.extend(["AsyncClientSecretCredential", "AsyncTokenCredentialChain"])
except SyntaxError:
pass
8 changes: 8 additions & 0 deletions sdk/identity/azure-identity/azure/identity/aio/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# -------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See LICENSE.txt in the project root for
# license information.
# --------------------------------------------------------------------------
from .credentials import AsyncClientSecretCredential, AsyncTokenCredentialChain

__all__ = ["AsyncClientSecretCredential", "AsyncTokenCredentialChain"]
53 changes: 53 additions & 0 deletions sdk/identity/azure-identity/azure/identity/aio/authn_client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# -------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See LICENSE.txt in the project root for
# license information.
# --------------------------------------------------------------------------
from typing import Any, Iterable, Mapping, Optional

from azure.core import Configuration
from azure.core.pipeline import AsyncPipeline
from azure.core.pipeline.policies import AsyncRetryPolicy, ContentDecodePolicy, HTTPPolicy, NetworkTraceLoggingPolicy
from azure.core.pipeline.transport import AsyncHttpTransport
from azure.core.pipeline.transport.requests_asyncio import AsyncioRequestsTransport

from ..authn_client import _AuthnClientBase


class AsyncAuthnClient(_AuthnClientBase):
def __init__(
self,
auth_url: str,
config: Optional[Configuration] = None,
policies: Optional[Iterable[HTTPPolicy]] = None,
transport: Optional[AsyncHttpTransport] = None,
**kwargs: Mapping[str, Any]
) -> None:
config = config or self.create_config(**kwargs)
# TODO: ContentDecodePolicy doesn't accept kwargs
policies = policies or [ContentDecodePolicy(), config.logging_policy, config.retry_policy]
if not transport:
transport = AsyncioRequestsTransport(configuration=config)
self._pipeline = AsyncPipeline(transport=transport, policies=policies)
super(AsyncAuthnClient, self).__init__(auth_url, **kwargs)

async def request_token(
self,
scopes: Iterable[str],
method: Optional[str] = "POST",
form_data: Optional[Mapping[str, str]] = None,
params: Optional[Mapping[str, str]] = None,
) -> str:
request = self._prepare_request(method, form_data, params)
response = await self._pipeline.run(request, stream=False)
token = self._deserialize_and_cache_token(response, scopes)
return token

@staticmethod
def create_config(**kwargs: Mapping[str, Any]) -> Configuration:
config = Configuration(**kwargs)
config.logging_policy = NetworkTraceLoggingPolicy(**kwargs)
config.retry_policy = AsyncRetryPolicy(
retry_on_status_codes=[404, 429] + [x for x in range(500, 600)], **kwargs
)
return config
77 changes: 77 additions & 0 deletions sdk/identity/azure-identity/azure/identity/aio/credentials.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# -------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See LICENSE.txt in the project root for
# license information.
# --------------------------------------------------------------------------
from typing import Any, Dict, Iterable, Mapping, Optional

from azure.core import Configuration
from azure.core.pipeline.policies import HTTPPolicy

from .authn_client import AsyncAuthnClient
from ..credentials import TokenCredentialChain
from ..exceptions import AuthenticationError


# pylint:disable=too-few-public-methods

# TODO: could share more code with sync
class _AsyncClientCredentialBase(object):
_OAUTH_ENDPOINT = "https://login.microsoftonline.com/{}/oauth2/v2.0/token"

def __init__(
self,
client_id: str,
tenant_id: str,
config: Optional[Configuration] = None,
policies: Optional[Iterable[HTTPPolicy]] = None,
**kwargs: Mapping[str, Any]
) -> None:
if not client_id:
raise ValueError("client_id")
if not tenant_id:
raise ValueError("tenant_id")
self._client = AsyncAuthnClient(self._OAUTH_ENDPOINT.format(tenant_id), config, policies, **kwargs)
self._form_data = {} # type: Dict[str, str]

async def get_token(self, scopes: Iterable[str]) -> str:
data = self._form_data.copy()
data["scope"] = " ".join(scopes)
token = self._client.get_cached_token(scopes)
if not token:
token = await self._client.request_token(scopes, form_data=data)
return token # type: ignore


class AsyncClientSecretCredential(_AsyncClientCredentialBase):
def __init__(
self,
client_id: str,
secret: str,
tenant_id: str,
config: Optional[Configuration] = None,
**kwargs: Mapping[str, Any]
) -> None:
if not secret:
raise ValueError("secret")
super(AsyncClientSecretCredential, self).__init__(client_id, tenant_id, config, **kwargs)
self._form_data = {"client_id": client_id, "client_secret": secret, "grant_type": "client_credentials"}


class AsyncTokenCredentialChain(TokenCredentialChain):
"""A sequence of token credentials"""

async def get_token(self, scopes: Iterable[str]) -> str:
"""Attempts to get a token from each credential, in order, returning the first token.
If no token is acquired, raises an exception listing error messages.
"""
history = []
for credential in self._credentials:
try:
return await credential.get_token(scopes)
except AuthenticationError as ex:
history.append((credential, ex.message))
except Exception as ex: # pylint: disable=broad-except
history.append((credential, str(ex)))
error_message = self._get_error_message(history)
raise AuthenticationError(error_message)
87 changes: 87 additions & 0 deletions sdk/identity/azure-identity/azure/identity/authn_client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# -------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See LICENSE.txt in the project root for
# license information.
# --------------------------------------------------------------------------
from time import time

from azure.core import Configuration, HttpRequest
from azure.core.pipeline import Pipeline
from azure.core.pipeline.policies import ContentDecodePolicy, NetworkTraceLoggingPolicy, RetryPolicy
from azure.core.pipeline.transport import RequestsTransport
from msal import TokenCache

from .exceptions import AuthenticationError

try:
from typing import TYPE_CHECKING
except ImportError:
TYPE_CHECKING = False
if TYPE_CHECKING:
from typing import Any, Iterable, Mapping, Optional


class _AuthnClientBase(object):
def __init__(self, auth_url, **kwargs):
if not auth_url:
raise ValueError("auth_url")
super(_AuthnClientBase, self).__init__(**kwargs)
self._cache = TokenCache()
self._auth_url = auth_url

def get_cached_token(self, scopes):
# type: (Iterable[str]) -> Optional[str]
tokens = self._cache.find(TokenCache.CredentialType.ACCESS_TOKEN, list(scopes))
for token in tokens:
if all((scope in token["target"] for scope in scopes)):
if int(token["expires_on"]) - 300 > int(time()):
return token["secret"]
return None

def _prepare_request(self, method="POST", form_data=None, params=None):
request = HttpRequest(method, self._auth_url)
if form_data:
request.headers["Content-Type"] = "application/x-www-form-urlencoded"
request.set_formdata_body(form_data)
if params:
request.format_parameters(params)
return request

def _deserialize_and_cache_token(self, response, scopes):
try:
if "deserialized_data" in response.context:
payload = response.context["deserialized_data"]
else:
payload = response.http_response.text()
token = payload["access_token"]
self._cache.add({"response": payload, "scope": scopes})
return token
except KeyError:
raise AuthenticationError("Unexpected authentication response: {}".format(payload))
except Exception as ex:
raise AuthenticationError("Authentication failed: {}".format(str(ex)))


class AuthnClient(_AuthnClientBase):
def __init__(self, auth_url, config=None, policies=None, transport=None, **kwargs):
config = config or self.create_config(**kwargs)
# TODO: ContentDecodePolicy doesn't accept kwargs
policies = policies or [ContentDecodePolicy(), config.logging_policy, config.retry_policy]
if not transport:
transport = RequestsTransport(configuration=config)
self._pipeline = Pipeline(transport=transport, policies=policies)
super(AuthnClient, self).__init__(auth_url, **kwargs)

def request_token(self, scopes, method="POST", form_data=None, params=None):
request = self._prepare_request(method, form_data, params)
response = self._pipeline.run(request, stream=False)
token = self._deserialize_and_cache_token(response, scopes)
return token

@staticmethod
def create_config(**kwargs):
# type: (Mapping[str, Any]) -> Configuration
config = Configuration(**kwargs)
config.logging_policy = NetworkTraceLoggingPolicy(**kwargs)
config.retry_policy = RetryPolicy(retry_on_status_codes=[404, 429] + [x for x in range(500, 600)], **kwargs)
return config
89 changes: 89 additions & 0 deletions sdk/identity/azure-identity/azure/identity/credentials.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# -------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See LICENSE.txt in the project root for
# license information.
# --------------------------------------------------------------------------
from azure.core import Configuration

from .authn_client import AuthnClient
from .exceptions import AuthenticationError

try:
from typing import TYPE_CHECKING
except ImportError:
TYPE_CHECKING = False

if TYPE_CHECKING:
# pylint:disable=unused-import
from typing import Any, Dict, Iterable, Mapping, Optional
from azure.core.pipeline.policies import HTTPPolicy
from azure.core.credentials import SupportsGetToken

# pylint:disable=too-few-public-methods


class _ClientCredentialBase(object):
_OAUTH_ENDPOINT = "https://login.microsoftonline.com/{}/oauth2/v2.0/token"

def __init__(self, client_id, tenant_id, config=None, policies=None, **kwargs):
# type: (str, str, Optional[Configuration], Optional[Iterable[HTTPPolicy]], Mapping[str, Any]) -> None
if not client_id:
raise ValueError("client_id")
if not tenant_id:
raise ValueError("tenant_id")
self._client = AuthnClient(self._OAUTH_ENDPOINT.format(tenant_id), config, policies, **kwargs)
self._form_data = {} # type: Dict[str, str]

def get_token(self, scopes):
# type: (Iterable[str]) -> str
data = self._form_data.copy()
data["scope"] = " ".join(scopes)
token = self._client.get_cached_token(scopes)
if not token:
return self._client.request_token(scopes, form_data=data)
return token


class ClientSecretCredential(_ClientCredentialBase):
def __init__(self, client_id, secret, tenant_id, config=None, **kwargs):
# type: (str, str, str, Optional[Configuration], Mapping[str, Any]) -> None
if not secret:
raise ValueError("secret")
super(ClientSecretCredential, self).__init__(client_id, tenant_id, config, **kwargs)
self._form_data = {"client_id": client_id, "client_secret": secret, "grant_type": "client_credentials"}


class TokenCredentialChain:
"""A sequence of token credentials"""

def __init__(self, credentials):
# type: (Iterable[SupportsGetToken]) -> None
if not credentials:
raise ValueError("at least one credential is required")
self._credentials = credentials

def get_token(self, scopes):
# type: (Iterable[str]) -> str
"""Attempts to get a token from each credential, in order, returning the first token.
If no token is acquired, raises an exception listing error messages.
"""
history = []
for credential in self._credentials:
try:
return credential.get_token(scopes)
except AuthenticationError as ex:
history.append((credential, ex.message))
except Exception as ex: # pylint: disable=broad-except
history.append((credential, str(ex)))
error_message = self._get_error_message(history)
raise AuthenticationError(error_message)

@staticmethod
def _get_error_message(history):
attempts = []
for credential, error in history:
if error:
attempts.append("{}: {}".format(credential.__class__.__name__, error))
else:
attempts.append(credential.__class__.__name__)
return "No valid token received. {}".format(". ".join(attempts))
15 changes: 15 additions & 0 deletions sdk/identity/azure-identity/azure/identity/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# -------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See LICENSE.txt in the project root for
# license information.
# --------------------------------------------------------------------------


# TODO: probably a better base for this in azure-core
class AuthenticationError(Exception):
def __init__(self, message):
# type: (str) -> None
self.message = message

def __str__(self):
return self.message
6 changes: 6 additions & 0 deletions sdk/identity/azure-identity/azure/identity/version.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# -------------------------------------------------------------------------
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License. See LICENSE.txt in the project root for
# license information.
# --------------------------------------------------------------------------
VERSION = "0.0.1"
1 change: 1 addition & 0 deletions sdk/identity/azure-identity/dev_requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
typing_extensions>=3.7.2
Loading