Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implemement OnBehalfOfCredential #22146

Merged
merged 31 commits into from
Sep 4, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
264bff6
SupportsCaching property, OBO credential
christothes Apr 28, 2021
1eaca9f
export
christothes Jun 24, 2021
3abc6a3
BAP test
christothes Jun 24, 2021
568908a
missing xml docs
christothes Jun 24, 2021
5287642
CredentialTestBase
christothes Jun 25, 2021
66adcea
add new virtual to UnsafeTokenCacheOptions
christothes Jun 29, 2021
ac08680
refactor UserAssertionScope
christothes Jun 29, 2021
0bb3d6a
merge
christothes Jun 29, 2021
1a7b5c1
export
christothes Jun 30, 2021
ae2b022
UnsafeTokenCacheOptions and TokenCacheNotificationDetails
christothes Jun 30, 2021
05fee10
tweaks
christothes Jun 30, 2021
cc4ac48
Merge remote-tracking branch 'upstream/main' into chriss/OnBehalfOf
christothes Jul 12, 2021
5029352
Merge remote-tracking branch 'upstream/main' into chriss/OnBehalfOf
christothes Jul 19, 2021
64d2788
fb
christothes Jul 19, 2021
4a68ef1
refactor using AccessToken.RefreshOn
christothes Aug 12, 2021
6be7493
export
christothes Aug 12, 2021
7f52c10
fb
christothes Aug 12, 2021
8a6be33
tweaks
christothes Aug 12, 2021
0440c00
remove comment
christothes Aug 12, 2021
52fe1b1
Merge remote-tracking branch 'upstream/main' into chriss/OnBehalfOf
christothes Aug 16, 2021
916eed1
Merge branch 'chriss/OnBehalfOf' of https://github.com/christothes/az…
christothes Aug 16, 2021
d7b582d
use DateTimeOffset.MinValue as no-cache indicator
christothes Aug 16, 2021
3b10559
protect against datetimeoffset underflow
christothes Aug 17, 2021
cecffe1
update RefreshOn doc comment
christothes Aug 18, 2021
cc7fd2f
Simple OBO
christothes Aug 27, 2021
bab4afc
revert core changes
christothes Aug 27, 2021
837d1d6
proj tweak
christothes Aug 27, 2021
ea9066f
merge
christothes Aug 27, 2021
baed5fe
more ctor overloads
christothes Aug 30, 2021
cf193ab
fb
christothes Sep 3, 2021
aea16af
merge
christothes Sep 3, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this feature deserves a sample.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed - I plan to add one in a follow up PR


```xml
<ItemGroup>
Expand Down
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