Skip to content

Commit

Permalink
[Identity] Adding ClientCertificateCredential and custom credential s…
Browse files Browse the repository at this point in the history
…amples (#18722)
  • Loading branch information
schaabs authored Feb 22, 2021
1 parent fa3e4b9 commit 7c62728
Show file tree
Hide file tree
Showing 5 changed files with 528 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
# Using the ClientCertificateCredential

Applications which execute in a protected environment can authenticate using a client assertion signed by a private key whose public key or root certificate is registered with AAD. The Azure.Identity library provides the `ClientCertificateCredential` for applications choosing to authenticate this way. Below are some examples of how applications can utilize the `ClientCertificateCredential` to authenticate clients.


## Loading certificates from disk

Applications commonly need to load a client certificate from disk. One approach is for the application to construct the `ClientCertificateCredential` by specifying the applications tenant id, client id, and the path to the certificate.

```C# Snippet:Identity_CertificateCredenetial_CreateWithPath
var credential = new ClientCertificateCredential(tenantId, clientId, "./certs/cert.pfx");
```
Alternatively, the application can construct the `X509Certificate2` themselves, such as in the following example, where the certificate key is password protected.

```C# Snippet:Identity_CertificateCredenetial_CreateWithX509Cert
var certificate = new X509Certificate2("./certs/cert-password-protected.pfx", "password");

var credential = new ClientCertificateCredential(tenantId, clientId, certificate);
```

## Loading certificates from an X509Store

Applications running on platforms which provide a secure certificate store might prefer to store and retrieve certificates from there. While the `ClientCertificateCredential` doesn't directly provide a mechanism for this, the application can retrieve the appropriate certificate from the store and use it to construct the `ClientCertificateCredential`.

Consider the scenario where a pinned certificate used for development authentication is stored in the Personal certificate store. Since the certificate is pinned it can be identified by its thumbprint, which the application might read from configuration or the environment.

```C# Snippet:Identity_CertificateCredenetial_CreateFromStore
using var store = new X509Store(StoreName.My, StoreLocation.LocalMachine);

store.Open(OpenFlags.ReadOnly);

var certificate = store.Certificates.Cast<X509Certificate2>().FirstOrDefault(cert => cert.Thumbprint == thumbprint);

var credential = new ClientCertificateCredential(tenantId, clientId, certificate);
```

## Rolling Certificates

Long running applications may have the need to roll certificates during process execution. Certificate rotation is not currently supported by the `ClientCertficateCredential` which treats the certificate used to construct the credential as immutable. This means that any clients constructed with an `ClientCertificateCredential` using a particular cert would fail to authenticate requests after that cert has been rolled and the original is no longer valid.

However, if an application wants to roll this certificate without creating new service clients, it can accomplish this by creating its own `TokenCredential` implementation which wraps the `ClientCertificateCredential`. The implementation of this custom credential `TokenCredential` would somewhat depend on how the application handles certificate rotation.

### Explicit rotation

If the application get's notified of certificate rotations and it can directly respond, it might choose to wrap the `ClientCertificateCredential` in a custom credential which provides a means for rotating the certificate.

```C# Snippet:Identity_CertificateCredenetial_RotatableCredential
public class RotatableCertificateCredential : TokenCredential
{
private readonly string _tenantId;
private readonly string _clientId;
private ClientCertificateCredential _credential;

public RotatableCertificateCredential(string tenantId, string clientId, X509Certificate2 certificate)
{
_tenantId = tenantId;
_clientId = clientId;
_credential = new ClientCertificateCredential(_tenantId, _clientId, certificate);
}

public override AccessToken GetToken(TokenRequestContext requestContext, CancellationToken cancellationToken)
{
return _credential.GetToken(requestContext, cancellationToken);
}

public async override ValueTask<AccessToken> GetTokenAsync(TokenRequestContext requestContext, CancellationToken cancellationToken)
{
return await _credential.GetTokenAsync(requestContext, cancellationToken);
}

public void RotateCertificate(X509Certificate2 certificate)
{
_credential = new ClientCertificateCredential(_tenantId, _clientId, certificate);
}
}
```

The above example shows a custom credential type `RotatableCertificateCredential` which provides a `RotateCertificateMethod`. The implementation internally relies on a `ClientCertificateCredential` instance `_credential`, and `RotateCertificate` simply replaces this instance with a new instance using the updated certificate.

### Implicit rotation
Some applications might want to respond to certificate rotations which are external to the application, for instance a separate process rotates the certificate by updating it on disk. Here the application create a custom credential which checks for certificate updates when tokens are requested.

```C# Snippet:Identity_CertificateCredenetial_RotatingCredential
public class RotatingCertificateCredential : TokenCredential
{
private readonly string _tenantId;
private readonly string _clientId;
private readonly string _path;
private readonly object _refreshLock = new object();
private DateTimeOffset _credentialLastModified;
private ClientCertificateCredential _credential;

public RotatingCertificateCredential(string tenantId, string clientId, string path)
{
_tenantId = tenantId;
_clientId = clientId;
_path = path;

RefreshCertificate();
}

public override AccessToken GetToken(TokenRequestContext requestContext, CancellationToken cancellationToken)
{
RefreshCertificate();

return _credential.GetToken(requestContext, cancellationToken);
}

public async override ValueTask<AccessToken> GetTokenAsync(TokenRequestContext requestContext, CancellationToken cancellationToken)
{
RefreshCertificate();

return await _credential.GetTokenAsync(requestContext, cancellationToken);
}

public void RefreshCertificate()
{
lock (_refreshLock)
{
var certificateLastModified = File.GetLastWriteTimeUtc(_path);

if (_credentialLastModified < certificateLastModified)
{
_credential = new ClientCertificateCredential(_tenantId, _clientId, new X509Certificate2(_path));

_credentialLastModified = certificateLastModified;
}
}
}
}
```

In this example the custom credential type `RotatingCertifiateCredential` again uses a `ClientCertificateCredential` instance `_credential` to retrieve tokens. However, in this case it will attempt to refresh the certificate prior to obtaining the token. The method `RefreshCertificate` will query to see if the certificate has changed, and if so it will replace the instance `_credential` with a new instance using the new certificate.
123 changes: 123 additions & 0 deletions sdk/identity/Azure.Identity/samples/DefiningCustomCredentialTypes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
# Defining Custom Credential types
The Azure.Identity library covers a broad range of Azure Active Directory authentication scenarios. However, it's possible the credential implementations in Azure.Identity might not meet the specific needs your application, or an application might want to avoid taking a dependency on the Azure.Identity library.

## Authenticating with a prefetched access token
The Azure.Identity library does not contain a `TokenCredential` implementation which can be constructed directly with an `AccessToken`. This is intentionally omitted as a main line scenario as access tokens expire frequently and have constrained usage. However, there are some scenarios where authenticating a service client with a prefetched token is necessary.

In this example `StaticTokenCredential` implements the `TokenCredential` abstraction. It takes a prefetched access token in its constructor as a `string` or `AccessToken`, and simply returns that from its implementation of `GetToken` and `GetTokenAsync`.

```C# Snippet:StaticTokenCredential
public class StaticTokenCredential : TokenCredential
{
private AccessToken _token;

public StaticTokenCredential(string token) : this(new AccessToken(token, DateTimeOffset.MinValue)) { }

public StaticTokenCredential(AccessToken token)
{
_token = token;
}

public override AccessToken GetToken(TokenRequestContext requestContext, CancellationToken cancellationToken)
{
return _token;
}

public override ValueTask<AccessToken> GetTokenAsync(TokenRequestContext requestContext, CancellationToken cancellationToken)
{
return new ValueTask<AccessToken>(_token);
}
}
```

Once the application has defined this credential type instances of it can be used to authenticate Azure SDK clients. The following example shows an how an application already using some other mechanism for acquiring tokens (in this case the hypothetical method `AquireTokenForScope`) could use the `StaticTokenCredential` to authenticate a `BlobClient`.

```C# Snippet:StaticTokenCredentialUsage
string token = GetTokenForScope("https://storage.azure.com/.default");

var credential = new StaticTokenCredential(token);

var client = new BlobClient(new Uri("https://aka.ms/bloburl"), credential);
```

It should be noted when using this custom credential type, it is the responsibility of the caller to ensure that the token is valid, and contains the correct claims needed to authenticate calls from the particular service client. For instance in the above case the token must have the scope "https://storage.azure.com/.default" to authorize calls to Azure Blob Storage.

## Authenticating with MSAL Directly

Some applications already use the MSAL library's `IConfidentialClientApplication` or `IPublicClientApplication` to authenticate portions of their application. In these cases the application might want to use the same to authenticate Azure SDK clients so to take advantage of token caching the client application is doing and prevent unnecessary authentication calls or user prompting.

### Authenticating with the Confidential Client

In this example the `ConfidentialClientApplicationCredential` is constructed with an instance of `IConfidentialClientApplication` it then implements `GetToken` and `GetTokenAsync` using the `AcquireTokenForClient` method to acquire a token.

```C# Snippet:ConfidentialClientCredential
public class ConfidentialClientCredential : TokenCredential
{
private readonly IConfidentialClientApplication _confidentialClient;

public ConfidentialClientCredential(IConfidentialClientApplication confidentialClient)
{
_confidentialClient = confidentialClient;
}

public override AccessToken GetToken(TokenRequestContext requestContext, CancellationToken cancellationToken)
{
return GetTokenAsync(requestContext, cancellationToken).GetAwaiter().GetResult();
}

public override async ValueTask<AccessToken> GetTokenAsync(TokenRequestContext requestContext, CancellationToken cancellationToken)
{
AuthenticationResult result = await _confidentialClient.AcquireTokenForClient(requestContext.Scopes).ExecuteAsync();

return new AccessToken(result.AccessToken, result.ExpiresOn);
}
}
```

The users could then use the `ConfidentialClientApplicationCredential` be used to authenticate a `BlobClient` with an `IConfidentialClientApplication` it has created to authenticate non Azure SDK calls.

```C# Snippet:ConfidentialClientCredentialUsage
IConfidentialClientApplication confidentialClient = ConfidentialClientApplicationBuilder.Create(clientId).WithClientSecret(clientSecret).Build();

var client = new SecretClient(new Uri("https://myvault.vault.azure.net/"), new ConfidentialClientCredential(confidentialClient));
```

## Authenticating with the On Behalf Of Flow
Currently the Azure.Identity library doesn't provide a credential type for clients which need to authenticate via the On Behalf Of flow. While future support for this is planned, users requiring this immediately will have to implement their own `TokenCredential` class.

In this example the `OnBehalfOfCredential` accepts a client id, client secret, and a user's access token. It then creates an instance of `IConfidentialClientApplication` from the Microsoft.Identity.Client library (MSAL) to obtain an OBO token which can be used to authenticate client requests.

```C# Snippet:OnBehalfOfCredential
public class OnBehalfOfCredential : TokenCredential
{
private readonly IConfidentialClientApplication _confidentialClient;
private readonly UserAssertion _userAssertion;

public OnBehalfOfCredential(string clientId, string clientSecret, string userAccessToken)
{
_confidentialClient = ConfidentialClientApplicationBuilder.Create(clientId).WithClientSecret(clientSecret).Build();

_userAssertion = new UserAssertion(userAccessToken);
}

public override AccessToken GetToken(TokenRequestContext requestContext, CancellationToken cancellationToken)
{
return GetTokenAsync(requestContext, cancellationToken).GetAwaiter().GetResult();
}

public override async ValueTask<AccessToken> GetTokenAsync(TokenRequestContext requestContext, CancellationToken cancellationToken)
{
AuthenticationResult result = await _confidentialClient.AcquireTokenOnBehalfOf(requestContext.Scopes, _userAssertion).ExecuteAsync();

return new AccessToken(result.AccessToken, result.ExpiresOn);
}
}
```
The following example shows an how the `OnBehalfOfCredential` could be used to authenticate a `SecretClient`.


```C# Snippet:OnBehalfOfCredentialUsage
var oboCredential = new OnBehalfOfCredential(clientId, clientSecret, userAccessToken);

var client = new SecretClient(new Uri("https://myvault.vault.azure.net/"), oboCredential);
```
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
<ProjectReference Include="$(AzureCoreTestFramework)" />
<ProjectReference Include="..\src\Azure.Identity.csproj" />
<None Update="Data\*" CopyToOutputDirectory="PreserveNewest" />
<ContentWithTargetPath Update="Data\*" CopyToOutputDirectory="PreserveNewest" TargetPath="certs\*" />
</ItemGroup>

</Project>
Loading

0 comments on commit 7c62728

Please sign in to comment.