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

StaticTokenCredential #22955

Merged
merged 9 commits into from
Sep 3, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions sdk/core/Azure.Core/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

### Features Added

- Added `TokenCredential.Create` static method, which returns an instance of `TokenCredential` that uses the supplied delgates to produce an `AccessToken`. This would most typically be used when an token has previously been obtained from some other source and that token needs to be returned by a `TokenCredential` instance.

### Breaking Changes

### Bugs Fixed
Expand Down
3 changes: 3 additions & 0 deletions sdk/core/Azure.Core/api/Azure.Core.net461.cs
Original file line number Diff line number Diff line change
Expand Up @@ -463,6 +463,9 @@ internal RetryOptions() { }
public abstract partial class TokenCredential
{
protected TokenCredential() { }
public static Azure.Core.TokenCredential Create(System.Func<Azure.Core.TokenRequestContext, System.Threading.CancellationToken, Azure.Core.AccessToken> getToken) { throw null; }
public static Azure.Core.TokenCredential Create(System.Func<Azure.Core.TokenRequestContext, System.Threading.CancellationToken, Azure.Core.AccessToken> getToken, System.Func<Azure.Core.TokenRequestContext, System.Threading.CancellationToken, System.Threading.Tasks.ValueTask<Azure.Core.AccessToken>> getTokenAsync) { throw null; }
public static Azure.Core.TokenCredential Create(System.Func<Azure.Core.TokenRequestContext, System.Threading.CancellationToken, System.Threading.Tasks.ValueTask<Azure.Core.AccessToken>> getTokenAsync) { throw null; }
public abstract Azure.Core.AccessToken GetToken(Azure.Core.TokenRequestContext requestContext, System.Threading.CancellationToken cancellationToken);
public abstract System.Threading.Tasks.ValueTask<Azure.Core.AccessToken> GetTokenAsync(Azure.Core.TokenRequestContext requestContext, System.Threading.CancellationToken cancellationToken);
}
Expand Down
3 changes: 3 additions & 0 deletions sdk/core/Azure.Core/api/Azure.Core.net5.0.cs
Original file line number Diff line number Diff line change
Expand Up @@ -463,6 +463,9 @@ internal RetryOptions() { }
public abstract partial class TokenCredential
{
protected TokenCredential() { }
public static Azure.Core.TokenCredential Create(System.Func<Azure.Core.TokenRequestContext, System.Threading.CancellationToken, Azure.Core.AccessToken> getToken) { throw null; }
public static Azure.Core.TokenCredential Create(System.Func<Azure.Core.TokenRequestContext, System.Threading.CancellationToken, Azure.Core.AccessToken> getToken, System.Func<Azure.Core.TokenRequestContext, System.Threading.CancellationToken, System.Threading.Tasks.ValueTask<Azure.Core.AccessToken>> getTokenAsync) { throw null; }
public static Azure.Core.TokenCredential Create(System.Func<Azure.Core.TokenRequestContext, System.Threading.CancellationToken, System.Threading.Tasks.ValueTask<Azure.Core.AccessToken>> getTokenAsync) { throw null; }
public abstract Azure.Core.AccessToken GetToken(Azure.Core.TokenRequestContext requestContext, System.Threading.CancellationToken cancellationToken);
public abstract System.Threading.Tasks.ValueTask<Azure.Core.AccessToken> GetTokenAsync(Azure.Core.TokenRequestContext requestContext, System.Threading.CancellationToken cancellationToken);
}
Expand Down
3 changes: 3 additions & 0 deletions sdk/core/Azure.Core/api/Azure.Core.netcoreapp2.1.cs
Original file line number Diff line number Diff line change
Expand Up @@ -463,6 +463,9 @@ internal RetryOptions() { }
public abstract partial class TokenCredential
{
protected TokenCredential() { }
public static Azure.Core.TokenCredential Create(System.Func<Azure.Core.TokenRequestContext, System.Threading.CancellationToken, Azure.Core.AccessToken> getToken) { throw null; }
public static Azure.Core.TokenCredential Create(System.Func<Azure.Core.TokenRequestContext, System.Threading.CancellationToken, Azure.Core.AccessToken> getToken, System.Func<Azure.Core.TokenRequestContext, System.Threading.CancellationToken, System.Threading.Tasks.ValueTask<Azure.Core.AccessToken>> getTokenAsync) { throw null; }
public static Azure.Core.TokenCredential Create(System.Func<Azure.Core.TokenRequestContext, System.Threading.CancellationToken, System.Threading.Tasks.ValueTask<Azure.Core.AccessToken>> getTokenAsync) { throw null; }
public abstract Azure.Core.AccessToken GetToken(Azure.Core.TokenRequestContext requestContext, System.Threading.CancellationToken cancellationToken);
public abstract System.Threading.Tasks.ValueTask<Azure.Core.AccessToken> GetTokenAsync(Azure.Core.TokenRequestContext requestContext, System.Threading.CancellationToken cancellationToken);
}
Expand Down
3 changes: 3 additions & 0 deletions sdk/core/Azure.Core/api/Azure.Core.netstandard2.0.cs
Original file line number Diff line number Diff line change
Expand Up @@ -463,6 +463,9 @@ internal RetryOptions() { }
public abstract partial class TokenCredential
{
protected TokenCredential() { }
public static Azure.Core.TokenCredential Create(System.Func<Azure.Core.TokenRequestContext, System.Threading.CancellationToken, Azure.Core.AccessToken> getToken) { throw null; }
public static Azure.Core.TokenCredential Create(System.Func<Azure.Core.TokenRequestContext, System.Threading.CancellationToken, Azure.Core.AccessToken> getToken, System.Func<Azure.Core.TokenRequestContext, System.Threading.CancellationToken, System.Threading.Tasks.ValueTask<Azure.Core.AccessToken>> getTokenAsync) { throw null; }
public static Azure.Core.TokenCredential Create(System.Func<Azure.Core.TokenRequestContext, System.Threading.CancellationToken, System.Threading.Tasks.ValueTask<Azure.Core.AccessToken>> getTokenAsync) { throw null; }
public abstract Azure.Core.AccessToken GetToken(Azure.Core.TokenRequestContext requestContext, System.Threading.CancellationToken cancellationToken);
public abstract System.Threading.Tasks.ValueTask<Azure.Core.AccessToken> GetTokenAsync(Azure.Core.TokenRequestContext requestContext, System.Threading.CancellationToken cancellationToken);
}
Expand Down
82 changes: 82 additions & 0 deletions sdk/core/Azure.Core/src/TokenCredential.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Azure.Core.Pipeline;

