Skip to content

Commit b042705

Browse files
authored
[Core] Document TokenCredential implementation requirements (Azure#22931)
1 parent c4fb02c commit b042705

File tree

3 files changed

+95
-11
lines changed

3 files changed

+95
-11
lines changed

sdk/core/azure-core/CLIENT_LIBRARY_DEVELOPER.md

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -522,3 +522,65 @@ class Pipeline:
522522
return first_node.send(pipeline_request) # type: ignore
523523

524524
```
525+
526+
## Credentials
527+
528+
### TokenCredential protocol
529+
530+
Clients from the Azure SDK often require a `TokenCredential` instance in their constructors. A `TokenCredential` is
531+
meant to provide OAuth tokens to authenticate service requests and can be implemented in a number of ways.
532+
533+
The `TokenCredential` protocol specifies a class that has a single method -- `get_token` -- which returns an
534+
`AccessToken`: a `NamedTuple` containing a `token` string and an `expires_on` integer (in Unix time).
535+
536+
```python
537+
AccessToken = NamedTuple("AccessToken", [("token", str), ("expires_on", int)])
538+
539+
class TokenCredential(Protocol):
540+
"""Protocol for classes able to provide OAuth tokens."""
541+
542+
def get_token(
543+
self, *scopes: str, claims: Optional[str] = None, tenant_id: Optional[str] = None, **kwargs: Any
544+
) -> AccessToken:
545+
"""Request an access token for `scopes`.
546+
547+
:param str scopes: The type(s) of access needed.
548+
549+
:keyword str claims: Additional claims required in the token, such as those returned in a resource
550+
provider's claims challenge following an authorization failure.
551+
:keyword str tenant_id: Optional tenant to include in the token request.
552+
553+
:rtype: AccessToken
554+
:return: An AccessToken instance containing the token string and its expiration time in Unix time.
555+
"""
556+
```
557+
558+
A `TokenCredential` implementation needs to implement the `get_token` method to these specifications and can optionally
559+
implement additional methods. The [`azure-identity`][identity_github] package has a number of `TokenCredential`
560+
implementations that can be used for reference. For example, the [`InteractiveCredential`][interactive_cred] is used as
561+
a base class for multiple credentials and uses `claims` and `tenant_id` in token requests.
562+
563+
There is also an async protocol -- the `AsyncTokenCredential` protocol -- that specifies a class with an aysnc
564+
`get_token` method with the same arguments. An `AsyncTokenCredential` implementation additionally needs to be a context
565+
manager, with `__aenter__`, `__aexit__`, and `close` methods.
566+
567+
#### Known uses of `get_token` keyword-only parameters
568+
569+
**`claims`**
570+
571+
| Service/Feature | Reason |
572+
| --- | --- |
573+
| [Continuous Access Evaluation][cae_doc] | Respond to claim challenges when unexpired tokens have access revoked
574+
575+
**`tenant_id`**
576+
577+
| Service/Feature | Reason |
578+
| --- | --- |
579+
| Key Vault ([example][kv_tenant_id]) | Request access in a tenant that was discovered as part of an authentication challenge
580+
581+
582+
[cae_doc]: https://docs.microsoft.com/azure/active-directory/conditional-access/concept-continuous-access-evaluation
583+
[custom_creds_sample]: https://github.com/Azure/azure-sdk-for-python/blob/fc95f8d3d84d076ffea158116ca1bf6912689c70/sdk/identity/azure-identity/samples/custom_credentials.py
584+
[identity_github]: https://github.com/Azure/azure-sdk-for-python/tree/main/sdk/identity/azure-identity
585+
[interactive_cred]: https://github.com/Azure/azure-sdk-for-python/blob/58c974883123b10b1ca9249ac49109220facb02f/sdk/identity/azure-identity/azure/identity/_internal/interactive.py
586+
[kv_tenant_id]: https://github.com/Azure/azure-sdk-for-python/blob/0a0cc97f178a7476ec79f29c090b8c93ad5d4955/sdk/keyvault/azure-keyvault-keys/azure/keyvault/keys/_shared/challenge_auth_policy.py#L102

sdk/core/azure-core/azure/core/credentials.py

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,21 +8,28 @@
88
import six
99

1010
if TYPE_CHECKING:
11-
from typing import Any, NamedTuple
11+
from typing import Any, Optional, NamedTuple
1212
from typing_extensions import Protocol
1313

1414
AccessToken = NamedTuple("AccessToken", [("token", str), ("expires_on", int)])
1515

1616
class TokenCredential(Protocol):
17-
"""Protocol for classes able to provide OAuth tokens.
17+
"""Protocol for classes able to provide OAuth tokens."""
1818

19-
:param str scopes: Lets you specify the type of access needed.
20-
"""
19+
def get_token(
20+
self, *scopes: str, claims: Optional[str] = None, tenant_id: Optional[str] = None, **kwargs: Any
21+
) -> AccessToken:
22+
"""Request an access token for `scopes`.
23+
24+
:param str scopes: The type of access needed.
2125
22-
# pylint:disable=too-few-public-methods
23-
def get_token(self, *scopes, **kwargs):
24-
# type: (*str, **Any) -> AccessToken
25-
pass
26+
:keyword str claims: Additional claims required in the token, such as those returned in a resource
27+
provider's claims challenge following an authorization failure.
28+
:keyword str tenant_id: Optional tenant to include in the token request.
29+
30+
:rtype: AccessToken
31+
:return: An AccessToken instance containing the token string and its expiration time in Unix time.
32+
"""
2633

2734

2835
else:
@@ -122,6 +129,7 @@ class AzureNamedKeyCredential(object):
122129
:param str key: The key used to authenticate to an Azure service.
123130
:raises: TypeError
124131
"""
132+
125133
def __init__(self, name, key):
126134
# type: (str, str) -> None
127135
if not isinstance(name, six.string_types) or not isinstance(key, six.string_types):

sdk/core/azure-core/azure/core/credentials_async.py

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,27 @@
55
from typing import TYPE_CHECKING
66

77
if TYPE_CHECKING:
8-
from typing import Any
8+
from typing import Any, Optional
99
from typing_extensions import Protocol
1010
from .credentials import AccessToken
1111

1212
class AsyncTokenCredential(Protocol):
13-
async def get_token(self, *scopes: str, **kwargs: Any) -> AccessToken:
14-
pass
13+
"""Protocol for classes able to provide OAuth tokens."""
14+
15+
async def get_token(
16+
self, *scopes: str, claims: Optional[str] = None, tenant_id: Optional[str] = None, **kwargs: Any
17+
) -> AccessToken:
18+
"""Request an access token for `scopes`.
19+
20+
:param str scopes: The type of access needed.
21+
22+
:keyword str claims: Additional claims required in the token, such as those returned in a resource
23+
provider's claims challenge following an authorization failure.
24+
:keyword str tenant_id: Optional tenant to include in the token request.
25+
26+
:rtype: AccessToken
27+
:return: An AccessToken instance containing the token string and its expiration time in Unix time.
28+
"""
1529

1630
async def close(self) -> None:
1731
pass

0 commit comments

Comments
 (0)