forked from Azure/azure-sdk-for-net
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Adding DeviceCodeCredential to Azure.Identity (Azure#7033)
* Adding DeviceCodeCredential to Azure.Identity * updates addressing PR feedback
- Loading branch information
Showing
8 changed files
with
773 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
166 changes: 166 additions & 0 deletions
166
sdk/identity/Azure.Identity/src/DeviceCodeCredential.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,166 @@ | ||
// Copyright (c) Microsoft Corporation. All rights reserved. | ||
// Licensed under the MIT License. | ||
|
||
using Azure.Core; | ||
using Azure.Core.Pipeline; | ||
using Microsoft.Identity.Client; | ||
using System; | ||
using System.Collections.Generic; | ||
using System.Linq; | ||
using System.Text; | ||
using System.Threading; | ||
using System.Threading.Tasks; | ||
|
||
namespace Azure.Identity | ||
{ | ||
/// <summary> | ||
/// A <see cref="TokenCredential"/> implementation which authenticates a user using the device code flow, and provides access tokens for that user account. | ||
/// For more information on the device code authentication flow see https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/wiki/Device-Code-Flow. | ||
/// </summary> | ||
public class DeviceCodeCredential : TokenCredential | ||
{ | ||
private IPublicClientApplication _pubApp = null; | ||
private HttpPipeline _pipeline = null; | ||
private IAccount _account = null; | ||
private IdentityClientOptions _options; | ||
private string _clientId; | ||
private Func<DeviceCodeInfo, CancellationToken, Task> _deviceCodeCallback; | ||
|
||
/// <summary> | ||
/// Protected constructor for mocking | ||
/// </summary> | ||
protected DeviceCodeCredential() | ||
{ | ||
|
||
} | ||
|
||
/// <summary> | ||
/// Creates a new DeviceCodeCredential which will authenticate users with the specified application. | ||
/// </summary> | ||
/// <param name="clientId">The client id of the application to which the users will authenticate.</param> | ||
/// TODO: need to link to info on how the application has to be created to authenticate users, for multiple applications | ||
/// <param name="deviceCodeCallback">The callback to be executed to display the device code to the user</param> | ||
public DeviceCodeCredential(string clientId, Func<DeviceCodeInfo, CancellationToken, Task> deviceCodeCallback) | ||
: this(clientId, deviceCodeCallback, null) | ||
{ | ||
|
||
} | ||
|
||
/// <summary> | ||
/// Creates a new DeviceCodeCredential with the specifeid options, which will authenticate users with the specified application. | ||
/// </summary> | ||
/// <param name="clientId">The client id of the application to which the users will authenticate</param> | ||
/// <param name="options">The client options for the newly created DeviceCodeCredential</param> | ||
/// <param name="deviceCodeCallback">The callback to be executed to display the device code to the user</param> | ||
public DeviceCodeCredential(string clientId, Func<DeviceCodeInfo, CancellationToken, Task> deviceCodeCallback, IdentityClientOptions options) | ||
{ | ||
_clientId = clientId ?? throw new ArgumentNullException(nameof(clientId)); | ||
|
||
_deviceCodeCallback = deviceCodeCallback ?? throw new ArgumentNullException(nameof(deviceCodeCallback)); | ||
|
||
_options = options ?? new IdentityClientOptions(); | ||
|
||
_pipeline = HttpPipelineBuilder.Build(_options, bufferResponse: true); | ||
|
||
_pubApp = PublicClientApplicationBuilder.Create(_clientId).WithHttpClientFactory(new HttpPipelineClientFactory(_pipeline)).WithRedirectUri("https://login.microsoftonline.com/common/oauth2/nativeclient").Build(); | ||
} | ||
|
||
/// <summary> | ||
/// Obtains a token for a user account, authenticating them through the device code authentication flow. | ||
/// </summary> | ||
/// <param name="scopes">The list of scopes for which the token will have access.</param> | ||
/// <param name="cancellationToken">A <see cref="CancellationToken"/> controlling the request lifetime.</param> | ||
/// <returns>An <see cref="AccessToken"/> which can be used to authenticate service client calls.</returns> | ||
public override AccessToken GetToken(string[] scopes, CancellationToken cancellationToken = default) | ||
{ | ||
using DiagnosticScope scope = _pipeline.Diagnostics.CreateScope("Azure.Identity.DeviceCodeCredential.GetToken"); | ||
|
||
scope.Start(); | ||
|
||
try | ||
{ | ||
if (_account != null) | ||
{ | ||
try | ||
{ | ||
AuthenticationResult result = _pubApp.AcquireTokenSilent(scopes, _account).ExecuteAsync(cancellationToken).GetAwaiter().GetResult(); | ||
|
||
return new AccessToken(result.AccessToken, result.ExpiresOn); | ||
} | ||
catch (MsalUiRequiredException) | ||
{ | ||
// TODO: logging for exception here? | ||
return GetTokenViaDeviceCodeAsync(scopes, cancellationToken).GetAwaiter().GetResult(); | ||
} | ||
} | ||
else | ||
{ | ||
return GetTokenViaDeviceCodeAsync(scopes, cancellationToken).GetAwaiter().GetResult(); | ||
} | ||
} | ||
catch (Exception e) | ||
{ | ||
scope.Failed(e); | ||
|
||
throw; | ||
} | ||
} | ||
|
||
/// <summary> | ||
/// Obtains a token for a user account, authenticating them through the device code authentication flow. | ||
/// </summary> | ||
/// <param name="scopes">The list of scopes for which the token will have access.</param> | ||
/// <param name="cancellationToken">A <see cref="CancellationToken"/> controlling the request lifetime.</param> | ||
/// <returns>An <see cref="AccessToken"/> which can be used to authenticate service client calls.</returns> | ||
public override async Task<AccessToken> GetTokenAsync(string[] scopes, CancellationToken cancellationToken = default) | ||
{ | ||
using DiagnosticScope scope = _pipeline.Diagnostics.CreateScope("Azure.Identity.DeviceCodeCredential.GetToken"); | ||
|
||
scope.Start(); | ||
|
||
try | ||
{ | ||
if (_account != null) | ||
{ | ||
try | ||
{ | ||
AuthenticationResult result = await _pubApp.AcquireTokenSilent(scopes, _account).ExecuteAsync(cancellationToken).ConfigureAwait(false); | ||
|
||
return new AccessToken(result.AccessToken, result.ExpiresOn); | ||
} | ||
catch (MsalUiRequiredException) | ||
{ | ||
// TODO: logging for exception here? | ||
return await GetTokenViaDeviceCodeAsync(scopes, cancellationToken).ConfigureAwait(false); | ||
} | ||
} | ||
else | ||
{ | ||
return await GetTokenViaDeviceCodeAsync(scopes, cancellationToken).ConfigureAwait(false); | ||
} | ||
} | ||
catch (Exception e) | ||
{ | ||
scope.Failed(e); | ||
|
||
throw; | ||
} | ||
} | ||
|
||
private async Task<AccessToken> GetTokenViaDeviceCodeAsync(string[] scopes, CancellationToken cancellationToken) | ||
{ | ||
AuthenticationResult result = await _pubApp.AcquireTokenWithDeviceCode(scopes, code => DeviceCodeCallback(code, cancellationToken)).ExecuteAsync(cancellationToken).ConfigureAwait(false); | ||
|
||
_account = result.Account; | ||
|
||
return new AccessToken(result.AccessToken, result.ExpiresOn); | ||
} | ||
|
||
private Task DeviceCodeCallback(DeviceCodeResult deviceCode, CancellationToken cancellationToken) | ||
{ | ||
return _deviceCodeCallback(new DeviceCodeInfo(deviceCode), cancellationToken); | ||
} | ||
|
||
|
||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
// Copyright (c) Microsoft Corporation. All rights reserved. | ||
// Licensed under the MIT License. | ||
|
||
using Microsoft.Identity.Client; | ||
using System; | ||
using System.Collections.Generic; | ||
using System.Text; | ||
|
||
namespace Azure.Identity | ||
{ | ||
/// <summary> | ||
/// Details of the device code to present to a user to allow them to authenticate through the device code authentication flow. | ||
/// </summary> | ||
public struct DeviceCodeInfo | ||
{ | ||
internal DeviceCodeInfo(DeviceCodeResult deviceCode) | ||
{ | ||
UserCode = deviceCode.UserCode; | ||
DeviceCode = deviceCode.DeviceCode; | ||
VerificationUrl = deviceCode.VerificationUrl; | ||
ExpiresOn = deviceCode.ExpiresOn; | ||
Interval = deviceCode.Interval; | ||
Message = deviceCode.Message; | ||
ClientId = deviceCode.ClientId; | ||
Scopes = deviceCode.Scopes; | ||
} | ||
|
||
/// <summary> | ||
/// User code returned by the service | ||
/// </summary> | ||
public string UserCode { get; private set; } | ||
|
||
/// <summary> | ||
/// Device code returned by the service | ||
/// </summary> | ||
public string DeviceCode { get; private set; } | ||
|
||
#pragma warning disable CA1056 // Uri properties should not be strings | ||
|
||
/// <summary> | ||
/// Verification URL where the user must navigate to authenticate using the device code and credentials. | ||
/// </summary> | ||
public string VerificationUrl { get; private set; } | ||
|
||
#pragma warning restore CA1056 // Uri properties should not be strings | ||
|
||
/// <summary> | ||
/// Time when the device code will expire. | ||
/// </summary> | ||
public DateTimeOffset ExpiresOn { get; private set; } | ||
|
||
/// <summary> | ||
/// Polling interval time to check for completion of authentication flow. | ||
/// </summary> | ||
public long Interval { get; private set; } | ||
|
||
/// <summary> | ||
/// User friendly text response that can be used for display purpose. | ||
/// </summary> | ||
public string Message { get; private set; } | ||
|
||
/// <summary> | ||
/// Identifier of the client requesting device code. | ||
/// </summary> | ||
public string ClientId { get; private set; } | ||
|
||
/// <summary> | ||
/// List of the scopes that would be held by token. | ||
/// </summary> | ||
public IReadOnlyCollection<string> Scopes { get; private set; } | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,81 @@ | ||
using Azure.Core.Pipeline; | ||
using Azure; | ||
using System; | ||
using System.Net.Http; | ||
using System.Threading; | ||
using System.Threading.Tasks; | ||
using System.Net; | ||
using Azure.Core.Http; | ||
|
||
namespace Azure.Identity | ||
{ | ||
internal static class HttpExtensions | ||
{ | ||
public static async Task<Request> ToPipelineRequestAsync(this HttpRequestMessage request, HttpPipeline pipeline) | ||
{ | ||
Request pipelineRequest = pipeline.CreateRequest(); | ||
|
||
pipelineRequest.Method = RequestMethod.Parse(request.Method.Method); | ||
|
||
pipelineRequest.UriBuilder.Uri = request.RequestUri; | ||
|
||
pipelineRequest.Content = await request.Content.ToPipelineRequestContentAsync().ConfigureAwait(false); | ||
|
||
foreach (var header in request.Headers) | ||
{ | ||
foreach (var value in header.Value) | ||
{ | ||
pipelineRequest.Headers.Add(header.Key, value); | ||
} | ||
} | ||
|
||
return pipelineRequest; | ||
} | ||
|
||
private static void AddHeader(HttpResponseMessage request, HttpHeader header) | ||
{ | ||
if (request.Headers.TryAddWithoutValidation(header.Name, header.Value)) | ||
{ | ||
return; | ||
} | ||
|
||
if (!request.Content.Headers.TryAddWithoutValidation(header.Name, header.Value)) | ||
{ | ||
throw new InvalidOperationException("Unable to add header to request or content"); | ||
} | ||
} | ||
|
||
public static HttpResponseMessage ToHttpResponseMessage(this Response response) | ||
{ | ||
HttpResponseMessage responseMessage = new HttpResponseMessage(); | ||
|
||
responseMessage.StatusCode = (HttpStatusCode)response.Status; | ||
|
||
responseMessage.Content = new StreamContent(response.ContentStream); | ||
|
||
foreach (var header in response.Headers) | ||
{ | ||
if (!responseMessage.Headers.TryAddWithoutValidation(header.Name, header.Value)) | ||
{ | ||
if (!responseMessage.Content.Headers.TryAddWithoutValidation(header.Name, header.Value)) | ||
{ | ||
throw new InvalidOperationException("Unable to add header to request or content"); | ||
} | ||
} | ||
} | ||
|
||
return responseMessage; | ||
} | ||
|
||
public static async Task<HttpPipelineRequestContent> ToPipelineRequestContentAsync(this HttpContent content) | ||
{ | ||
if (content != null) | ||
{ | ||
return HttpPipelineRequestContent.Create(await content.ReadAsStreamAsync().ConfigureAwait(false)); | ||
} | ||
|
||
return null; | ||
} | ||
|
||
} | ||
} |
53 changes: 53 additions & 0 deletions
53
sdk/identity/Azure.Identity/src/HttpPipelineClientFactory.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
using Azure.Core.Http; | ||
using Azure.Core.Pipeline; | ||
using Microsoft.Identity.Client; | ||
using System; | ||
using System.Collections.Generic; | ||
using System.Linq; | ||
using System.Net.Http; | ||
using System.Text; | ||
using System.Threading; | ||
using System.Threading.Tasks; | ||
|
||
namespace Azure.Identity | ||
{ | ||
/// <summary> | ||
/// This class is an HttpClient factory which creates an HttpClient which delegates it's transport to an HttpPipeline, to enable MSAL to send requests through an Azure.Core HttpPipeline. | ||
/// </summary> | ||
internal class HttpPipelineClientFactory : IMsalHttpClientFactory | ||
{ | ||
private HttpPipeline _pipeline; | ||
|
||
public HttpPipelineClientFactory(HttpPipeline pipeline) | ||
{ | ||
_pipeline = pipeline; | ||
} | ||
|
||
public HttpClient GetHttpClient() | ||
{ | ||
return new HttpClient(new PipelineHttpMessageHandler(_pipeline)); | ||
} | ||
|
||
/// <summary> | ||
/// An HttpMessageHandler which delegates SendAsync to a specified HttpPipeline. | ||
/// </summary> | ||
private class PipelineHttpMessageHandler : HttpMessageHandler | ||
{ | ||
private HttpPipeline _pipeline; | ||
|
||
public PipelineHttpMessageHandler(HttpPipeline pipeline) | ||
{ | ||
_pipeline = pipeline; | ||
} | ||
|
||
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) | ||
{ | ||
Request pipelineRequest = await request.ToPipelineRequestAsync(_pipeline).ConfigureAwait(false); | ||
|
||
Response pipelineResponse = await _pipeline.SendRequestAsync(pipelineRequest, cancellationToken).ConfigureAwait(false); | ||
|
||
return pipelineResponse.ToHttpResponseMessage(); | ||
} | ||
} | ||
} | ||
} |
Oops, something went wrong.