namespace Azure.Core
{
Expand All @@ -29,5 +30,86 @@ public abstract class TokenCredential
/// <returns>A valid <see cref="AccessToken"/>.</returns>
/// <remarks>Caching and management of the lifespan for the <see cref="AccessToken"/> is considered the responsibility of the caller: each call should request a fresh token being requested.</remarks>
public abstract AccessToken GetToken(TokenRequestContext requestContext, CancellationToken cancellationToken);

/// <summary>
/// Creates a static <see cref="TokenCredential"/> that accepts delegates which will produce an <see cref="AccessToken"/>.
/// </summary>
/// <remarks>
/// Typically, the <see cref="TokenCredential"/> created by this method is for use when you have already obtained an <see cref="AccessToken"/>
/// from some other source and need a <see cref="TokenCredential"/> that will simply return that token. Because the static token can expire,
/// the delegates offer a mechanism to handle <see cref="AccessToken"/> renewal.
/// </remarks>
/// <param name="getToken">A delegate that returns an <see cref="AccessToken"/>.</param>
/// <param name="getTokenAsync">A delegate that returns a <see cref="ValueTask"/> of type <see cref="AccessToken"/>.</param>
/// <returns></returns>
public static TokenCredential Create(
Func<TokenRequestContext, CancellationToken, AccessToken> getToken,
Func<TokenRequestContext, CancellationToken, ValueTask<AccessToken>> getTokenAsync) => new StaticTokenCredential(getToken, getTokenAsync);

/// <summary>
/// Creates a static <see cref="TokenCredential"/> that accepts delegates which will produce an <see cref="AccessToken"/>.
/// </summary>
/// <remarks>
/// Typically, the <see cref="TokenCredential"/> created by this method is for use when you have already obtained an <see cref="AccessToken"/>
/// from some other source and need a <see cref="TokenCredential"/> that will simply return that token. Because the static token can expire,
/// the delegates offer a mechanism to handle <see cref="AccessToken"/> renewal.
/// </remarks>
/// <param name="getToken">A delegate that returns an <see cref="AccessToken"/>.</param>
/// <returns></returns>
public static TokenCredential Create(
Func<TokenRequestContext, CancellationToken, AccessToken> getToken) => new StaticTokenCredential(getToken);

/// <summary>
/// Creates a static <see cref="TokenCredential"/> that accepts delegates which will produce an <see cref="AccessToken"/>.
/// </summary>
/// <remarks>
/// Typically, the <see cref="TokenCredential"/> created by this method is for use when you have already obtained an <see cref="AccessToken"/>
/// from some other source and need a <see cref="TokenCredential"/> that will simply return that token. Because the static token can expire,
/// the delegates offer a mechanism to handle <see cref="AccessToken"/> renewal.
/// </remarks>
/// <param name="getTokenAsync">A delegate that returns a <see cref="ValueTask"/> of type <see cref="AccessToken"/>.
/// If <see cref="GetToken"/> is called, the provided delegate will be executed sync over async.</param>
/// <returns></returns>
public static TokenCredential Create(
Func<TokenRequestContext, CancellationToken, ValueTask<AccessToken>> getTokenAsync) => new StaticTokenCredential(getTokenAsync);
Copy link
Member

Choose a reason for hiding this comment

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

Does the func need to return ValueTask? If not I wonder if Task<AccessToken> would be easier for users.

Copy link
Member Author

Choose a reason for hiding this comment

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

I wanted to keep it 1:1 with GetToken's signature, unless you feel strongly otherwise

Copy link
Member

Choose a reason for hiding this comment

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

I don't feel strongly I just wanted to pose the question to others.


private class StaticTokenCredential : TokenCredential
{
private readonly Func<TokenRequestContext, CancellationToken, AccessToken> _getToken;
private readonly Func<TokenRequestContext, CancellationToken, ValueTask<AccessToken>> _getTokenAsync;

internal StaticTokenCredential(
Func<TokenRequestContext, CancellationToken, AccessToken> getToken,
Func<TokenRequestContext, CancellationToken, ValueTask<AccessToken>> getTokenAsync)
{
_getToken = getToken;
_getTokenAsync = getTokenAsync;
}

internal StaticTokenCredential(
Func<TokenRequestContext, CancellationToken, AccessToken> getToken)
{
_getToken = getToken;
_getTokenAsync = (context, token) => new ValueTask<AccessToken>(_getToken(context, token));
}

internal StaticTokenCredential(
Func<TokenRequestContext, CancellationToken, ValueTask<AccessToken>> getTokenAsync)
{
_getToken = (context, token) => getTokenAsync(context, token)
#pragma warning disable AZC0102 // Do not use GetAwaiter().GetResult(). Use the TaskExtensions.EnsureCompleted() extension method instead.
.GetAwaiter().GetResult();
#pragma warning restore AZC0102 // Do not use GetAwaiter().GetResult(). Use the TaskExtensions.EnsureCompleted() extension method instead.
_getTokenAsync = getTokenAsync;
}

/// <inheritdoc cref="GetTokenAsync"/>
public override ValueTask<AccessToken> GetTokenAsync(TokenRequestContext requestContext, CancellationToken cancellationToken) =>
_getTokenAsync(requestContext, cancellationToken);

/// <inheritdoc cref="GetToken"/>
public override AccessToken GetToken(TokenRequestContext requestContext, CancellationToken cancellationToken) =>
_getToken(requestContext, cancellationToken);
}
}
}
61 changes: 61 additions & 0 deletions sdk/core/Azure.Core/tests/StaticTokenCredentialTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using NUnit.Framework;

