Skip to content

Commit

Permalink
Implemement OnBehalfOfCredential (#22146)
Browse files Browse the repository at this point in the history
* Simple OBO
  • Loading branch information
christothes authored Sep 4, 2021
1 parent bcc61ba commit 7649803
Show file tree
Hide file tree
Showing 29 changed files with 877 additions and 666 deletions.
3 changes: 3 additions & 0 deletions sdk/identity/Azure.Identity/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

### Features Added

- Implement `OnBehalfOfCredential` which enables authentication to Azure Active Directory using an On-Behalf-Of flow.

### Breaking Changes

### Bugs Fixed
Expand Down Expand Up @@ -43,6 +45,7 @@ Thank you to our developer community members who helped to make Azure Identity b
- Added support to `ManagedIdentityCredential` for Bridge to Kubernetes local development authentication.
- TenantId values returned from service challenge responses can now be used to request tokens from the correct tenantId. To support this feature, there is a new `AllowMultiTenantAuthentication` option on `TokenCredentialOptions`.
- By default, `AllowMultiTenantAuthentication` is false. When this option property is false and the tenant Id configured in the credential options differs from the tenant Id set in the `TokenRequestContext` sent to a credential, an `AuthorizationFailedException` will be thrown. This is potentially breaking change as it could be a different exception than what was thrown previously. This exception behavior can be overridden by either setting an `AppContext` switch named "Azure.Identity.EnableLegacyTenantSelection" to `true` or by setting the environment variable "AZURE_IDENTITY_ENABLE_LEGACY_TENANT_SELECTION" to "true". Note: AppContext switches can also be configured via configuration like below:
- Added `OnBehalfOfFlowCredential` which enables support for AAD On-Behalf-Of (OBO) flow. See the [Azure Active Directory documentation](https://docs.microsoft.com/azure/active-directory/develop/v2-oauth2-on-behalf-of-flow) to learn more about OBO flow scenarios.

```xml
<ItemGroup>
Expand Down
29 changes: 29 additions & 0 deletions sdk/identity/Azure.Identity/api/Azure.Identity.netstandard2.0.cs
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,22 @@ 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<Azure.Core.AccessToken> GetTokenAsync(Azure.Core.TokenRequestContext requestContext, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
}
public partial class OnBehalfOfCredential : Azure.Core.TokenCredential
{
protected OnBehalfOfCredential() { }
public OnBehalfOfCredential(string tenantId, string clientId, System.Security.Cryptography.X509Certificates.X509Certificate2 clientCertificate, string userAssertion) { }
public OnBehalfOfCredential(string tenantId, string clientId, System.Security.Cryptography.X509Certificates.X509Certificate2 clientCertificate, string userAssertion, Azure.Identity.OnBehalfOfCredentialOptions options) { }
public OnBehalfOfCredential(string tenantId, string clientId, string clientSecret, string userAssertion, Azure.Identity.OnBehalfOfCredentialOptions options = null) { }
public override Azure.Core.AccessToken GetToken(Azure.Core.TokenRequestContext requestContext, System.Threading.CancellationToken cancellationToken) { throw null; }
public override System.Threading.Tasks.ValueTask<Azure.Core.AccessToken> GetTokenAsync(Azure.Core.TokenRequestContext requestContext, System.Threading.CancellationToken cancellationToken) { throw null; }
}
public partial class OnBehalfOfCredentialOptions : Azure.Identity.TokenCredentialOptions
{
public OnBehalfOfCredentialOptions() { }
public Azure.Identity.RegionalAuthority? RegionalAuthority { get { throw null; } set { } }
public bool SendCertificateChain { get { throw null; } set { } }
public Azure.Identity.TokenCachePersistenceOptions TokenCachePersistenceOptions { get { throw null; } set { } }
}
[System.Runtime.InteropServices.StructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Sequential)]
public readonly partial struct RegionalAuthority : System.IEquatable<Azure.Identity.RegionalAuthority>
{
Expand Down Expand Up @@ -326,6 +342,18 @@ public SharedTokenCacheCredentialOptions(Azure.Identity.TokenCachePersistenceOpt
public Azure.Identity.TokenCachePersistenceOptions TokenCachePersistenceOptions { get { throw null; } }
public string Username { get { throw null; } set { } }
}
[System.Runtime.InteropServices.StructLayoutAttribute(System.Runtime.InteropServices.LayoutKind.Sequential)]
public partial struct TokenCacheDetails
{
private object _dummy;
private int _dummyPrimitive;
public System.ReadOnlyMemory<byte> CacheBytes { get { throw null; } set { } }
}
public partial class TokenCacheNotificationDetails
{
internal TokenCacheNotificationDetails() { }
public string SuggestedCacheKey { get { throw null; } }
}
public partial class TokenCachePersistenceOptions
{
public TokenCachePersistenceOptions() { }
Expand All @@ -348,6 +376,7 @@ public abstract partial class UnsafeTokenCacheOptions : Azure.Identity.TokenCach
{
protected UnsafeTokenCacheOptions() { }
protected internal abstract System.Threading.Tasks.Task<System.ReadOnlyMemory<byte>> RefreshCacheAsync();
protected internal virtual System.Threading.Tasks.Task<Azure.Identity.TokenCacheDetails> RefreshCacheAsync(Azure.Identity.TokenCacheNotificationDetails details) { throw null; }
protected internal abstract System.Threading.Tasks.Task TokenCacheUpdatedAsync(Azure.Identity.TokenCacheUpdatedArgs tokenCacheUpdatedArgs);
}
public partial class UsernamePasswordCredential : Azure.Core.TokenCredential
Expand Down
211 changes: 44 additions & 167 deletions sdk/identity/Azure.Identity/src/ClientCertificateCredential.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,7 @@ public class ClientCertificateCredential : TokenCredential
/// Protected constructor for mocking.
/// </summary>
protected ClientCertificateCredential()
{
}
{ }

/// <summary>
/// Creates an instance of the ClientCertificateCredential with the details needed to authenticate against Azure Active Directory with the specified certificate.
Expand All @@ -52,8 +51,7 @@ protected ClientCertificateCredential()
/// <param name="clientCertificatePath">The path to a file which contains both the client certificate and private key.</param>
public ClientCertificateCredential(string tenantId, string clientId, string clientCertificatePath)
: this(tenantId, clientId, clientCertificatePath, null, null, null)
{
}
{ }

/// <summary>
/// Creates an instance of the ClientCertificateCredential with the details needed to authenticate against Azure Active Directory with the specified certificate.
Expand All @@ -64,8 +62,7 @@ public ClientCertificateCredential(string tenantId, string clientId, string clie
/// <param name="options">Options that allow to configure the management of the requests sent to the Azure Active Directory service.</param>
public ClientCertificateCredential(string tenantId, string clientId, string clientCertificatePath, TokenCredentialOptions options)
: this(tenantId, clientId, clientCertificatePath, options, null, null)
{
}
{ }

/// <summary>
/// Creates an instance of the ClientCertificateCredential with the details needed to authenticate against Azure Active Directory with the specified certificate.
Expand All @@ -86,8 +83,7 @@ public ClientCertificateCredential(string tenantId, string clientId, string clie
/// <param name="clientCertificate">The authentication X509 Certificate of the service principal</param>
public ClientCertificateCredential(string tenantId, string clientId, X509Certificate2 clientCertificate)
: this(tenantId, clientId, clientCertificate, null, null, null)
{
}
{ }

/// <summary>
/// Creates an instance of the ClientCertificateCredential with the details needed to authenticate against Azure Active Directory with the specified certificate.
Expand All @@ -97,7 +93,8 @@ public ClientCertificateCredential(string tenantId, string clientId, X509Certifi
/// <param name="clientCertificate">The authentication X509 Certificate of the service principal</param>
/// <param name="options">Options that allow to configure the management of the requests sent to the Azure Active Directory service.</param>
public ClientCertificateCredential(string tenantId, string clientId, X509Certificate2 clientCertificate, TokenCredentialOptions options)
: this(tenantId, clientId, clientCertificate, options, null, null) {}
: this(tenantId, clientId, clientCertificate, options, null, null)
{ }

/// <summary>
/// Creates an instance of the ClientCertificateCredential with the details needed to authenticate against Azure Active Directory with the specified certificate.
Expand All @@ -108,20 +105,47 @@ public ClientCertificateCredential(string tenantId, string clientId, X509Certifi
/// <param name="options">Options that allow to configure the management of the requests sent to the Azure Active Directory service.</param>
public ClientCertificateCredential(string tenantId, string clientId, X509Certificate2 clientCertificate, ClientCertificateCredentialOptions options)
: this(tenantId, clientId, clientCertificate, options, null, null)
{
}
{ }

internal ClientCertificateCredential(string tenantId, string clientId, string certificatePath, TokenCredentialOptions options, CredentialPipeline pipeline, MsalConfidentialClient client)
: this(tenantId, clientId, new X509Certificate2FromFileProvider(certificatePath ?? throw new ArgumentNullException(nameof(certificatePath))), options, pipeline, client)
{
}
internal ClientCertificateCredential(
string tenantId,
string clientId,
string certificatePath,
TokenCredentialOptions options,
CredentialPipeline pipeline,
MsalConfidentialClient client)
: this(
tenantId,
clientId,
new X509Certificate2FromFileProvider(certificatePath ?? throw new ArgumentNullException(nameof(certificatePath))),
options,
pipeline,
client)
{ }

internal ClientCertificateCredential(string tenantId, string clientId, X509Certificate2 certificate, TokenCredentialOptions options, CredentialPipeline pipeline, MsalConfidentialClient client)
: this(tenantId, clientId, new X509Certificate2FromObjectProvider(certificate ?? throw new ArgumentNullException(nameof(certificate))), options, pipeline, client)
{
}
internal ClientCertificateCredential(
string tenantId,
string clientId,
X509Certificate2 certificate,
TokenCredentialOptions options,
CredentialPipeline pipeline,
MsalConfidentialClient client)
: this(
tenantId,
clientId,
new X509Certificate2FromObjectProvider(certificate ?? throw new ArgumentNullException(nameof(certificate))),
options,
pipeline,
client)
{ }

internal ClientCertificateCredential(string tenantId, string clientId, IX509Certificate2Provider certificateProvider, TokenCredentialOptions options, CredentialPipeline pipeline, MsalConfidentialClient client)
internal ClientCertificateCredential(
string tenantId,
string clientId,
IX509Certificate2Provider certificateProvider,
TokenCredentialOptions options,
CredentialPipeline pipeline,
MsalConfidentialClient client)
{
TenantId = Validations.ValidateTenantId(tenantId, nameof(tenantId));

Expand Down Expand Up @@ -193,152 +217,5 @@ public override async ValueTask<AccessToken> GetTokenAsync(TokenRequestContext r
throw scope.FailWrapAndThrow(e);
}
}

/// <summary>
/// IX509Certificate2Provider provides a way to control how the X509Certificate2 object is fetched.
/// </summary>
internal interface IX509Certificate2Provider
{
ValueTask<X509Certificate2> GetCertificateAsync(bool async, CancellationToken cancellationToken);
}

/// <summary>
/// X509Certificate2FromObjectProvider provides an X509Certificate2 from an existing instance.
/// </summary>
private class X509Certificate2FromObjectProvider : IX509Certificate2Provider
{
private X509Certificate2 Certificate { get; }

public X509Certificate2FromObjectProvider(X509Certificate2 clientCertificate)
{
Certificate = clientCertificate ?? throw new ArgumentNullException(nameof(clientCertificate));
}

public ValueTask<X509Certificate2> GetCertificateAsync(bool async, CancellationToken cancellationToken)
{
return new ValueTask<X509Certificate2>(Certificate);
}
}

/// <summary>
/// X509Certificate2FromFileProvider provides an X509Certificate2 from a file on disk. It supports both
/// "pfx" and "pem" encoded certificates.
/// </summary>
internal class X509Certificate2FromFileProvider : IX509Certificate2Provider
{
// Lazy initialized on the first call to GetCertificateAsync, based on CertificatePath.
private X509Certificate2 Certificate { get; set; }
internal string CertificatePath { get; }

public X509Certificate2FromFileProvider(string clientCertificatePath)
{
CertificatePath = clientCertificatePath ?? throw new ArgumentNullException(nameof(clientCertificatePath));
}

public ValueTask<X509Certificate2> GetCertificateAsync(bool async, CancellationToken cancellationToken)
{
if (!(Certificate is null))
{
return new ValueTask<X509Certificate2>(Certificate);
}

string fileType = Path.GetExtension(CertificatePath);

switch (fileType.ToLowerInvariant())
{
case ".pfx":
return LoadCertificateFromPfxFileAsync(async, CertificatePath, cancellationToken);
case ".pem":
return LoadCertificateFromPemFileAsync(async, CertificatePath, cancellationToken);
default:
throw new CredentialUnavailableException("Only .pfx and .pem files are supported.");
}
}

private async ValueTask<X509Certificate2> LoadCertificateFromPfxFileAsync(bool async, string clientCertificatePath, CancellationToken cancellationToken)
{
const int BufferSize = 4 * 1024;

if (!(Certificate is null))
{
return Certificate;
}

try
{
if (!async)
{
Certificate = new X509Certificate2(clientCertificatePath);
}
else
{
List<byte> certContents = new List<byte>();
byte[] buf = new byte[BufferSize];
int offset = 0;
using (Stream s = File.OpenRead(clientCertificatePath))
{
while (true)
{
int read = await s.ReadAsync(buf, offset, buf.Length, cancellationToken).ConfigureAwait(false);
for (int i = 0; i < read; i++)
{
certContents.Add(buf[i]);
}

if (read == 0)
{
break;
}
}
}

Certificate = new X509Certificate2(certContents.ToArray());
}

return Certificate;
}
catch (Exception e) when (!(e is OperationCanceledException))
{
throw new CredentialUnavailableException("Could not load certificate file", e);
}
}

private async ValueTask<X509Certificate2> LoadCertificateFromPemFileAsync(bool async, string clientCertificatePath, CancellationToken cancellationToken)
{
if (!(Certificate is null))
{
return Certificate;
}

string certficateText;

try
{
if (!async)
{
certficateText = File.ReadAllText(clientCertificatePath);
}
else
{
cancellationToken.ThrowIfCancellationRequested();

using (StreamReader sr = new StreamReader(clientCertificatePath))
{
certficateText = await sr.ReadToEndAsync().ConfigureAwait(false);
}
}

Certificate = PemReader.LoadCertificate(certficateText.AsSpan(), keyType: PemReader.KeyType.RSA);

return Certificate;
}
catch (Exception e) when (!(e is OperationCanceledException))
{
throw new CredentialUnavailableException("Could not load certificate file", e);
}
}

private delegate void ImportPkcs8PrivateKeyDelegate(ReadOnlySpan<byte> blob, out int bytesRead);
}
}
}
17 changes: 17 additions & 0 deletions sdk/identity/Azure.Identity/src/IX509Certificate2Provider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System.Security.Cryptography.X509Certificates;
using System.Threading;
using System.Threading.Tasks;

namespace Azure.Identity
{
/// <summary>
/// IX509Certificate2Provider provides a way to control how the X509Certificate2 object is fetched.
/// </summary>
internal interface IX509Certificate2Provider
{
ValueTask<X509Certificate2> GetCertificateAsync(bool async, CancellationToken cancellationToken);
}
}
Loading

0 comments on commit 7649803

Please sign in to comment.