1515
1616from ._authn_client import AuthnClient
1717from ._base import ClientSecretCredentialBase , CertificateCredentialBase
18- from ._internal import PublicClientCredential
18+ from ._internal import PublicClientCredential , wrap_exceptions
1919from ._managed_identity import ImdsCredential , MsiCredential
2020from .constants import Endpoints , EnvironmentVariables
2121
2626
2727if TYPE_CHECKING :
2828 # pylint:disable=unused-import
29- from typing import Any , Dict , Mapping , Optional , Union
29+ from typing import Any , Callable , Dict , Mapping , Optional , Union
3030 from azure .core .credentials import TokenCredential
31+
3132 EnvironmentCredentialTypes = Union ["CertificateCredential" , "ClientSecretCredential" , "UsernamePasswordCredential" ]
3233
3334# pylint:disable=too-few-public-methods
@@ -249,6 +250,86 @@ def _get_error_message(history):
249250 return "No valid token received. {}" .format (". " .join (attempts ))
250251
251252
253+ class DeviceCodeCredential (PublicClientCredential ):
254+ """
255+ Authenticates users through the device code flow. When ``get_token`` is called, this credential acquires a
256+ verification URL and code from Azure Active Directory. A user must browse to the URL, enter the code, and
257+ authenticate with Directory. If the user authenticates successfully, the credential receives an access token.
258+
259+ This credential doesn't cache tokens--each ``get_token`` call begins a new authentication flow.
260+
261+ For more information about the device code flow, see Azure Active Directory documentation:
262+ https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-device-code
263+
264+ :param str client_id: the application's ID
265+ :param prompt_callback: (optional) A callback enabling control of how authentication instructions are presented.
266+ If not provided, the credential will print instructions to stdout.
267+ :type prompt_callback: A callable accepting arguments (``verification_uri``, ``user_code``, ``expires_in``):
268+ - ``verification_uri`` (str) the URL the user must visit
269+ - ``user_code`` (str) the code the user must enter there
270+ - ``expires_in`` (int) the number of seconds the code will be valid
271+
272+ **Keyword arguments:**
273+
274+ - *tenant (str)* - tenant ID or a domain associated with a tenant. If not provided, the credential defaults to the
275+ 'organizations' tenant, which supports only Azure Active Directory work or school accounts.
276+
277+ - *timeout (int)* - seconds to wait for the user to authenticate. Defaults to the validity period of the device code
278+ as set by Azure Active Directory, which also prevails when ``timeout`` is longer.
279+
280+ """
281+
282+ def __init__ (self , client_id , prompt_callback = None , ** kwargs ):
283+ # type: (str, Optional[Callable[[str, str], None]], Any) -> None
284+ self ._timeout = kwargs .pop ("timeout" , None ) # type: Optional[int]
285+ self ._prompt_callback = prompt_callback
286+ super (DeviceCodeCredential , self ).__init__ (client_id = client_id , ** kwargs )
287+
288+ @wrap_exceptions
289+ def get_token (self , * scopes ):
290+ # type (*str) -> AccessToken
291+ """
292+ Request an access token for `scopes`. This credential won't cache the token. Each call begins a new
293+ authentication flow.
294+
295+ :param str scopes: desired scopes for the token
296+ :rtype: :class:`azure.core.credentials.AccessToken`
297+ :raises: :class:`azure.core.exceptions.ClientAuthenticationError`
298+ """
299+
300+ # MSAL requires scopes be a list
301+ scopes = list (scopes ) # type: ignore
302+ now = int (time .time ())
303+
304+ app = self ._get_app ()
305+ flow = app .initiate_device_flow (scopes )
306+ if "error" in flow :
307+ raise ClientAuthenticationError (
308+ message = "Couldn't begin authentication: {}" .format (flow .get ("error_description" ) or flow .get ("error" ))
309+ )
310+
311+ if self ._prompt_callback :
312+ self ._prompt_callback (flow ["verification_uri" ], flow ["user_code" ], flow ["expires_in" ])
313+ else :
314+ print (flow ["message" ])
315+
316+ if self ._timeout is not None and self ._timeout < flow ["expires_in" ]:
317+ deadline = now + self ._timeout
318+ result = app .acquire_token_by_device_flow (flow , exit_condition = lambda flow : time .time () > deadline )
319+ else :
320+ result = app .acquire_token_by_device_flow (flow )
321+
322+ if "access_token" not in result :
323+ if result .get ("error" ) == "authorization_pending" :
324+ message = "Timed out waiting for user to authenticate"
325+ else :
326+ message = "Authentication failed: {}" .format (result .get ("error_description" ) or result .get ("error" ))
327+ raise ClientAuthenticationError (message = message )
328+
329+ token = AccessToken (result ["access_token" ], now + int (result ["expires_in" ]))
330+ return token
331+
332+
252333class UsernamePasswordCredential (PublicClientCredential ):
253334 """
254335 Authenticates a user with a username and password. In general, Microsoft doesn't recommend this kind of
@@ -267,8 +348,9 @@ class UsernamePasswordCredential(PublicClientCredential):
267348
268349 **Keyword arguments:**
269350
270- *tenant (str)* - a tenant ID or a domain associated with a tenant. If not provided, the credential defaults to the
271- 'organizations' tenant.
351+ - **tenant (str)** - a tenant ID or a domain associated with a tenant. If not provided, defaults to the
352+ 'organizations' tenant.
353+
272354 """
273355
274356 def __init__ (self , client_id , username , password , ** kwargs ):
@@ -277,6 +359,7 @@ def __init__(self, client_id, username, password, **kwargs):
277359 self ._username = username
278360 self ._password = password
279361
362+ @wrap_exceptions
280363 def get_token (self , * scopes ):
281364 # type (*str) -> AccessToken
282365 """
0 commit comments