diff --git a/sdk/identity/Azure.Identity/CHANGELOG.md b/sdk/identity/Azure.Identity/CHANGELOG.md index 6b51a911ec91..8b2196528557 100644 --- a/sdk/identity/Azure.Identity/CHANGELOG.md +++ b/sdk/identity/Azure.Identity/CHANGELOG.md @@ -1,7 +1,13 @@ # Release History +## 1.4.0-beta.2 (Unreleased) -## 1.4.0-beta.1 (Unreleased) +## 1.4.0-beta.1 (2020-10-15) + +### New Features +- Redesigned Application Authentication APIs + - Adds `TokenCache` and `PersistentTokenCache` classes to give more user control over how the tokens are cached and how the cache is persisted. + - Adds `TokenCache` property to options for credentials supporting token cache configuration. ## 1.3.0 (2020-11-12) diff --git a/sdk/identity/Azure.Identity/api/Azure.Identity.netstandard2.0.cs b/sdk/identity/Azure.Identity/api/Azure.Identity.netstandard2.0.cs index 47c3b31eea78..b00f08cdc215 100644 --- a/sdk/identity/Azure.Identity/api/Azure.Identity.netstandard2.0.cs +++ b/sdk/identity/Azure.Identity/api/Azure.Identity.netstandard2.0.cs @@ -5,6 +5,25 @@ public partial class AuthenticationFailedException : System.Exception public AuthenticationFailedException(string message) { } public AuthenticationFailedException(string message, System.Exception innerException) { } } + public partial class AuthenticationRecord + { + internal AuthenticationRecord() { } + public string Authority { get { throw null; } } + public string ClientId { get { throw null; } } + public string HomeAccountId { get { throw null; } } + public string TenantId { get { throw null; } } + public string Username { get { throw null; } } + public static Azure.Identity.AuthenticationRecord Deserialize(System.IO.Stream stream, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } + public static System.Threading.Tasks.Task DeserializeAsync(System.IO.Stream stream, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } + public void Serialize(System.IO.Stream stream, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { } + public System.Threading.Tasks.Task SerializeAsync(System.IO.Stream stream, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } + } + public partial class AuthenticationRequiredException : Azure.Identity.CredentialUnavailableException + { + public AuthenticationRequiredException(string message, Azure.Core.TokenRequestContext context) : base (default(string)) { } + public AuthenticationRequiredException(string message, Azure.Core.TokenRequestContext context, System.Exception innerException) : base (default(string)) { } + public Azure.Core.TokenRequestContext TokenRequestContext { get { throw null; } } + } public partial class AuthorizationCodeCredential : Azure.Core.TokenCredential { protected AuthorizationCodeCredential() { } @@ -48,15 +67,22 @@ public partial class ClientCertificateCredentialOptions : Azure.Identity.TokenCr { public ClientCertificateCredentialOptions() { } public bool SendCertificateChain { get { throw null; } set { } } + public Azure.Identity.TokenCache TokenCache { get { throw null; } set { } } } public partial class ClientSecretCredential : Azure.Core.TokenCredential { protected ClientSecretCredential() { } public ClientSecretCredential(string tenantId, string clientId, string clientSecret) { } + public ClientSecretCredential(string tenantId, string clientId, string clientSecret, Azure.Identity.ClientSecretCredentialOptions options) { } public ClientSecretCredential(string tenantId, string clientId, string clientSecret, Azure.Identity.TokenCredentialOptions options) { } public override Azure.Core.AccessToken GetToken(Azure.Core.TokenRequestContext requestContext, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } public override System.Threading.Tasks.ValueTask GetTokenAsync(Azure.Core.TokenRequestContext requestContext, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } } + public partial class ClientSecretCredentialOptions : Azure.Identity.TokenCredentialOptions + { + public ClientSecretCredentialOptions() { } + public Azure.Identity.TokenCache TokenCache { get { throw null; } set { } } + } public partial class CredentialUnavailableException : Azure.Identity.AuthenticationFailedException { public CredentialUnavailableException(string message) : base (default(string)) { } @@ -94,15 +120,22 @@ public DeviceCodeCredential(Azure.Identity.DeviceCodeCredentialOptions options) public DeviceCodeCredential(System.Func deviceCodeCallback, string clientId, Azure.Identity.TokenCredentialOptions options = null) { } [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] public DeviceCodeCredential(System.Func deviceCodeCallback, string tenantId, string clientId, Azure.Identity.TokenCredentialOptions options = null) { } + public virtual Azure.Identity.AuthenticationRecord Authenticate(Azure.Core.TokenRequestContext requestContext, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } + public virtual Azure.Identity.AuthenticationRecord Authenticate(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } + public virtual System.Threading.Tasks.Task AuthenticateAsync(Azure.Core.TokenRequestContext requestContext, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } + public virtual System.Threading.Tasks.Task AuthenticateAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } public override Azure.Core.AccessToken GetToken(Azure.Core.TokenRequestContext requestContext, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } public override System.Threading.Tasks.ValueTask GetTokenAsync(Azure.Core.TokenRequestContext requestContext, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } } public partial class DeviceCodeCredentialOptions : Azure.Identity.TokenCredentialOptions { public DeviceCodeCredentialOptions() { } + public Azure.Identity.AuthenticationRecord AuthenticationRecord { get { throw null; } set { } } public string ClientId { get { throw null; } set { } } public System.Func DeviceCodeCallback { get { throw null; } set { } } + public bool DisableAutomaticAuthentication { get { throw null; } set { } } public string TenantId { get { throw null; } set { } } + public Azure.Identity.TokenCache TokenCache { get { throw null; } set { } } } [System.Runtime.InteropServices.StructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Sequential)] public partial struct DeviceCodeInfo @@ -126,6 +159,7 @@ public EnvironmentCredential(Azure.Identity.TokenCredentialOptions options) { } } public static partial class IdentityModelFactory { + public static Azure.Identity.AuthenticationRecord AuthenticationRecord(string username, string authority, string homeAccountId, string tenantId, string clientId) { throw null; } public static Azure.Identity.DeviceCodeInfo DeviceCodeInfo(string userCode, string deviceCode, System.Uri verificationUri, System.DateTimeOffset expiresOn, string message, string clientId, System.Collections.Generic.IReadOnlyCollection scopes) { throw null; } } public partial class InteractiveBrowserCredential : Azure.Core.TokenCredential @@ -136,15 +170,22 @@ public InteractiveBrowserCredential(Azure.Identity.InteractiveBrowserCredentialO public InteractiveBrowserCredential(string clientId) { } [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)] public InteractiveBrowserCredential(string tenantId, string clientId, Azure.Identity.TokenCredentialOptions options = null) { } + public virtual Azure.Identity.AuthenticationRecord Authenticate(Azure.Core.TokenRequestContext requestContext, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } + public virtual Azure.Identity.AuthenticationRecord Authenticate(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } + public virtual System.Threading.Tasks.Task AuthenticateAsync(Azure.Core.TokenRequestContext requestContext, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } + public virtual System.Threading.Tasks.Task AuthenticateAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } public override Azure.Core.AccessToken GetToken(Azure.Core.TokenRequestContext requestContext, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } public override System.Threading.Tasks.ValueTask GetTokenAsync(Azure.Core.TokenRequestContext requestContext, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } } public partial class InteractiveBrowserCredentialOptions : Azure.Identity.TokenCredentialOptions { public InteractiveBrowserCredentialOptions() { } + public Azure.Identity.AuthenticationRecord AuthenticationRecord { get { throw null; } set { } } public string ClientId { get { throw null; } set { } } + public bool DisableAutomaticAuthentication { get { throw null; } set { } } public System.Uri RedirectUri { get { throw null; } set { } } public string TenantId { get { throw null; } set { } } + public Azure.Identity.TokenCache TokenCache { get { throw null; } set { } } } public partial class ManagedIdentityCredential : Azure.Core.TokenCredential { @@ -153,6 +194,17 @@ public ManagedIdentityCredential(string clientId = null, Azure.Identity.TokenCre public override Azure.Core.AccessToken GetToken(Azure.Core.TokenRequestContext requestContext, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } public override System.Threading.Tasks.ValueTask GetTokenAsync(Azure.Core.TokenRequestContext requestContext, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } } + public partial class PersistentTokenCache : Azure.Identity.TokenCache + { + public PersistentTokenCache(Azure.Identity.PersistentTokenCacheOptions options) { } + public PersistentTokenCache(bool allowUnencryptedStorage = true) { } + } + public partial class PersistentTokenCacheOptions + { + public PersistentTokenCacheOptions() { } + public bool AllowUnencryptedStorage { get { throw null; } set { } } + public string Name { get { throw null; } set { } } + } public partial class SharedTokenCacheCredential : Azure.Core.TokenCredential { public SharedTokenCacheCredential() { } @@ -165,9 +217,30 @@ public SharedTokenCacheCredential(string username, Azure.Identity.TokenCredentia public partial class SharedTokenCacheCredentialOptions : Azure.Identity.TokenCredentialOptions { public SharedTokenCacheCredentialOptions() { } + public SharedTokenCacheCredentialOptions(Azure.Identity.TokenCache tokenCache) { } + public Azure.Identity.AuthenticationRecord AuthenticationRecord { get { throw null; } set { } } + public string ClientId { get { throw null; } set { } } + public bool EnableGuestTenantAuthentication { get { throw null; } set { } } public string TenantId { get { throw null; } set { } } + public Azure.Identity.TokenCache TokenCache { get { throw null; } } public string Username { get { throw null; } set { } } } + public partial class TokenCache : System.IDisposable + { + public TokenCache() { } + public event System.Func Updated { add { } remove { } } + public static Azure.Identity.TokenCache Deserialize(System.IO.Stream stream, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } + public static System.Threading.Tasks.Task DeserializeAsync(System.IO.Stream stream, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } + public void Dispose() { } + protected virtual void Dispose(bool disposing) { } + public void Serialize(System.IO.Stream stream, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { } + public System.Threading.Tasks.Task SerializeAsync(System.IO.Stream stream, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } + } + public partial class TokenCacheUpdatedArgs + { + internal TokenCacheUpdatedArgs() { } + public Azure.Identity.TokenCache Cache { get { throw null; } } + } public partial class TokenCredentialOptions : Azure.Core.ClientOptions { public TokenCredentialOptions() { } @@ -178,9 +251,19 @@ public partial class UsernamePasswordCredential : Azure.Core.TokenCredential protected UsernamePasswordCredential() { } public UsernamePasswordCredential(string username, string password, string tenantId, string clientId) { } public UsernamePasswordCredential(string username, string password, string tenantId, string clientId, Azure.Identity.TokenCredentialOptions options) { } + public UsernamePasswordCredential(string username, string password, string tenantId, string clientId, Azure.Identity.UsernamePasswordCredentialOptions options) { } + public virtual Azure.Identity.AuthenticationRecord Authenticate(Azure.Core.TokenRequestContext requestContext, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } + public virtual Azure.Identity.AuthenticationRecord Authenticate(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } + public virtual System.Threading.Tasks.Task AuthenticateAsync(Azure.Core.TokenRequestContext requestContext, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } + public virtual System.Threading.Tasks.Task AuthenticateAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } public override Azure.Core.AccessToken GetToken(Azure.Core.TokenRequestContext requestContext, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } public override System.Threading.Tasks.ValueTask GetTokenAsync(Azure.Core.TokenRequestContext requestContext, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } } + public partial class UsernamePasswordCredentialOptions : Azure.Identity.TokenCredentialOptions + { + public UsernamePasswordCredentialOptions() { } + public Azure.Identity.TokenCache TokenCache { get { throw null; } set { } } + } public partial class VisualStudioCodeCredential : Azure.Core.TokenCredential { public VisualStudioCodeCredential() { } diff --git a/sdk/identity/Azure.Identity/samples/ClientSideUserAuthentication.md b/sdk/identity/Azure.Identity/samples/ClientSideUserAuthentication.md new file mode 100644 index 000000000000..4059a091a909 --- /dev/null +++ b/sdk/identity/Azure.Identity/samples/ClientSideUserAuthentication.md @@ -0,0 +1,105 @@ +# Client side user authentication + +Client side applications often need to authenticate users to interact with resources in Azure. Some examples of this might be a command line tool which fetches secrets a user has access to from a key vault to setup a local test environment, or a GUI based application which allows a user to browse storage blobs they have access to. This sample demonstrates authenticating users with the `Azure.Identity` library. + +## Interactive user authentication + +Most often authenticating users requires some user interaction. Properly handling this user interaction for OAuth2 authorization code or device code authentication can be challenging. To simplify this for client side applications the `Azure.Identity` library provides the `InteractiveBrowserCredential` and the `DeviceCodeCredential`. These credentials are designed to handle the user interactions needed to authenticate via these two client side authentication flows, so the application developer can simply create the credential and authenticate clients with it. + + +## Authenticating users with the InteractiveBrowserCredential + +For clients which have a default browser available, the `InteractiveBrowserCredential` provides the most simple user authentication experience. In the sample below an application authenticates a `SecretClient` using the `InteractiveBrowserCredential`. + +```C# Snippet:Identity_ClientSideUserAuthentication_SimpleInteractiveBrowser +var client = new SecretClient(new Uri("https://myvault.azure.vaults.net/"), new InteractiveBrowserCredential()); +``` +As code uses the `SecretClient` in the above sample, the `InteractiveBrowserCredential` will automatically authenticate the user by launching the default system browser prompting the user to login. In this case the user interaction happens on demand as is necessary to authenticate calls from the client. + + +## Authenticating users with the DeviceCodeCredential + +For terminal clients without an available web browser, or clients with limited UI capabilities the `DeviceCodeCredential` provides the ability to authenticate any client using a device code. The next sample shows authenticating a `BlobClient` using the `DeviceCodeCredential`. + +```C# Snippet:Identity_ClientSideUserAuthentication_SimpleDeviceCode +var credential = new DeviceCodeCredential(); + +var client = new BlobClient(new Uri("https://myaccount.blob.core.windows.net/mycontainer/myblob"), credential); +``` +Similarly to the `InteractiveBrowserCredential` the `DeviceCodeCredential` will also initiate the user interaction automatically as needed. To instantiate the `DeviceCodeCredential` the application must provide a callback which is called to display the device code along with details on how to authenticate to the user. In the above sample a lambda is provided which prints the full device code message to the console. + + +## Controlling user interaction + +In many cases applications require tight control over user interaction. In these applications automatically blocking on required user interaction is often undesired or impractical. For this reason, credentials in the `Azure.Identity` library which interact with the user offer mechanisms to fully control user interaction. + +```C# Snippet:Identity_ClientSideUserAuthentication_DisableAutomaticAuthentication +var credential = new InteractiveBrowserCredential(new InteractiveBrowserCredentialOptions { DisableAutomaticAuthentication = true }); + +await credential.AuthenticateAsync(); + +var client = new SecretClient(new Uri("https://myvault.azure.vaults.net/"), credential); +``` +In this sample the application is again using the `InteractiveBrowserCredential` to authenticate a `SecretClient`, but with two major differences from our first example. First, in this example the application is explicitly forcing any user interaction to happen before the credential is given to the client by calling `AuthenticateAsync`. + +The second difference is here the application is preventing the credential from automatically initiating user interaction. Even though the application authenticates the user before the credential is used, further interaction might still be needed, for instance in the case that the user's refresh token expires, or a specific method require additional consent or authentication. + +By setting the option `DisableAutomaticAuthentication` to `true` the credential will fail to automatically authenticate calls where user interaction is necessary. Instead, the credential will throw an `AuthenticationRequiredException`. The following example demonstrates an application handling such an exception to prompt the user to authenticate only after some application logic has completed. + +```C# Snippet:Identity_ClientSideUserAuthentication_DisableAutomaticAuthentication_ExHandling +try +{ + client.GetSecret("secret"); +} +catch (AuthenticationRequiredException e) +{ + await EnsureAnimationCompleteAsync(); + + await credential.AuthenticateAsync(e.TokenRequestContext); + + client.GetSecret("secret"); +} +``` + +## Persisting user authentication data + +Quite often applications desire the ability to be run multiple times without having to reauthenticate the user on each execution. This requires that data from the original authentication be persisted outside of the application memory, so that it can authenticate silently on subsequent executions. Specifically two pieces of data need to be persisted, the `TokenCache` and the `AuthenticationRecord`. + +### Persisting the TokenCache + +The `TokenCache` contains all the data needed to silently authenticate, one or many accounts. It contains sensitive data such as refresh tokens, and access tokens and must be protected to prevent compromising the accounts it houses tokens for. The `Azure.Identity` library provides the `PersistentTokenCache` class which by default will protect and persist the cache using available platform data protection. + +To use the `PersistentTokenCache` to persist the cache of any credential simply set the `TokenCache` option. + +```C# Snippet:Identity_ClientSideUserAuthentication_Persist_TokenCache +var credential = new InteractiveBrowserCredential(new InteractiveBrowserCredentialOptions { TokenCache = new PersistentTokenCache() }); +``` + +### Persisting the AuthenticationRecord + +The `AuthenticationRecord` which is returned from the `Authenticate` and `AuthenticateAsync`, contains data identifying an authenticated account. It is needed to identify the appropriate entry in the `TokenCache` to silently authenticate on subsequent executions. There is no sensitive data in the `AuthenticationRecord` so it can be persisted in a non-protected state. + +Here is an example of an application storing the `AuthenticationRecord` to the local file system after authenticating the user. + +```C# Snippet:Identity_ClientSideUserAuthentication_Persist_AuthRecord +AuthenticationRecord authRecord = await credential.AuthenticateAsync(); + +using var authRecordStream = new FileStream(AUTH_RECORD_PATH, FileMode.Create, FileAccess.Write); + +await authRecord.SerializeAsync(authRecordStream); + +await authRecordStream.FlushAsync(); +``` +### Silent authentication with AuthenticationRecord and PersistentTokenCache + +Once an application has persisted both the `TokenCache` and the `AuthenticationRecord` this data can be used to silently authenticate. This example demonstrates an application using the `PersistentTokenCache` and retrieving an `AuthenticationRecord` from the local file system to create an `InteractiveBrowserCredential` capable of silent authentication. + +```C# Snippet:Identity_ClientSideUserAuthentication_Persist_SilentAuth +using var authRecordStream = new FileStream(AUTH_RECORD_PATH, FileMode.Open, FileAccess.Read); + +AuthenticationRecord authRecord = await AuthenticationRecord.DeserializeAsync(authRecordStream); + +var credential = new InteractiveBrowserCredential(new InteractiveBrowserCredentialOptions { TokenCache = new PersistentTokenCache(), AuthenticationRecord = authRecord }); +``` + +The credential created in this example will silently authenticate given that a valid token for corresponding to the `AuthenticationRecord` still exists in the `TokenCache`. There are some cases where interaction will still be required such as on token expiry, or when additional authentication is required for a particular resource. diff --git a/sdk/identity/Azure.Identity/samples/TokenCache.md b/sdk/identity/Azure.Identity/samples/TokenCache.md new file mode 100644 index 000000000000..6741aada3d2c --- /dev/null +++ b/sdk/identity/Azure.Identity/samples/TokenCache.md @@ -0,0 +1,81 @@ +# Persisting the credential TokenCache +Many credential implementations in the Azure.Identity library have an underlying `TokenCache` which caches sensitive authentication data such as account information, access tokens, and refresh tokens. By default this `TokenCache` instance is an in memory cache which is specific to the credential instance. However, there are scenarios where an application needs to share the token cache across credentials, and persist it across executions. To accomplish this the Azure.Identity provides the `TokenCache` and `PeristantTokenCache` classes. + +>IMPORTANT! The `TokenCache` contains sensitive data and **MUST** be protected to prevent compromising accounts. All application decisions regarding the storage of the `TokenCache` must consider that a breach of its content will fully compromise all the accounts it contains. + +## Using the default PersistentTokenCache + +The simplest way to persist the `TokenCache` of a credential is to to use the default `PersistentTokenCache`. This will persist and read the `TokenCache` from a shared persisted token cache protected to the current account. + +```C# Snippet:Identity_TokenCache_PersistentDefault +var credential = new InteractiveBrowserCredential(new InteractiveBrowserCredentialOptions { TokenCache = new PersistentTokenCache() }); +``` + +## Using a named PersistentTokenCache + +Some applications may prefer to isolate the `PersistentTokenCache` they user rather than using the shared instance. To accomplish this they can specify a `PersistentTokenCacheOptions` when creating the `PersistentTokenCache` and provide a `Name` for the persisted cache instance. + +```C# Snippet:Identity_TokenCache_PersistentNamed +var tokenCache = new PersistentTokenCache(new PersistentTokenCacheOptions { Name = "my_application_name" }); + +var credential = new InteractiveBrowserCredential(new InteractiveBrowserCredentialOptions { TokenCache = tokenCache }); +``` + +## Allowing unencrypted storage +By default the `PersistentTokenCache` will protect any data which is persisted using the user data protection APIs available on the current platform. However, there are cases where no data protection is available, and applications may choose to still persist the token cache in an unencrypted state. This is accomplished with the `AllowUnencryptedStorage` option. + +```C# Snippet:Identity_TokenCache_PersistentUnencrypted +var tokenCache = new PersistentTokenCache(new PersistentTokenCacheOptions { AllowUnencryptedStorage = true }); + +var credential = new InteractiveBrowserCredential(new InteractiveBrowserCredentialOptions { TokenCache = tokenCache}); +``` +By setting `AllowUnencryptedStorage` to `true`, the `PersistentTokenCache` will encrypt the contents of the `TokenCache` before persisting it if data protection is available on the current platform, otherwise it will write and read the `TokenCache` data to an unencrypted local file ACL'd to the current account. If `AllowUnencryptedStorage` is `false` (the default) a `CredentialUnavailableException` will be raised in the case no data protection is available. + +## Implementing custom TokenCache persistence +Some applications may require complete control of how the `TokenCache` is persisted. To enable this the `TokenCache` provides the methods `Serialize`, `SerializeAsync`, `Deserialize` and `DeserializeAsync` methods so applications can write the `TokenCache` to any stream. The following samples illustrate how to use these serialization methods to write and read the cache from a stream. + +> IMPORTANT! This sample assumes the location of the file it is using for storage is secure. The `Serialize` and `SerializeAsync` methods will write the unencrypted content of the `TokenCache` to the provide stream. It is the responsibility the implementer to properly protect the `TokenCache` data. + +The `Serialize` or `SerializeAsync` methods can be used to write out content of a `TokenCache` to any writeable stream. + +```C# Snippet:Identity_TokenCache_CustomPersistence_Write +using var cacheStream = new FileStream(TokenCachePath, FileMode.Create, FileAccess.Write); + +await tokenCache.SerializeAsync(cacheStream); +``` + +The `Deserialize` or `DeserializeAsync` methods can be used to read the content of a `TokenCache` from any readable stream. + +```C# Snippet:Identity_TokenCache_CustomPersistence_Read +using var cacheStream = new FileStream(TokenCachePath, FileMode.OpenOrCreate, FileAccess.Read); + +var tokenCache = await TokenCache.DeserializeAsync(cacheStream); +``` + +Applications can combine these methods along with the `Updated` event to automatically persist and read the token from a storage solution of their choice. +```C# Snippet:Identity_TokenCache_CustomPersistence_Usage +public static async Task ReadTokenCacheAsync() +{ + using var cacheStream = new FileStream(TokenCachePath, FileMode.OpenOrCreate, FileAccess.Read); + + var tokenCache = await TokenCache.DeserializeAsync(cacheStream); + + tokenCache.Updated += WriteCacheOnUpdateAsync; + + return tokenCache; +} + +public static async Task WriteCacheOnUpdateAsync(TokenCacheUpdatedArgs args) +{ + using var cacheStream = new FileStream(TokenCachePath, FileMode.Create, FileAccess.Write); + + await args.Cache.SerializeAsync(cacheStream); +} + +public static async Task Main() +{ + var tokenCache = await ReadTokenCacheAsync(); + + var credential = new InteractiveBrowserCredential(new InteractiveBrowserCredentialOptions { TokenCache = tokenCache }); +} +``` \ No newline at end of file diff --git a/sdk/identity/Azure.Identity/src/AuthenticationRecord.cs b/sdk/identity/Azure.Identity/src/AuthenticationRecord.cs index b6ddea4cabb7..c74a76b1aae5 100644 --- a/sdk/identity/Azure.Identity/src/AuthenticationRecord.cs +++ b/sdk/identity/Azure.Identity/src/AuthenticationRecord.cs @@ -15,7 +15,7 @@ namespace Azure.Identity /// /// Account information relating to an authentication request. /// - internal class AuthenticationRecord + public class AuthenticationRecord { private const string UsernamePropertyName = "username"; private const string AuthorityPropertyName = "authority"; diff --git a/sdk/identity/Azure.Identity/src/AuthenticationRequiredException.cs b/sdk/identity/Azure.Identity/src/AuthenticationRequiredException.cs index df539fa97513..7367809bbad0 100644 --- a/sdk/identity/Azure.Identity/src/AuthenticationRequiredException.cs +++ b/sdk/identity/Azure.Identity/src/AuthenticationRequiredException.cs @@ -9,7 +9,7 @@ namespace Azure.Identity /// /// An exception indicating that interactive authentication is required. /// - internal class AuthenticationRequiredException : CredentialUnavailableException + public class AuthenticationRequiredException : CredentialUnavailableException { /// /// Creates a new with the specified message and context. diff --git a/sdk/identity/Azure.Identity/src/Azure.Identity.csproj b/sdk/identity/Azure.Identity/src/Azure.Identity.csproj index 43528f24385b..d6b9783b0e65 100644 --- a/sdk/identity/Azure.Identity/src/Azure.Identity.csproj +++ b/sdk/identity/Azure.Identity/src/Azure.Identity.csproj @@ -2,7 +2,7 @@ This is the implementation of the Azure SDK Client Library for Azure Identity Microsoft Azure.Identity Component - 1.4.0-beta.1 + 1.4.0-beta.2 1.3.0 Microsoft Azure Identity;$(PackageCommonTags) $(RequiredTargetFrameworks) diff --git a/sdk/identity/Azure.Identity/src/ClientCertificateCredentialOptions.cs b/sdk/identity/Azure.Identity/src/ClientCertificateCredentialOptions.cs index fa9b3f4baf08..f36272d6840d 100644 --- a/sdk/identity/Azure.Identity/src/ClientCertificateCredentialOptions.cs +++ b/sdk/identity/Azure.Identity/src/ClientCertificateCredentialOptions.cs @@ -8,24 +8,14 @@ namespace Azure.Identity /// public class ClientCertificateCredentialOptions : TokenCredentialOptions, ITokenCacheOptions { - - /// - /// If set to true the credential will store tokens in a cache persisted to the machine, protected to the current user, which can be shared by other credentials and processes. - /// - internal bool EnablePersistentCache { get; set; } - /// - /// If set to true the credential will fall back to storing tokens in an unencrypted file if no OS level user encryption is available. + /// Specifies the to be used by the credential. /// - internal bool AllowUnencryptedCache { get; set; } + public TokenCache TokenCache { get; set; } /// /// Will include x5c header in client claims when acquiring a token to enable subject name / issuer based authentication for the . /// public bool SendCertificateChain { get; set; } - - bool ITokenCacheOptions.EnablePersistentCache => EnablePersistentCache; - - bool ITokenCacheOptions.AllowUnencryptedCache => AllowUnencryptedCache; } } diff --git a/sdk/identity/Azure.Identity/src/ClientSecretCredential.cs b/sdk/identity/Azure.Identity/src/ClientSecretCredential.cs index 7afc6b0a2550..05d342f7dfef 100644 --- a/sdk/identity/Azure.Identity/src/ClientSecretCredential.cs +++ b/sdk/identity/Azure.Identity/src/ClientSecretCredential.cs @@ -60,7 +60,7 @@ public ClientSecretCredential(string tenantId, string clientId, string clientSec /// The client (application) ID of the service principal /// A client secret that was generated for the App Registration used to authenticate the client. /// Options that allow to configure the management of the requests sent to the Azure Active Directory service. - internal ClientSecretCredential(string tenantId, string clientId, string clientSecret, ClientSecretCredentialOptions options) + public ClientSecretCredential(string tenantId, string clientId, string clientSecret, ClientSecretCredentialOptions options) : this(tenantId, clientId, clientSecret, options, null, null) { } diff --git a/sdk/identity/Azure.Identity/src/ClientSecretCredentialOptions.cs b/sdk/identity/Azure.Identity/src/ClientSecretCredentialOptions.cs index 63256bed76df..1780e03fd820 100644 --- a/sdk/identity/Azure.Identity/src/ClientSecretCredentialOptions.cs +++ b/sdk/identity/Azure.Identity/src/ClientSecretCredentialOptions.cs @@ -6,17 +6,12 @@ namespace Azure.Identity /// /// Options used to configure the . /// - internal class ClientSecretCredentialOptions : TokenCredentialOptions, ITokenCacheOptions + public class ClientSecretCredentialOptions : TokenCredentialOptions, ITokenCacheOptions { - /// - /// If set to true the credential will store tokens in a persistent cache shared by other credentials. + /// Specifies the to be used by the credential. /// - public bool EnablePersistentCache { get; set; } + public TokenCache TokenCache { get; set; } - /// - /// If set to true the credential will fall back to storing tokens in an unencrypted file if no OS level user encryption is available. - /// - public bool AllowUnencryptedCache { get; set; } } } diff --git a/sdk/identity/Azure.Identity/src/DefaultAzureCredentialFactory.cs b/sdk/identity/Azure.Identity/src/DefaultAzureCredentialFactory.cs index c7cb34405238..a72706c0ebc0 100644 --- a/sdk/identity/Azure.Identity/src/DefaultAzureCredentialFactory.cs +++ b/sdk/identity/Azure.Identity/src/DefaultAzureCredentialFactory.cs @@ -34,7 +34,7 @@ public virtual TokenCredential CreateSharedTokenCacheCredential(string tenantId, public virtual TokenCredential CreateInteractiveBrowserCredential(string tenantId) { - return new InteractiveBrowserCredential(tenantId, Constants.DeveloperSignOnClientId, new InteractiveBrowserCredentialOptions { EnablePersistentCache = true }, Pipeline); + return new InteractiveBrowserCredential(tenantId, Constants.DeveloperSignOnClientId, new InteractiveBrowserCredentialOptions { TokenCache = new PersistentTokenCache() }, Pipeline); } public virtual TokenCredential CreateAzureCliCredential() diff --git a/sdk/identity/Azure.Identity/src/DeviceCodeCredential.cs b/sdk/identity/Azure.Identity/src/DeviceCodeCredential.cs index a62008e4a63f..2c229d71d213 100644 --- a/sdk/identity/Azure.Identity/src/DeviceCodeCredential.cs +++ b/sdk/identity/Azure.Identity/src/DeviceCodeCredential.cs @@ -96,7 +96,7 @@ internal DeviceCodeCredential(Func devi /// /// A controlling the request lifetime. /// The result of the authentication request, containing the acquired , and the which can be used to silently authenticate the account. - internal virtual AuthenticationRecord Authenticate(CancellationToken cancellationToken = default) + public virtual AuthenticationRecord Authenticate(CancellationToken cancellationToken = default) { // get the default scope for the authority, throw if no default scope exists string defaultScope = AzureAuthorityHosts.GetDefaultScope(Pipeline.AuthorityHost) ?? throw new CredentialUnavailableException(NoDefaultScopeMessage); @@ -108,8 +108,8 @@ internal virtual AuthenticationRecord Authenticate(CancellationToken cancellatio /// Interactively authenticates a user via the default browser. /// /// A controlling the request lifetime. - /// The which can be used to silently authenticate the account on future execution if persistent caching was enabled via when credential was instantiated. - internal virtual async Task AuthenticateAsync(CancellationToken cancellationToken = default) + /// The which can be used to silently authenticate the account on future execution of credentials using the same . + public virtual async Task AuthenticateAsync(CancellationToken cancellationToken = default) { // get the default scope for the authority, throw if no default scope exists string defaultScope = AzureAuthorityHosts.GetDefaultScope(Pipeline.AuthorityHost) ?? throw new CredentialUnavailableException(NoDefaultScopeMessage); @@ -123,7 +123,7 @@ internal virtual async Task AuthenticateAsync(Cancellation /// A controlling the request lifetime. /// The details of the authentication request. /// The of the authenticated account. - internal virtual AuthenticationRecord Authenticate(TokenRequestContext requestContext, CancellationToken cancellationToken = default) + public virtual AuthenticationRecord Authenticate(TokenRequestContext requestContext, CancellationToken cancellationToken = default) { return AuthenticateImplAsync(false, requestContext, cancellationToken).EnsureCompleted(); } @@ -134,7 +134,7 @@ internal virtual AuthenticationRecord Authenticate(TokenRequestContext requestCo /// A controlling the request lifetime. /// The details of the authentication request. /// The of the authenticated account. - internal virtual async Task AuthenticateAsync(TokenRequestContext requestContext, CancellationToken cancellationToken = default) + public virtual async Task AuthenticateAsync(TokenRequestContext requestContext, CancellationToken cancellationToken = default) { return await AuthenticateImplAsync(true, requestContext, cancellationToken).ConfigureAwait(false); } diff --git a/sdk/identity/Azure.Identity/src/DeviceCodeCredentialOptions.cs b/sdk/identity/Azure.Identity/src/DeviceCodeCredentialOptions.cs index b18ad0883848..79eede0e9e33 100644 --- a/sdk/identity/Azure.Identity/src/DeviceCodeCredentialOptions.cs +++ b/sdk/identity/Azure.Identity/src/DeviceCodeCredentialOptions.cs @@ -18,7 +18,7 @@ public class DeviceCodeCredentialOptions : TokenCredentialOptions, ITokenCacheOp /// Prevents the from automatically prompting the user. If automatic authentication is disabled a AuthenticationRequiredException will be thrown from and in the case that /// user interaction is necessary. The application is responsible for handling this exception, and calling or to authenticate the user interactively. /// - internal bool DisableAutomaticAuthentication { get; set; } + public bool DisableAutomaticAuthentication { get; set; } /// /// The tenant ID the user will be authenticated to. If not specified the user will be authenticated to their home tenant. @@ -35,27 +35,18 @@ public string TenantId public string ClientId { get; set; } = Constants.DeveloperSignOnClientId; /// - /// If set to true the credential will store tokens in a cache persisted to the machine, protected to the current user, which can be shared by other credentials and processes. + /// Specifies the to be used by the credential. /// - internal bool EnablePersistentCache { get; set; } - - /// - /// If set to true the credential will fall back to storing tokens in an unencrypted file if no OS level user encryption is available. - /// - internal bool AllowUnencryptedCache { get; set; } + public TokenCache TokenCache { get; set; } /// /// The captured from a previous authentication. /// - internal AuthenticationRecord AuthenticationRecord { get; set; } + public AuthenticationRecord AuthenticationRecord { get; set; } /// /// The callback which will be executed to display the device code login details to the user. In not specified the device code and login instructions will be printed to the console. /// public Func DeviceCodeCallback { get; set; } - - bool ITokenCacheOptions.EnablePersistentCache => EnablePersistentCache; - - bool ITokenCacheOptions.AllowUnencryptedCache => AllowUnencryptedCache; } } diff --git a/sdk/identity/Azure.Identity/src/ITokenCacheOptions.cs b/sdk/identity/Azure.Identity/src/ITokenCacheOptions.cs index b6bab41a1ee9..2de22c734195 100644 --- a/sdk/identity/Azure.Identity/src/ITokenCacheOptions.cs +++ b/sdk/identity/Azure.Identity/src/ITokenCacheOptions.cs @@ -9,8 +9,6 @@ namespace Azure.Identity { internal interface ITokenCacheOptions { - bool EnablePersistentCache { get; } - - bool AllowUnencryptedCache { get; } + TokenCache TokenCache { get; } } } diff --git a/sdk/identity/Azure.Identity/src/IdentityModelFactory.cs b/sdk/identity/Azure.Identity/src/IdentityModelFactory.cs index 3f97279e701e..961ff8b74073 100644 --- a/sdk/identity/Azure.Identity/src/IdentityModelFactory.cs +++ b/sdk/identity/Azure.Identity/src/IdentityModelFactory.cs @@ -21,7 +21,7 @@ public static class IdentityModelFactory /// Sets the . /// Sets the . /// A new instance of the for mocking purposes. - internal static AuthenticationRecord AuthenticationRecord(string username, string authority, string homeAccountId, string tenantId, string clientId) + public static AuthenticationRecord AuthenticationRecord(string username, string authority, string homeAccountId, string tenantId, string clientId) => new AuthenticationRecord(username, authority, homeAccountId, tenantId, clientId); /// diff --git a/sdk/identity/Azure.Identity/src/InteractiveBrowserCredential.cs b/sdk/identity/Azure.Identity/src/InteractiveBrowserCredential.cs index f535d2a0c434..b12cc434dd1a 100644 --- a/sdk/identity/Azure.Identity/src/InteractiveBrowserCredential.cs +++ b/sdk/identity/Azure.Identity/src/InteractiveBrowserCredential.cs @@ -91,7 +91,7 @@ internal InteractiveBrowserCredential(string tenantId, string clientId, TokenCre /// /// A controlling the request lifetime. /// The result of the authentication request, containing the acquired , and the which can be used to silently authenticate the account. - internal virtual AuthenticationRecord Authenticate(CancellationToken cancellationToken = default) + public virtual AuthenticationRecord Authenticate(CancellationToken cancellationToken = default) { // get the default scope for the authority, throw if no default scope exists string defaultScope = AzureAuthorityHosts.GetDefaultScope(Pipeline.AuthorityHost) ?? throw new CredentialUnavailableException(NoDefaultScopeMessage); @@ -104,7 +104,7 @@ internal virtual AuthenticationRecord Authenticate(CancellationToken cancellatio /// /// A controlling the request lifetime. /// The result of the authentication request, containing the acquired , and the which can be used to silently authenticate the account. - internal virtual async Task AuthenticateAsync(CancellationToken cancellationToken = default) + public virtual async Task AuthenticateAsync(CancellationToken cancellationToken = default) { // get the default scope for the authority, throw if no default scope exists string defaultScope = AzureAuthorityHosts.GetDefaultScope(Pipeline.AuthorityHost) ?? throw new CredentialUnavailableException(NoDefaultScopeMessage); @@ -118,7 +118,7 @@ internal virtual async Task AuthenticateAsync(Cancellation /// A controlling the request lifetime. /// The details of the authentication request. /// The of the authenticated account. - internal virtual AuthenticationRecord Authenticate(TokenRequestContext requestContext, CancellationToken cancellationToken = default) + public virtual AuthenticationRecord Authenticate(TokenRequestContext requestContext, CancellationToken cancellationToken = default) { return AuthenticateImplAsync(false, requestContext, cancellationToken).EnsureCompleted(); } @@ -129,7 +129,7 @@ internal virtual AuthenticationRecord Authenticate(TokenRequestContext requestCo /// A controlling the request lifetime. /// The details of the authentication request. /// The of the authenticated account. - internal virtual async Task AuthenticateAsync(TokenRequestContext requestContext, CancellationToken cancellationToken = default) + public virtual async Task AuthenticateAsync(TokenRequestContext requestContext, CancellationToken cancellationToken = default) { return await AuthenticateImplAsync(true, requestContext, cancellationToken).ConfigureAwait(false); } diff --git a/sdk/identity/Azure.Identity/src/InteractiveBrowserCredentialOptions.cs b/sdk/identity/Azure.Identity/src/InteractiveBrowserCredentialOptions.cs index 3bd98d602916..00de1da5ce2c 100644 --- a/sdk/identity/Azure.Identity/src/InteractiveBrowserCredentialOptions.cs +++ b/sdk/identity/Azure.Identity/src/InteractiveBrowserCredentialOptions.cs @@ -17,7 +17,7 @@ public class InteractiveBrowserCredentialOptions : TokenCredentialOptions, IToke /// Prevents the from automatically prompting the user. If automatic authentication is disabled a AuthenticationRequiredException will be thrown from and in the case that /// user interaction is necessary. The application is responsible for handling this exception, and calling or to authenticate the user interactively. /// - internal bool DisableAutomaticAuthentication { get; set; } + public bool DisableAutomaticAuthentication { get; set; } /// /// The tenant ID the user will be authenticated to. If not specified the user will be authenticated to the home tenant. @@ -34,14 +34,9 @@ public string TenantId public string ClientId { get; set; } = Constants.DeveloperSignOnClientId; /// - /// If set to true the credential will store tokens in a cache persisted to the machine, protected to the current user, which can be shared by other credentials and processes. + /// Specifies the to be used by the credential. /// - internal bool EnablePersistentCache { get; set; } - - /// - /// If set to true the credential will fall back to storing tokens in an unencrypted file if no OS level user encryption is available. - /// - internal bool AllowUnencryptedCache { get; set; } + public TokenCache TokenCache { get; set; } /// /// Uri where the STS will call back the application with the security token. This parameter is not required if the caller is not using a custom . In @@ -52,10 +47,6 @@ public string TenantId /// /// The captured from a previous authentication. /// - internal AuthenticationRecord AuthenticationRecord { get; set; } - - bool ITokenCacheOptions.EnablePersistentCache => EnablePersistentCache; - - bool ITokenCacheOptions.AllowUnencryptedCache => AllowUnencryptedCache; + public AuthenticationRecord AuthenticationRecord { get; set; } } } diff --git a/sdk/identity/Azure.Identity/src/MsalClientBase.cs b/sdk/identity/Azure.Identity/src/MsalClientBase.cs index bf51f855e99b..0b48b26f1dca 100644 --- a/sdk/identity/Azure.Identity/src/MsalClientBase.cs +++ b/sdk/identity/Azure.Identity/src/MsalClientBase.cs @@ -12,10 +12,6 @@ namespace Azure.Identity internal abstract class MsalClientBase where TClient : IClientApplicationBase { - // we are creating the MsalCacheHelper with a random guid based clientId to work around issue https://github.com/AzureAD/microsoft-authentication-extensions-for-dotnet/issues/98 - // This does not impact the functionality of the cacheHelper as the ClientId is only used to iterate accounts in the cache not for authentication purposes. - private static readonly string s_msalCacheClientId = Guid.NewGuid().ToString(); - private readonly AsyncLockWithValue _clientAsyncLock; /// @@ -40,9 +36,7 @@ protected MsalClientBase(CredentialPipeline pipeline, string tenantId, string cl ClientId = clientId; - EnablePersistentCache = cacheOptions?.EnablePersistentCache ?? false; - - AllowUnencryptedCache = cacheOptions?.AllowUnencryptedCache ?? false; + TokenCache = cacheOptions?.TokenCache; _clientAsyncLock = new AsyncLockWithValue(); } @@ -51,9 +45,7 @@ protected MsalClientBase(CredentialPipeline pipeline, string tenantId, string cl internal string ClientId { get; } - internal bool EnablePersistentCache { get; } - - internal bool AllowUnencryptedCache { get; } + internal TokenCache TokenCache { get; } protected CredentialPipeline Pipeline { get; } @@ -69,54 +61,13 @@ protected async ValueTask GetClientAsync(bool async, CancellationToken var client = await CreateClientAsync(async, cancellationToken).ConfigureAwait(false); - if (EnablePersistentCache) + if (TokenCache != null) { - MsalCacheHelper cacheHelper; - - StorageCreationProperties storageProperties = new StorageCreationPropertiesBuilder(Constants.DefaultMsalTokenCacheName, Constants.DefaultMsalTokenCacheDirectory, s_msalCacheClientId) - .WithMacKeyChain(Constants.DefaultMsalTokenCacheKeychainService, Constants.DefaultMsalTokenCacheKeychainAccount) - .WithLinuxKeyring(Constants.DefaultMsalTokenCacheKeyringSchema, Constants.DefaultMsalTokenCacheKeyringCollection, Constants.DefaultMsalTokenCacheKeyringLabel, Constants.DefaultMsaltokenCacheKeyringAttribute1, Constants.DefaultMsaltokenCacheKeyringAttribute2) - .Build(); - - try - { - cacheHelper = await CreateCacheHelper(storageProperties, async).ConfigureAwait(false); - - cacheHelper.VerifyPersistence(); - } - catch (MsalCachePersistenceException) - { - if (AllowUnencryptedCache) - { - storageProperties = new StorageCreationPropertiesBuilder(Constants.DefaultMsalTokenCacheName, Constants.DefaultMsalTokenCacheDirectory, s_msalCacheClientId) - .WithMacKeyChain(Constants.DefaultMsalTokenCacheKeychainService, Constants.DefaultMsalTokenCacheKeychainAccount) - .WithLinuxUnprotectedFile() - .Build(); - - cacheHelper = await CreateCacheHelper(storageProperties, async).ConfigureAwait(false); - - cacheHelper.VerifyPersistence(); - } - else - { - throw; - } - } - - cacheHelper.RegisterCache(client.UserTokenCache); + await TokenCache.RegisterCache(async, client.UserTokenCache, cancellationToken).ConfigureAwait(false); } asyncLock.SetValue(client); return client; } - - private static async ValueTask CreateCacheHelper(StorageCreationProperties storageProperties, bool async) - { - return async - ? await MsalCacheHelper.CreateAsync(storageProperties).ConfigureAwait(false) -#pragma warning disable AZC0102 // Do not use GetAwaiter().GetResult(). Use the TaskExtensions.EnsureCompleted() extension method instead. - : MsalCacheHelper.CreateAsync(storageProperties).GetAwaiter().GetResult(); -#pragma warning restore AZC0102 // Do not use GetAwaiter().GetResult(). Use the TaskExtensions.EnsureCompleted() extension method instead. - } } } diff --git a/sdk/identity/Azure.Identity/src/PersistentTokenCache.cs b/sdk/identity/Azure.Identity/src/PersistentTokenCache.cs new file mode 100644 index 000000000000..c18da46943a3 --- /dev/null +++ b/sdk/identity/Azure.Identity/src/PersistentTokenCache.cs @@ -0,0 +1,157 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Identity.Client; +using Microsoft.Identity.Client.Extensions.Msal; + +namespace Azure.Identity +{ + /// + /// Persistent token cache. + /// + public class PersistentTokenCache : TokenCache + { + // we are creating the MsalCacheHelper with a random guid based clientId to work around issue https://github.com/AzureAD/microsoft-authentication-extensions-for-dotnet/issues/98 + // This does not impact the functionality of the cacheHelper as the ClientId is only used to iterate accounts in the cache not for authentication purposes. + private static readonly string s_msalCacheClientId = Guid.NewGuid().ToString(); + + private static AsyncLockWithValue cacheHelperLock = new AsyncLockWithValue(); + private static AsyncLockWithValue s_ProtectedCacheHelperLock = new AsyncLockWithValue(); + private static AsyncLockWithValue s_FallbackCacheHelperLock = new AsyncLockWithValue(); + private readonly bool _allowUnencryptedStorage; + private readonly string _name; + + /// + /// Creates a new instance of . + /// + /// + public PersistentTokenCache(bool allowUnencryptedStorage = true) + { + _allowUnencryptedStorage = allowUnencryptedStorage; + } + + /// + /// Creates a new instance of with the specified options. + /// + /// Options controlling the storage of the . + public PersistentTokenCache(PersistentTokenCacheOptions options) + { + _allowUnencryptedStorage = options?.AllowUnencryptedStorage ?? false; + + _name = options?.Name; + } + + internal override async Task RegisterCache(bool async, ITokenCache tokenCache, CancellationToken cancellationToken) + { + MsalCacheHelper cacheHelper = await GetCacheHelperAsync(async, cancellationToken).ConfigureAwait(false); + + cacheHelper.RegisterCache(tokenCache); + + await base.RegisterCache(async, tokenCache, cancellationToken).ConfigureAwait(false); + } + + private async Task GetCacheHelperAsync(bool async, CancellationToken cancellationToken) + { + using var asyncLock = await cacheHelperLock.GetLockOrValueAsync(async, cancellationToken).ConfigureAwait(false); + + if (asyncLock.HasValue) + { + return asyncLock.Value; + } + + MsalCacheHelper cacheHelper; + + try + { + cacheHelper = string.IsNullOrEmpty(_name) ? await GetProtectedCacheHelperAsync(async, cancellationToken).ConfigureAwait(false) : await GetProtectedCacheHelperAsync(async, _name).ConfigureAwait(false); + + cacheHelper.VerifyPersistence(); + } + catch (MsalCachePersistenceException) + { + if (_allowUnencryptedStorage) + { + cacheHelper = string.IsNullOrEmpty(_name) ? await GetFallbackCacheHelperAsync(async, cancellationToken).ConfigureAwait(false) : await GetFallbackCacheHelperAsync(async, _name).ConfigureAwait(false); + + cacheHelper.VerifyPersistence(); + } + else + { + throw; + } + } + + asyncLock.SetValue(cacheHelper); + + return cacheHelper; + } + + private static async Task GetProtectedCacheHelperAsync(bool async, CancellationToken cancellationToken) + { + using var asyncLock = await s_ProtectedCacheHelperLock.GetLockOrValueAsync(async, cancellationToken).ConfigureAwait(false); + + if (asyncLock.HasValue) + { + return asyncLock.Value; + } + + MsalCacheHelper cacheHelper = await GetProtectedCacheHelperAsync(async, Constants.DefaultMsalTokenCacheName).ConfigureAwait(false); + + asyncLock.SetValue(cacheHelper); + + return cacheHelper; + } + + private static async Task GetProtectedCacheHelperAsync(bool async, string name) + { + StorageCreationProperties storageProperties = new StorageCreationPropertiesBuilder(name, Constants.DefaultMsalTokenCacheDirectory, s_msalCacheClientId) + .WithMacKeyChain(Constants.DefaultMsalTokenCacheKeychainService, name) + .WithLinuxKeyring(Constants.DefaultMsalTokenCacheKeyringSchema, Constants.DefaultMsalTokenCacheKeyringCollection, name, Constants.DefaultMsaltokenCacheKeyringAttribute1, Constants.DefaultMsaltokenCacheKeyringAttribute2) + .Build(); + + MsalCacheHelper cacheHelper = await CreateCacheHelper(async, storageProperties).ConfigureAwait(false); + + return cacheHelper; + } + + private static async Task GetFallbackCacheHelperAsync(bool async, CancellationToken cancellationToken) + { + using var asyncLock = await s_FallbackCacheHelperLock.GetLockOrValueAsync(async, cancellationToken).ConfigureAwait(false); + + if (asyncLock.HasValue) + { + return asyncLock.Value; + } + + MsalCacheHelper cacheHelper = await GetFallbackCacheHelperAsync(async, Constants.DefaultMsalTokenCacheName).ConfigureAwait(false); + + asyncLock.SetValue(cacheHelper); + + return cacheHelper; + } + + private static async Task GetFallbackCacheHelperAsync(bool async, string name) + { + StorageCreationProperties storageProperties = new StorageCreationPropertiesBuilder(name, Constants.DefaultMsalTokenCacheDirectory, s_msalCacheClientId) + .WithMacKeyChain(Constants.DefaultMsalTokenCacheKeychainService, name) + .WithLinuxUnprotectedFile() + .Build(); + + MsalCacheHelper cacheHelper = await CreateCacheHelper(async, storageProperties).ConfigureAwait(false); + + return cacheHelper; + } + + private static async Task CreateCacheHelper(bool async, StorageCreationProperties storageProperties) + { + return async + ? await MsalCacheHelper.CreateAsync(storageProperties).ConfigureAwait(false) +#pragma warning disable AZC0102 // Do not use GetAwaiter().GetResult(). Use the TaskExtensions.EnsureCompleted() extension method instead. + : MsalCacheHelper.CreateAsync(storageProperties).GetAwaiter().GetResult(); +#pragma warning restore AZC0102 // Do not use GetAwaiter().GetResult(). Use the TaskExtensions.EnsureCompleted() extension method instead. + } + } +} diff --git a/sdk/identity/Azure.Identity/src/PersistentTokenCacheOptions.cs b/sdk/identity/Azure.Identity/src/PersistentTokenCacheOptions.cs new file mode 100644 index 000000000000..4adb61a9614a --- /dev/null +++ b/sdk/identity/Azure.Identity/src/PersistentTokenCacheOptions.cs @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +namespace Azure.Identity +{ + /// + /// Options controlling the storage of the . + /// + public class PersistentTokenCacheOptions + { + /// + /// Name uniquely identifying the . + /// + public string Name { get; set; } + + /// + /// If set to true the token cache may be persisted as an unencrypted file if no OS level user encryption is available. When set to false the + /// will throw a in the event no OS level user encryption is available. + /// + public bool AllowUnencryptedStorage { get; set; } + } +} diff --git a/sdk/identity/Azure.Identity/src/SharedTokenCacheCredential.cs b/sdk/identity/Azure.Identity/src/SharedTokenCacheCredential.cs index b0db8766d027..db389118e27e 100644 --- a/sdk/identity/Azure.Identity/src/SharedTokenCacheCredential.cs +++ b/sdk/identity/Azure.Identity/src/SharedTokenCacheCredential.cs @@ -25,12 +25,15 @@ public class SharedTokenCacheCredential : TokenCredential internal const string MultipleMatchingAccountsInCacheMessage = "SharedTokenCacheCredential authentication unavailable. Multiple accounts matching the specified{0}{1} were found in the cache."; private static readonly ITokenCacheOptions s_DefaultCacheOptions = new SharedTokenCacheCredentialOptions(); - private readonly MsalPublicClient _client; private readonly CredentialPipeline _pipeline; private readonly string _tenantId; private readonly string _username; + private readonly bool _skipTenantValidation; private readonly AuthenticationRecord _record; private readonly AsyncLockWithValue _accountAsyncLock; + + internal MsalPublicClient Client { get; } + /// /// Creates a new which will authenticate users signed in through developer tools supporting Azure single sign on. /// @@ -71,11 +74,13 @@ internal SharedTokenCacheCredential(string tenantId, string username, TokenCrede _username = username; + _skipTenantValidation = (options as SharedTokenCacheCredentialOptions)?.EnableGuestTenantAuthentication ?? false; + _record = (options as SharedTokenCacheCredentialOptions)?.AuthenticationRecord; _pipeline = pipeline ?? CredentialPipeline.GetInstance(options); - _client = client ?? new MsalPublicClient(_pipeline, tenantId, Constants.DeveloperSignOnClientId, null, (options as ITokenCacheOptions) ?? s_DefaultCacheOptions); + Client = client ?? new MsalPublicClient(_pipeline, tenantId, (options as SharedTokenCacheCredentialOptions)?.ClientId ?? Constants.DeveloperSignOnClientId, null, (options as ITokenCacheOptions) ?? s_DefaultCacheOptions); _accountAsyncLock = new AsyncLockWithValue(); } @@ -109,7 +114,7 @@ private async ValueTask GetTokenImplAsync(bool async, TokenRequestC try { IAccount account = await GetAccountAsync(async, cancellationToken).ConfigureAwait(false); - AuthenticationResult result = await _client.AcquireTokenSilentAsync(requestContext.Scopes, account, async, cancellationToken).ConfigureAwait(false); + AuthenticationResult result = await Client.AcquireTokenSilentAsync(requestContext.Scopes, account, async, cancellationToken).ConfigureAwait(false); return scope.Succeeded(new AccessToken(result.AccessToken, result.ExpiresOn)); } catch (MsalUiRequiredException) @@ -138,15 +143,15 @@ private async ValueTask GetAccountAsync(bool async, CancellationToken return account; } - List accounts = await _client.GetAccountsAsync(async, cancellationToken).ConfigureAwait(false); + List accounts = await Client.GetAccountsAsync(async, cancellationToken).ConfigureAwait(false); // filter the accounts to those matching the specified user and tenant List filteredAccounts = accounts.Where(a => // if _username is specified it must match the account - (string.IsNullOrEmpty(_username) || string.Compare(a.Username, _username, StringComparison.OrdinalIgnoreCase) == 0) + ((string.IsNullOrEmpty(_username) || string.Compare(a.Username, _username, StringComparison.OrdinalIgnoreCase) == 0)) && - //if _tenantId is specified it must match the account - (string.IsNullOrEmpty(_tenantId) || string.Compare(a.HomeAccountId?.TenantId, _tenantId, StringComparison.OrdinalIgnoreCase) == 0) + // if _skipTenantValidation is false and _tenantId is specified it must match the account + (_skipTenantValidation || (string.IsNullOrEmpty(_tenantId) || string.Compare(a.HomeAccountId?.TenantId, _tenantId, StringComparison.OrdinalIgnoreCase) == 0)) ).ToList(); if (filteredAccounts.Count != 1) diff --git a/sdk/identity/Azure.Identity/src/SharedTokenCacheCredentialOptions.cs b/sdk/identity/Azure.Identity/src/SharedTokenCacheCredentialOptions.cs index d4a483a84eb5..2042afbaa7aa 100644 --- a/sdk/identity/Azure.Identity/src/SharedTokenCacheCredentialOptions.cs +++ b/sdk/identity/Azure.Identity/src/SharedTokenCacheCredentialOptions.cs @@ -10,6 +10,11 @@ public class SharedTokenCacheCredentialOptions : TokenCredentialOptions, ITokenC { private string _tenantId = null; + /// + /// The client id of the application registration used to authenticate users in the cache. + /// + public string ClientId { get; set; } = Constants.DeveloperSignOnClientId; + /// /// Specifies the preferred authentication account username, or UPN, to be retrieved from the shared token cache for single sign on authentication with /// development tools, in the case multiple accounts are found in the shared token. @@ -26,18 +31,35 @@ public string TenantId set { _tenantId = Validations.ValidateTenantId(value, allowNull: true); } } + /// + /// When set to true the can be used to authenticate to tenants other than the home tenant, requiring and also to be specified as well. + /// + public bool EnableGuestTenantAuthentication { get; set; } + /// /// The captured from a previous authentication with an interactive credential, such as the or . /// - internal AuthenticationRecord AuthenticationRecord { get; set; } + public AuthenticationRecord AuthenticationRecord { get; set; } /// - /// If set to true the credential will fall back to storing tokens in an unencrypted file if no OS level user encryption is available. + /// Specifies the to be used by the credential. /// - internal bool AllowUnencryptedCache { get; set; } + public TokenCache TokenCache { get; } - bool ITokenCacheOptions.AllowUnencryptedCache => AllowUnencryptedCache; + /// + /// SharedTokenCacheCredentialOptions + /// + public SharedTokenCacheCredentialOptions() + :this(null) + { } - bool ITokenCacheOptions.EnablePersistentCache => true; + /// + /// SharedTokenCacheCredentialOptions + /// + /// + public SharedTokenCacheCredentialOptions(TokenCache tokenCache) + { + TokenCache = tokenCache ?? new PersistentTokenCache(); + } } } diff --git a/sdk/identity/Azure.Identity/src/TokenCache.cs b/sdk/identity/Azure.Identity/src/TokenCache.cs new file mode 100644 index 000000000000..bce569e8a8ec --- /dev/null +++ b/sdk/identity/Azure.Identity/src/TokenCache.cs @@ -0,0 +1,292 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; +using Azure.Core.Pipeline; +using Microsoft.Identity.Client; +using Microsoft.Identity.Client.Extensions.Msal; + +namespace Azure.Identity +{ + /// + /// A cache for Tokens. + /// + public class TokenCache : IDisposable + { + private SemaphoreSlim _lock = new SemaphoreSlim(1,1); + private byte[] _data; + private DateTimeOffset _lastUpdated; + private ConditionalWeakTable _cacheAccessMap; + private bool _disposedValue; + + private class CacheTimestamp + { + private DateTimeOffset _timestamp; + + public CacheTimestamp() + { + Update(); + } + + public void Update() + { + _timestamp = DateTimeOffset.UtcNow; + } + + public DateTimeOffset Value { get { return _timestamp; } } + } + + /// + /// Instantiates a new . + /// + public TokenCache() + : this(Array.Empty()) + { + } + + internal TokenCache(byte[] data) + { + _data = data; + _lastUpdated = DateTimeOffset.UtcNow; + _cacheAccessMap = new ConditionalWeakTable(); + } + + /// + /// An event notifying the subscriber that the underlying has been updated. This event can be handled to persist the updated cache data. + /// + public event Func Updated; + + /// + /// Serializes the to the specified . + /// + /// The which the serialized will be written to. + /// A controlling the request lifetime. + public void Serialize(Stream stream, CancellationToken cancellationToken = default) + { + if (stream is null) + throw new ArgumentNullException(nameof(stream)); + + SerializeAsync(stream, false, cancellationToken).EnsureCompleted(); + } + + /// + /// Serializes the to the specified . + /// + /// The to which the serialized will be written. + /// A controlling the request lifetime. + public async Task SerializeAsync(Stream stream, CancellationToken cancellationToken = default) + { + if (stream is null) + throw new ArgumentNullException(nameof(stream)); + + await SerializeAsync(stream, true, cancellationToken).ConfigureAwait(false); + } + + + /// + /// Deserializes the from the specified . + /// + /// The from which the serialized will be read. + /// A controlling the request lifetime. + public static TokenCache Deserialize(Stream stream, CancellationToken cancellationToken = default) + { + if (stream is null) + throw new ArgumentNullException(nameof(stream)); + + return DeserializeAsync(stream, false, cancellationToken).EnsureCompleted(); + } + + /// + /// Deserializes the from the specified . + /// + /// The from which the serialized will be read. + /// A controlling the request lifetime. + public static async Task DeserializeAsync(Stream stream, CancellationToken cancellationToken = default) + { + if (stream is null) + throw new ArgumentNullException(nameof(stream)); + + return await DeserializeAsync(stream, true, cancellationToken).ConfigureAwait(false); + } + + private async Task SerializeAsync(Stream stream, bool async, CancellationToken cancellationToken) + { + if (async) + { + await stream.WriteAsync(_data, 0, _data.Length, cancellationToken).ConfigureAwait(false); + } + else + { + stream.Write(_data, 0, _data.Length); + } + } + + private static async Task DeserializeAsync(Stream stream, bool async, CancellationToken cancellationToken) + { + var data = new byte[stream.Length - stream.Position]; + + if (async) + { + await stream.ReadAsync(data, 0, data.Length, cancellationToken).ConfigureAwait(false); + } + else + { + stream.Read(data, 0, data.Length); + } + + return new TokenCache(data); + } + + internal virtual async Task RegisterCache(bool async, ITokenCache tokenCache, CancellationToken cancellationToken) + { + if (async) + { + await _lock.WaitAsync(cancellationToken).ConfigureAwait(false); + } + else + { + _lock.Wait(cancellationToken); + } + + try + { + if (!_cacheAccessMap.TryGetValue(tokenCache, out _)) + { + tokenCache.SetBeforeAccessAsync(OnBeforeCacheAccessAsync); + + tokenCache.SetAfterAccessAsync(OnAfterCacheAccessAsync); + + _cacheAccessMap.Add(tokenCache, new CacheTimestamp()); + } + } + finally + { + _lock.Release(); + } + } + + private async Task OnBeforeCacheAccessAsync(TokenCacheNotificationArgs args) + { + if (_disposedValue) + { + throw new ObjectDisposedException(nameof(TokenCache)); + } + + await _lock.WaitAsync().ConfigureAwait(false); + + try + { + args.TokenCache.DeserializeMsalV3(_data, true); + + _cacheAccessMap.GetOrCreateValue(args.TokenCache).Update(); + } + finally + { + _lock.Release(); + } + } + + + private async Task OnAfterCacheAccessAsync(TokenCacheNotificationArgs args) + { + if (_disposedValue) + { + throw new ObjectDisposedException(nameof(TokenCache)); + } + + if (args.HasStateChanged) + { + await UpdateCacheDataAsync(args.TokenCache).ConfigureAwait(false); + } + } + + private async Task UpdateCacheDataAsync(ITokenCacheSerializer tokenCache) + { + await _lock.WaitAsync().ConfigureAwait(false); + + try + { + if (!_cacheAccessMap.TryGetValue(tokenCache, out CacheTimestamp lastRead) || lastRead.Value < _lastUpdated) + { + _data = await MergeCacheData(_data, tokenCache.SerializeMsalV3()).ConfigureAwait(false); + } + else + { + _data = tokenCache.SerializeMsalV3(); + } + + _cacheAccessMap.GetOrCreateValue(tokenCache).Update(); + + _lastUpdated = DateTime.UtcNow; + } + finally + { + _lock.Release(); + } + + if (Updated != null) + { + foreach (Func handler in Updated.GetInvocationList()) + { + await handler(new TokenCacheUpdatedArgs(this)).ConfigureAwait(false); + } + } + } + + private static async Task MergeCacheData(byte[] cacheA, byte[] cacheB) + { + byte[] merged = null; + + var client = PublicClientApplicationBuilder.Create(Guid.NewGuid().ToString()).Build(); + + client.UserTokenCache.SetBeforeAccess(args => args.TokenCache.DeserializeMsalV3(cacheA)); + + await client.GetAccountsAsync().ConfigureAwait(false); + + client.UserTokenCache.SetBeforeAccess(args => args.TokenCache.DeserializeMsalV3(cacheB, shouldClearExistingCache: false)); + + client.UserTokenCache.SetAfterAccess(args => merged = args.TokenCache.SerializeMsalV3()); + + await client.GetAccountsAsync().ConfigureAwait(false); + + return merged; + } + + /// + /// Disposes of the . + /// + /// Indicates whether managed resources should be disposed. + protected virtual void Dispose(bool disposing) + { + if (!_disposedValue) + { + if (disposing) + { + _lock.Dispose(); + } + + _cacheAccessMap = null; + + _data = null; + + _disposedValue = true; + } + } + + /// + /// Disposes of the . + /// + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + } +} diff --git a/sdk/identity/Azure.Identity/src/TokenCacheUpdatedArgs.cs b/sdk/identity/Azure.Identity/src/TokenCacheUpdatedArgs.cs new file mode 100644 index 000000000000..117e78f0b2ba --- /dev/null +++ b/sdk/identity/Azure.Identity/src/TokenCacheUpdatedArgs.cs @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +namespace Azure.Identity +{ + /// + /// Data regarding an update of a . + /// + public class TokenCacheUpdatedArgs + { + internal TokenCacheUpdatedArgs(TokenCache cache) + { + Cache = cache; + } + + /// + /// The instance which was updated. + /// + public TokenCache Cache { get; } + } +} diff --git a/sdk/identity/Azure.Identity/src/UsernamePasswordCredential.cs b/sdk/identity/Azure.Identity/src/UsernamePasswordCredential.cs index cde4f01aa8fa..f51e4d497647 100644 --- a/sdk/identity/Azure.Identity/src/UsernamePasswordCredential.cs +++ b/sdk/identity/Azure.Identity/src/UsernamePasswordCredential.cs @@ -73,7 +73,7 @@ public UsernamePasswordCredential(string username, string password, string tenan /// The Azure Active Directory tenant (directory) ID or name. /// The client (application) ID of an App Registration in the tenant. /// The client options for the newly created UsernamePasswordCredential - internal UsernamePasswordCredential(string username, string password, string tenantId, string clientId, UsernamePasswordCredentialOptions options) + public UsernamePasswordCredential(string username, string password, string tenantId, string clientId, UsernamePasswordCredentialOptions options) : this(username, password, tenantId, clientId, options, null, null) { } @@ -98,7 +98,7 @@ internal UsernamePasswordCredential(string username, string password, string ten /// /// A controlling the request lifetime. /// The of the authenticated account. - internal virtual AuthenticationRecord Authenticate(CancellationToken cancellationToken = default) + public virtual AuthenticationRecord Authenticate(CancellationToken cancellationToken = default) { // get the default scope for the authority, throw if no default scope exists string defaultScope = AzureAuthorityHosts.GetDefaultScope(_pipeline.AuthorityHost) ?? throw new CredentialUnavailableException(NoDefaultScopeMessage); @@ -111,7 +111,7 @@ internal virtual AuthenticationRecord Authenticate(CancellationToken cancellatio /// /// A controlling the request lifetime. /// The of the authenticated account. - internal virtual async Task AuthenticateAsync(CancellationToken cancellationToken = default) + public virtual async Task AuthenticateAsync(CancellationToken cancellationToken = default) { // get the default scope for the authority, throw if no default scope exists string defaultScope = AzureAuthorityHosts.GetDefaultScope(_pipeline.AuthorityHost) ?? throw new CredentialUnavailableException(NoDefaultScopeMessage); @@ -125,7 +125,7 @@ internal virtual async Task AuthenticateAsync(Cancellation /// A controlling the request lifetime. /// The details of the authentication request. /// The of the authenticated account. - internal virtual AuthenticationRecord Authenticate(TokenRequestContext requestContext, CancellationToken cancellationToken = default) + public virtual AuthenticationRecord Authenticate(TokenRequestContext requestContext, CancellationToken cancellationToken = default) { return AuthenticateImplAsync(false, requestContext, cancellationToken).EnsureCompleted(); } @@ -136,7 +136,7 @@ internal virtual AuthenticationRecord Authenticate(TokenRequestContext requestCo /// A controlling the request lifetime. /// The details of the authentication request. /// The of the authenticated account. - internal virtual async Task AuthenticateAsync(TokenRequestContext requestContext, CancellationToken cancellationToken = default) + public virtual async Task AuthenticateAsync(TokenRequestContext requestContext, CancellationToken cancellationToken = default) { return await AuthenticateImplAsync(true, requestContext, cancellationToken).ConfigureAwait(false); } diff --git a/sdk/identity/Azure.Identity/src/UsernamePasswordCredentialOptions.cs b/sdk/identity/Azure.Identity/src/UsernamePasswordCredentialOptions.cs index 91dead139eac..d2d188752a06 100644 --- a/sdk/identity/Azure.Identity/src/UsernamePasswordCredentialOptions.cs +++ b/sdk/identity/Azure.Identity/src/UsernamePasswordCredentialOptions.cs @@ -6,16 +6,12 @@ namespace Azure.Identity /// /// Options to configure the . /// - internal class UsernamePasswordCredentialOptions : TokenCredentialOptions, ITokenCacheOptions + public class UsernamePasswordCredentialOptions : TokenCredentialOptions, ITokenCacheOptions { /// - /// If set to true the credential will store tokens in a persistent cache shared by other user credentials. + /// Specifies the to be used by the credential. /// - public bool EnablePersistentCache { get; set; } + public TokenCache TokenCache { get; set; } - /// - /// If set to true the credential will fall back to storing tokens in an unencrypted file if no OS level user encryption is available. - /// - public bool AllowUnencryptedCache { get; set; } } } diff --git a/sdk/identity/Azure.Identity/tests/DeviceCodeCredentialCtorTests.cs b/sdk/identity/Azure.Identity/tests/DeviceCodeCredentialCtorTests.cs index 65388ba36966..8492fd82b59d 100644 --- a/sdk/identity/Azure.Identity/tests/DeviceCodeCredentialCtorTests.cs +++ b/sdk/identity/Azure.Identity/tests/DeviceCodeCredentialCtorTests.cs @@ -43,8 +43,7 @@ public void ValidateConstructorOverload1() TenantId = Guid.NewGuid().ToString(), AuthorityHost = new Uri("https://login.myauthority.com/"), DisableAutomaticAuthentication = true, - EnablePersistentCache = true, - AllowUnencryptedCache = true, + TokenCache = new TokenCache(), AuthenticationRecord = new AuthenticationRecord(), DeviceCodeCallback = DummyCallback, }; @@ -123,8 +122,7 @@ public void AssertOptionsHonored(DeviceCodeCredentialOptions options, DeviceCode Assert.AreEqual(options.TenantId, credential.Client.TenantId); Assert.AreEqual(options.AuthorityHost, credential.Pipeline.AuthorityHost); Assert.AreEqual(options.DisableAutomaticAuthentication, credential.DisableAutomaticAuthentication); - Assert.AreEqual(options.EnablePersistentCache, credential.Client.EnablePersistentCache); - Assert.AreEqual(options.AllowUnencryptedCache, credential.Client.AllowUnencryptedCache); + Assert.AreEqual(options.TokenCache, credential.Client.TokenCache); Assert.AreEqual(options.AuthenticationRecord, credential.Record); AssertCallbacksEqual(options.DeviceCodeCallback ?? DeviceCodeCredential.DefaultDeviceCodeHandler, credential.DeviceCodeCallback); diff --git a/sdk/identity/Azure.Identity/tests/InteractiveBrowserCredentialCtorTests.cs b/sdk/identity/Azure.Identity/tests/InteractiveBrowserCredentialCtorTests.cs index 62da55872a0f..eefc7fc8a8ae 100644 --- a/sdk/identity/Azure.Identity/tests/InteractiveBrowserCredentialCtorTests.cs +++ b/sdk/identity/Azure.Identity/tests/InteractiveBrowserCredentialCtorTests.cs @@ -36,8 +36,7 @@ public void ValidateConstructorOverload1() TenantId = Guid.NewGuid().ToString(), AuthorityHost = new Uri("https://login.myauthority.com/"), DisableAutomaticAuthentication = true, - EnablePersistentCache = true, - AllowUnencryptedCache = true, + TokenCache = new TokenCache(), AuthenticationRecord = new AuthenticationRecord(), RedirectUri = new Uri("https://localhost:8080"), }; @@ -116,8 +115,7 @@ public void AssertOptionsHonored(InteractiveBrowserCredentialOptions options, In Assert.AreEqual(options.TenantId, credential.Client.TenantId); Assert.AreEqual(options.AuthorityHost, credential.Pipeline.AuthorityHost); Assert.AreEqual(options.DisableAutomaticAuthentication, credential.DisableAutomaticAuthentication); - Assert.AreEqual(options.EnablePersistentCache, credential.Client.EnablePersistentCache); - Assert.AreEqual(options.AllowUnencryptedCache, credential.Client.AllowUnencryptedCache); + Assert.AreEqual(options.TokenCache, credential.Client.TokenCache); Assert.AreEqual(options.AuthenticationRecord, credential.Record); if (options.RedirectUri != null) diff --git a/sdk/identity/Azure.Identity/tests/InteractiveBrowserCredentialLiveTests.cs b/sdk/identity/Azure.Identity/tests/InteractiveBrowserCredentialLiveTests.cs index 73f44b785b7d..acc2f71bcd78 100644 --- a/sdk/identity/Azure.Identity/tests/InteractiveBrowserCredentialLiveTests.cs +++ b/sdk/identity/Azure.Identity/tests/InteractiveBrowserCredentialLiveTests.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. using System; +using System.IO; using System.Threading; using System.Threading.Tasks; using Azure.Core; @@ -73,12 +74,32 @@ public async Task AuthenticateFollowedByGetTokenAsync() [Ignore("This test is an integration test which can only be run with user interaction")] public async Task AuthenticateWithSharedTokenCacheAsync() { - var cred = new InteractiveBrowserCredential(new InteractiveBrowserCredentialOptions { EnablePersistentCache = true }); + var cred = new InteractiveBrowserCredential(new InteractiveBrowserCredentialOptions { TokenCache = new PersistentTokenCache() }); // this should pop browser AuthenticationRecord record = await cred.AuthenticateAsync(); - var cred2 = new InteractiveBrowserCredential(new InteractiveBrowserCredentialOptions { EnablePersistentCache = true, AuthenticationRecord = record }); + var cred2 = new InteractiveBrowserCredential(new InteractiveBrowserCredentialOptions { TokenCache = new PersistentTokenCache(), AuthenticationRecord = record }); + + // this should not pop browser + AccessToken token = await cred2.GetTokenAsync(new TokenRequestContext(new string[] { "https://vault.azure.net/.default" })).ConfigureAwait(false); + + Assert.NotNull(token.Token); + } + + + [Test] + [Ignore("This test is an integration test which can only be run with user interaction")] + public async Task AuthenticateWithCommonTokenCacheAsync() + { + var tokenCache = new TokenCache(); + + var cred = new InteractiveBrowserCredential(new InteractiveBrowserCredentialOptions { TokenCache = tokenCache }); + + // this should pop browser + AuthenticationRecord record = await cred.AuthenticateAsync(); + + var cred2 = new InteractiveBrowserCredential(new InteractiveBrowserCredentialOptions { TokenCache = tokenCache, AuthenticationRecord = record }); // this should not pop browser AccessToken token = await cred2.GetTokenAsync(new TokenRequestContext(new string[] { "https://vault.azure.net/.default" })).ConfigureAwait(false); diff --git a/sdk/identity/Azure.Identity/tests/Mock/AuthenticationResultFactory.cs b/sdk/identity/Azure.Identity/tests/Mock/AuthenticationResultFactory.cs index fe7e42298751..c179ee706b9d 100644 --- a/sdk/identity/Azure.Identity/tests/Mock/AuthenticationResultFactory.cs +++ b/sdk/identity/Azure.Identity/tests/Mock/AuthenticationResultFactory.cs @@ -33,7 +33,7 @@ public static AuthenticationResult Create(string accessToken = default, bool? is correlationId ??= Guid.NewGuid(); - return new AuthenticationResult(accessToken, isExtendedLifeTimeToken.Value, uniqueId, expiresOn.Value, extendedExpiresOn.Value, tenantId, account, idToken, scopes, correlationId.Value); + return new AuthenticationResult(accessToken, isExtendedLifeTimeToken.Value, uniqueId, expiresOn.Value, extendedExpiresOn.Value, tenantId, account, idToken, scopes, correlationId.Value, (AuthenticationResultMetadata)null); } } } diff --git a/sdk/identity/Azure.Identity/tests/Mock/TestDefaultAzureCredentialFactory.cs b/sdk/identity/Azure.Identity/tests/Mock/TestDefaultAzureCredentialFactory.cs index 31e89ed219a4..0d4d5bd3dfbe 100644 --- a/sdk/identity/Azure.Identity/tests/Mock/TestDefaultAzureCredentialFactory.cs +++ b/sdk/identity/Azure.Identity/tests/Mock/TestDefaultAzureCredentialFactory.cs @@ -32,7 +32,7 @@ public override TokenCredential CreateSharedTokenCacheCredential(string tenantId => new SharedTokenCacheCredential(tenantId, username, default, Pipeline); public override TokenCredential CreateInteractiveBrowserCredential(string tenantId) - => new InteractiveBrowserCredential(tenantId, Constants.DeveloperSignOnClientId, new InteractiveBrowserCredentialOptions { EnablePersistentCache = true }, Pipeline); + => new InteractiveBrowserCredential(tenantId, Constants.DeveloperSignOnClientId, new InteractiveBrowserCredentialOptions(), Pipeline); public override TokenCredential CreateAzureCliCredential() => new AzureCliCredential(Pipeline, _processService); diff --git a/sdk/identity/Azure.Identity/tests/SharedTokenCacheCredentialTests.cs b/sdk/identity/Azure.Identity/tests/SharedTokenCacheCredentialTests.cs index 530bf2eaa5b8..4b53a039c2c9 100644 --- a/sdk/identity/Azure.Identity/tests/SharedTokenCacheCredentialTests.cs +++ b/sdk/identity/Azure.Identity/tests/SharedTokenCacheCredentialTests.cs @@ -401,5 +401,58 @@ public async Task UiRequiredException() await Task.CompletedTask; } + + [Test] + public async Task MatchAnySingleTenantIdWithEnableGuestTenantAuthentication() + { + string expToken = Guid.NewGuid().ToString(); + DateTimeOffset expExpiresOn = DateTimeOffset.UtcNow.AddMinutes(5); + string tenantId = Guid.NewGuid().ToString(); + var mockMsalClient = new MockMsalPublicClient + { + Accounts = new List { new MockAccount("mockuser@mockdomain.com", Guid.NewGuid().ToString()) }, + SilentAuthFactory = (_) => { return AuthenticationResultFactory.Create(accessToken: expToken, expiresOn: expExpiresOn); } + }; + + var credential = InstrumentClient(new SharedTokenCacheCredential(tenantId, null, new SharedTokenCacheCredentialOptions { EnableGuestTenantAuthentication = true }, null, mockMsalClient)); + + AccessToken token = await credential.GetTokenAsync(new TokenRequestContext(MockScopes.Default)); + + Assert.AreEqual(expToken, token.Token); + + Assert.AreEqual(expExpiresOn, token.ExpiresOn); + } + + [Test] + public async Task MatchAnyTenantIdWithEnableGuestTenantAuthenticationAndUsername() + { + string expToken = Guid.NewGuid().ToString(); + DateTimeOffset expExpiresOn = DateTimeOffset.UtcNow.AddMinutes(5); + string tenantId = Guid.NewGuid().ToString(); + var mockMsalClient = new MockMsalPublicClient + { + Accounts = new List { new MockAccount("mockuser@mockdomain.com", Guid.NewGuid().ToString()), new MockAccount("fakeuser@fakedomain.com", Guid.NewGuid().ToString()) }, + SilentAuthFactory = (_) => { return AuthenticationResultFactory.Create(accessToken: expToken, expiresOn: expExpiresOn); } + }; + + + var credential = InstrumentClient(new SharedTokenCacheCredential(tenantId, "mockuser@mockdomain.com", new SharedTokenCacheCredentialOptions { EnableGuestTenantAuthentication = true }, null, mockMsalClient)); + + AccessToken token = await credential.GetTokenAsync(new TokenRequestContext(MockScopes.Default)); + + Assert.AreEqual(expToken, token.Token); + + Assert.AreEqual(expExpiresOn, token.ExpiresOn); + } + + [Test] + public void ValidateClientIdSetOnMsalClient() + { + var clientId = Guid.NewGuid().ToString(); + + var credential = new SharedTokenCacheCredential(new SharedTokenCacheCredentialOptions { ClientId = clientId }); + + Assert.AreEqual(clientId, credential.Client.ClientId); + } } } diff --git a/sdk/identity/Azure.Identity/tests/samples/TokenCacheSnippets.cs b/sdk/identity/Azure.Identity/tests/samples/TokenCacheSnippets.cs new file mode 100644 index 000000000000..f9c9804229fa --- /dev/null +++ b/sdk/identity/Azure.Identity/tests/samples/TokenCacheSnippets.cs @@ -0,0 +1,87 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.IO; +using System.Threading.Tasks; +using Microsoft.Extensions.Azure; + +namespace Azure.Identity.Samples +{ + public class TokenCacheSnippets + { + public void Identity_TokenCache_PersistentDefault() + { + #region Snippet:Identity_TokenCache_PersistentDefault + var credential = new InteractiveBrowserCredential(new InteractiveBrowserCredentialOptions { TokenCache = new PersistentTokenCache() }); + #endregion + } + + public void Identity_TokenCache_PersistentNamed() + { + #region Snippet:Identity_TokenCache_PersistentNamed + var tokenCache = new PersistentTokenCache(new PersistentTokenCacheOptions { Name = "my_application_name" }); + + var credential = new InteractiveBrowserCredential(new InteractiveBrowserCredentialOptions { TokenCache = tokenCache }); + #endregion + } + + public void Identity_TokenCache_PersistentUnencrypted() + { + #region Snippet:Identity_TokenCache_PersistentUnencrypted + var tokenCache = new PersistentTokenCache(new PersistentTokenCacheOptions { AllowUnencryptedStorage = true }); + + var credential = new InteractiveBrowserCredential(new InteractiveBrowserCredentialOptions { TokenCache = tokenCache}); + #endregion + } + + public async Task Identity_TokenCache_CustomPersistence_Read() + { + #region Snippet:Identity_TokenCache_CustomPersistence_Read + using var cacheStream = new FileStream(TokenCachePath, FileMode.OpenOrCreate, FileAccess.Read); + + var tokenCache = await TokenCache.DeserializeAsync(cacheStream); + #endregion + } + + public async Task Identity_TokenCache_CustomPersistence_Write() + { + var tokenCache = new TokenCache(); + + #region Snippet:Identity_TokenCache_CustomPersistence_Write + using var cacheStream = new FileStream(TokenCachePath, FileMode.Create, FileAccess.Write); + + await tokenCache.SerializeAsync(cacheStream); + #endregion + } + + private const string TokenCachePath = "./tokencache.bin"; + + #region Snippet:Identity_TokenCache_CustomPersistence_Usage + public static async Task ReadTokenCacheAsync() + { + using var cacheStream = new FileStream(TokenCachePath, FileMode.OpenOrCreate, FileAccess.Read); + + var tokenCache = await TokenCache.DeserializeAsync(cacheStream); + + tokenCache.Updated += WriteCacheOnUpdateAsync; + + return tokenCache; + } + + public static async Task WriteCacheOnUpdateAsync(TokenCacheUpdatedArgs args) + { + using var cacheStream = new FileStream(TokenCachePath, FileMode.Create, FileAccess.Write); + + await args.Cache.SerializeAsync(cacheStream); + } + + public static async Task Main() + { + var tokenCache = await ReadTokenCacheAsync(); + + var credential = new InteractiveBrowserCredential(new InteractiveBrowserCredentialOptions { TokenCache = tokenCache }); + } + #endregion + } +} diff --git a/sdk/identity/Azure.Identity/tests/samples/UserAuthenticationSnippets.cs b/sdk/identity/Azure.Identity/tests/samples/UserAuthenticationSnippets.cs new file mode 100644 index 000000000000..2c6fa2619f1e --- /dev/null +++ b/sdk/identity/Azure.Identity/tests/samples/UserAuthenticationSnippets.cs @@ -0,0 +1,126 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.IO; +using System.Threading.Tasks; +using Azure.Core; +using Azure.Security.KeyVault.Secrets; +using Azure.Storage.Blobs; + +namespace Azure.Identity.Samples +{ + public class UserAuthenticationSnippets + { + public void Identity_ClientSideUserAuthentication_SimpleInteractiveBrowser() + { + #region Snippet:Identity_ClientSideUserAuthentication_SimpleInteractiveBrowser + var client = new SecretClient(new Uri("https://myvault.azure.vaults.net/"), new InteractiveBrowserCredential()); + #endregion + } + + public void Identity_ClientSideUserAuthentication_SimpleDeviceCode() + { + #region Snippet:Identity_ClientSideUserAuthentication_SimpleDeviceCode + var credential = new DeviceCodeCredential(); + + var client = new BlobClient(new Uri("https://myaccount.blob.core.windows.net/mycontainer/myblob"), credential); + #endregion + } + + public async Task Identity_ClientSideUserAuthentication_DisableAutomaticAuthentication() + { + #region Snippet:Identity_ClientSideUserAuthentication_DisableAutomaticAuthentication + var credential = new InteractiveBrowserCredential(new InteractiveBrowserCredentialOptions { DisableAutomaticAuthentication = true }); + + await credential.AuthenticateAsync(); + + var client = new SecretClient(new Uri("https://myvault.azure.vaults.net/"), credential); + #endregion + + #region Snippet:Identity_ClientSideUserAuthentication_DisableAutomaticAuthentication_ExHandling + try + { + client.GetSecret("secret"); + } + catch (AuthenticationRequiredException e) + { + await EnsureAnimationCompleteAsync(); + + await credential.AuthenticateAsync(e.TokenRequestContext); + + client.GetSecret("secret"); + } + #endregion + } + + private Task EnsureAnimationCompleteAsync() => Task.CompletedTask; + + private const string AUTH_RECORD_PATH = @".\Data\authrecord.bin"; + + + public static async Task GetUserCredentialAsync() + { + if (!File.Exists(AUTH_RECORD_PATH)) + { + + #region Snippet:Identity_ClientSideUserAuthentication_Persist_TokenCache + var credential = new InteractiveBrowserCredential(new InteractiveBrowserCredentialOptions { TokenCache = new PersistentTokenCache() }); + #endregion + + #region Snippet:Identity_ClientSideUserAuthentication_Persist_AuthRecord + AuthenticationRecord authRecord = await credential.AuthenticateAsync(); + + using var authRecordStream = new FileStream(AUTH_RECORD_PATH, FileMode.Create, FileAccess.Write); + + await authRecord.SerializeAsync(authRecordStream); + + await authRecordStream.FlushAsync(); + #endregion + + return credential; + } + else + { + #region Snippet:Identity_ClientSideUserAuthentication_Persist_SilentAuth + using var authRecordStream = new FileStream(AUTH_RECORD_PATH, FileMode.Open, FileAccess.Read); + + AuthenticationRecord authRecord = await AuthenticationRecord.DeserializeAsync(authRecordStream); + + var credential = new InteractiveBrowserCredential(new InteractiveBrowserCredentialOptions { TokenCache = new PersistentTokenCache(), AuthenticationRecord = authRecord }); + #endregion + + return credential; + } + } + + + public static async Task Main() + { + InteractiveBrowserCredential credential; + + if (!File.Exists(AUTH_RECORD_PATH)) + { + credential = new InteractiveBrowserCredential(new InteractiveBrowserCredentialOptions { TokenCache = new PersistentTokenCache() }); + + AuthenticationRecord authRecord = await credential.AuthenticateAsync(); + + using var authRecordStream = new FileStream(AUTH_RECORD_PATH, FileMode.Create, FileAccess.Write); + + await authRecord.SerializeAsync(authRecordStream); + + await authRecordStream.FlushAsync(); + } + else + { + using var authRecordStream = new FileStream(AUTH_RECORD_PATH, FileMode.Open, FileAccess.Read); + + AuthenticationRecord authRecord = await AuthenticationRecord.DeserializeAsync(authRecordStream); + + credential = new InteractiveBrowserCredential(new InteractiveBrowserCredentialOptions { TokenCache = new PersistentTokenCache(), AuthenticationRecord = authRecord }); + } + + var client = new SecretClient(new Uri("https://myvault.azure.vaults.net/"), credential); + } + } +}