namespace Azure.Core.Tests
{
public class StaticTokenCredentialTests
{
private static string[] scopes = { "https://default.mock.auth.scope/.default" };
private static CancellationToken ctx = new CancellationTokenSource(TimeSpan.FromMinutes(5)).Token;
private static string expectedToken = "token";
private static DateTimeOffset expires = DateTimeOffset.UtcNow;
private static AccessToken staticToken;
private static Func<TokenRequestContext, CancellationToken, AccessToken> getToken;
private static Func<TokenRequestContext, CancellationToken, ValueTask<AccessToken>> getTokenAsync;

private static IEnumerable<object[]> Credentials()
{
staticToken = new AccessToken(expectedToken, expires);
getToken = (context, token) =>
{
Assert.AreEqual(scopes, context.Scopes);
Assert.AreEqual(ctx, token);
return staticToken;
};
getTokenAsync = async (context, token) =>
{
Assert.AreEqual(scopes, context.Scopes);
Assert.AreEqual(ctx, token);
await Task.Yield();
return staticToken;
};
yield return new object[] { TokenCredential.Create(getTokenAsync) };
yield return new object[] { TokenCredential.Create(getToken) };
yield return new object[] { TokenCredential.Create(getToken, getTokenAsync) };
}

[TestCaseSource(nameof(Credentials))]
public async Task CreateGetTokenAsyncCallsDelegate(TokenCredential credential)
{
AccessToken actualToken = await credential.GetTokenAsync(new TokenRequestContext(scopes), ctx);

Assert.AreEqual(expectedToken, actualToken.Token);
Assert.AreEqual(expires, actualToken.ExpiresOn);
}

[TestCaseSource(nameof(Credentials))]
public void CreateGetTokenCallsDelegate(TokenCredential credential)
{
AccessToken actualToken = credential.GetToken(new TokenRequestContext(scopes), ctx);

Assert.AreEqual(expectedToken, actualToken.Token);
Assert.AreEqual(expires, actualToken.ExpiresOn);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,40 +2,14 @@
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`.
For scenarios where you need to authenticate with a prefetched access token, the `TokenCredential.Create` static method is available.

```C# Snippet:StaticTokenCredential
public class StaticTokenCredential : TokenCredential
{
private AccessToken _token;
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 `TokenCredential.Create` to authenticate a `BlobClient`.

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);
```C# Snippet:TokenCredentialCreateUsage
AccessToken token = GetTokenForScope("https://storage.azure.com/.default");
var credential = TokenCredential.Create((_, _) => token);

var client = new BlobClient(new Uri("https://aka.ms/bloburl"), credential);
```
Expand Down Expand Up @@ -120,4 +94,4 @@ The following example shows an how the `OnBehalfOfCredential` could be used to a
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 @@ -14,45 +14,22 @@ namespace Azure.Identity.Tests.samples
{
public class CustomCredentialSnippets
{
#region 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);
}
}
#endregion

[Test]
public void StaticTokenCredentialUsage()
public void TokenCredentialCreateUsage()
{
#region Snippet:StaticTokenCredentialUsage
string token = GetTokenForScope("https://storage.azure.com/.default");

var credential = new StaticTokenCredential(token);
#region Snippet:TokenCredentialCreateUsage
AccessToken token = GetTokenForScope("https://storage.azure.com/.default");
#if SNIPPET
var credential = TokenCredential.Create((_, _) => token);

var client = new BlobClient(new Uri("https://aka.ms/bloburl"), credential);
#endif
#endregion
}

private string GetTokenForScope(string scope)
private AccessToken GetTokenForScope(string scope)
{
return null;
return new AccessToken();
}

#region Snippet:ConfidentialClientCredential
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,6 @@ namespace Azure.Identity.Samples
{
public class TokenCacheSnippets
{
private const string TOKEN_CACHE_PATH = "./tokencache.bin";

public void Identity_TokenCache_PersistentDefault()
{
#region Snippet:Identity_TokenCache_PersistentDefault
Expand Down