Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
Next Next commit
UsernamePasswordCredential
  • Loading branch information
chlowell committed Jul 30, 2019
commit 197b2680d3d4665dddae1a745e4275544bf28f8f
2 changes: 2 additions & 0 deletions sdk/identity/azure-identity/azure/identity/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
ClientSecretCredential,
EnvironmentCredential,
ManagedIdentityCredential,
UsernamePasswordCredential,
)


Expand Down Expand Up @@ -35,4 +36,5 @@ def __init__(self, **kwargs):
"DefaultAzureCredential",
"EnvironmentCredential",
"ManagedIdentityCredential",
"UsernamePasswordCredential",
]
Original file line number Diff line number Diff line change
Expand Up @@ -93,4 +93,8 @@ class PublicClientCredential(MsalCredential):

def __init__(self, **kwargs):
# type: (Any) -> None
super(PublicClientCredential, self).__init__(app_class=msal.PublicClientApplication, **kwargs)
super(PublicClientCredential, self).__init__(
app_class=msal.PublicClientApplication,
authority="https://login.microsoftonline.com/organizations",
**kwargs
)
58 changes: 58 additions & 0 deletions sdk/identity/azure-identity/azure/identity/credentials.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
Credentials for Azure SDK authentication.
"""
import os
import time

from azure.core import Configuration
from azure.core.credentials import AccessToken
Expand All @@ -14,6 +15,7 @@

from ._authn_client import AuthnClient
from ._base import ClientSecretCredentialBase, CertificateCredentialBase
from ._internal import PublicClientCredential
from ._managed_identity import ImdsCredential, MsiCredential
from .constants import Endpoints, EnvironmentVariables

Expand Down Expand Up @@ -233,3 +235,59 @@ def _get_error_message(history):
else:
attempts.append(credential.__class__.__name__)
return "No valid token received. {}".format(". ".join(attempts))


class UsernamePasswordCredential(PublicClientCredential):
"""
Authenticates a user with a username and password. In general, Microsoft doesn't recommend this kind of
authentication, because it's less secure than other authentication flows.

Authentication with this credential is not interactive, so it is **not compatible with any form of
multi-factor authentication or consent prompting**. The application must already have the user's consent.

This credential can only authenticate work and school accounts; Microsoft accounts are not supported.
See this document for more information about account types:
https://docs.microsoft.com/en-us/azure/active-directory/fundamentals/sign-up-organization

:param str client_id: the application's client ID
:param str username: the user's username (usually an email address)
:param str password: the user's password
"""

def __init__(self, client_id, username, password, **kwargs):
# type: (str, str, str, Any) -> None
super(UsernamePasswordCredential, self).__init__(client_id=client_id, **kwargs)
self._username = username
self._password = password

def get_token(self, *scopes):
# type (*str) -> AccessToken
"""
Request an access token for `scopes`.

:param str scopes: desired scopes for the token
:rtype: :class:`azure.core.credentials.AccessToken`
:raises: :class:`azure.core.exceptions.ClientAuthenticationError`
"""

# MSAL requires scopes be a list
scopes = list(scopes) # type: ignore
now = int(time.time())

accounts = self._app.get_accounts(username=self._username)
result = None
for account in accounts:
result = self._app.acquire_token_silent(scopes, account=account)
if result:
break

if not result:
# cache miss -> request a new token
result = self._app.acquire_token_by_username_password(
username=self._username, password=self._password, scopes=scopes
)

if "access_token" not in result:
raise ClientAuthenticationError(message="authentication failed: {}".format(result.get("error_description")))

return AccessToken(result["access_token"], now + int(result["expires_in"]))
33 changes: 33 additions & 0 deletions sdk/identity/azure-identity/tests/test_identity.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
EnvironmentCredential,
ManagedIdentityCredential,
ChainedTokenCredential,
UsernamePasswordCredential,
)
from azure.identity._managed_identity import ImdsCredential
from azure.identity.constants import EnvironmentVariables
Expand Down Expand Up @@ -239,3 +240,35 @@ def test_imds_credential_retries():

def test_default_credential():
DefaultAzureCredential()


def test_username_password_credential():
expected_token = "access-token"
tenant_id = "guid"
transport = validating_transport(
requests=[Request()] * 2, # not validating requests because they're formed by MSAL
responses=[
# tenant discovery, then a token request
mock_response(json_payload={"authorization_endpoint": "https://a/b", "token_endpoint": "https://a/b"}),
mock_response(
json_payload={
"access_token": expected_token,
"expires_in": 42,
"token_type": "Bearer",
"ext_expires_in": 42,
}
),
],
)

credential = UsernamePasswordCredential(
client_id="some-guid",
tenant_id=tenant_id,
username="user@azure",
password="secret_password",
transport=transport,
instance_discovery=False, # kwargs are passed to MSAL; this one prevents an AAD verification request
)

token = credential.get_token("scope")
assert token.token == expected_token