Skip to content

Commit

Permalink
Adding DeviceCodeCredential to Azure.Identity (Azure#7033)
Browse files Browse the repository at this point in the history
* Adding DeviceCodeCredential to Azure.Identity

* updates addressing PR feedback
  • Loading branch information
schaabs authored Jul 29, 2019
1 parent 506f75f commit 8a7d510
Show file tree
Hide file tree
Showing 8 changed files with 773 additions and 0 deletions.
1 change: 1 addition & 0 deletions eng/Packages.Data.props
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
<PackageReference Update="Microsoft.Extensions.Configuration" Version="1.0.2" />
<PackageReference Update="Microsoft.Extensions.PlatformAbstractions" Version="1.1.0" />
<PackageReference Update="Microsoft.IdentityModel.Clients.ActiveDirectory" Version="4.5.1" />
<PackageReference Update="Microsoft.Identity.Client" Version="4.1.0" />
<PackageReference Update="Microsoft.NET.Test.Sdk" Version="16.1.0" />
<PackageReference Update="Microsoft.NETCore.Platforms" Version="2.2.1" />
<PackageReference Update="Microsoft.NETFramework.ReferenceAssemblies" Version="1.0.0-alpha-004" />
Expand Down
2 changes: 2 additions & 0 deletions sdk/identity/Azure.Identity/src/Azure.Identity.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
<PackageReference Include="System.Memory" />
<PackageReference Include="System.Text.Json" />
<PackageReference Include="System.Threading.Tasks.Extensions" />
<PackageReference Include="Microsoft.Identity.Client" />

</ItemGroup>

<ItemGroup>
Expand Down
166 changes: 166 additions & 0 deletions sdk/identity/Azure.Identity/src/DeviceCodeCredential.cs
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);
}


}
}
72 changes: 72 additions & 0 deletions sdk/identity/Azure.Identity/src/DeviceCodeInfo.cs
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; }
}
}
81 changes: 81 additions & 0 deletions sdk/identity/Azure.Identity/src/HttpExtensions.cs
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 sdk/identity/Azure.Identity/src/HttpPipelineClientFactory.cs
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();
}
}
}
}
Loading

0 comments on commit 8a7d510

Please sign in to comment.