From 8a7d510c4f7dc88ad6a68bc77f859f0247a71089 Mon Sep 17 00:00:00 2001 From: Scott Schaab Date: Mon, 29 Jul 2019 09:14:28 -0700 Subject: [PATCH] Adding DeviceCodeCredential to Azure.Identity (#7033) * Adding DeviceCodeCredential to Azure.Identity * updates addressing PR feedback --- eng/Packages.Data.props | 1 + .../Azure.Identity/src/Azure.Identity.csproj | 2 + .../src/DeviceCodeCredential.cs | 166 ++++++++ .../Azure.Identity/src/DeviceCodeInfo.cs | 72 ++++ .../Azure.Identity/src/HttpExtensions.cs | 81 ++++ .../src/HttpPipelineClientFactory.cs | 53 +++ .../tests/DeviceCodeCredentialTests.cs | 381 ++++++++++++++++++ .../Azure.Identity/tests/MockExtensions.cs | 17 + 8 files changed, 773 insertions(+) create mode 100644 sdk/identity/Azure.Identity/src/DeviceCodeCredential.cs create mode 100644 sdk/identity/Azure.Identity/src/DeviceCodeInfo.cs create mode 100644 sdk/identity/Azure.Identity/src/HttpExtensions.cs create mode 100644 sdk/identity/Azure.Identity/src/HttpPipelineClientFactory.cs create mode 100644 sdk/identity/Azure.Identity/tests/DeviceCodeCredentialTests.cs create mode 100644 sdk/identity/Azure.Identity/tests/MockExtensions.cs diff --git a/eng/Packages.Data.props b/eng/Packages.Data.props index 1825607ad303d..04f42ab83cd9d 100755 --- a/eng/Packages.Data.props +++ b/eng/Packages.Data.props @@ -42,6 +42,7 @@ + diff --git a/sdk/identity/Azure.Identity/src/Azure.Identity.csproj b/sdk/identity/Azure.Identity/src/Azure.Identity.csproj index 2cd4336a37a85..b4019fa5db076 100644 --- a/sdk/identity/Azure.Identity/src/Azure.Identity.csproj +++ b/sdk/identity/Azure.Identity/src/Azure.Identity.csproj @@ -20,6 +20,8 @@ + + diff --git a/sdk/identity/Azure.Identity/src/DeviceCodeCredential.cs b/sdk/identity/Azure.Identity/src/DeviceCodeCredential.cs new file mode 100644 index 0000000000000..821f66c263d56 --- /dev/null +++ b/sdk/identity/Azure.Identity/src/DeviceCodeCredential.cs @@ -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 +{ + /// + /// A 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. + /// + public class DeviceCodeCredential : TokenCredential + { + private IPublicClientApplication _pubApp = null; + private HttpPipeline _pipeline = null; + private IAccount _account = null; + private IdentityClientOptions _options; + private string _clientId; + private Func _deviceCodeCallback; + + /// + /// Protected constructor for mocking + /// + protected DeviceCodeCredential() + { + + } + + /// + /// Creates a new DeviceCodeCredential which will authenticate users with the specified application. + /// + /// The client id of the application to which the users will authenticate. + /// TODO: need to link to info on how the application has to be created to authenticate users, for multiple applications + /// The callback to be executed to display the device code to the user + public DeviceCodeCredential(string clientId, Func deviceCodeCallback) + : this(clientId, deviceCodeCallback, null) + { + + } + + /// + /// Creates a new DeviceCodeCredential with the specifeid options, which will authenticate users with the specified application. + /// + /// The client id of the application to which the users will authenticate + /// The client options for the newly created DeviceCodeCredential + /// The callback to be executed to display the device code to the user + public DeviceCodeCredential(string clientId, Func 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(); + } + + /// + /// Obtains a token for a user account, authenticating them through the device code authentication flow. + /// + /// The list of scopes for which the token will have access. + /// A controlling the request lifetime. + /// An which can be used to authenticate service client calls. + 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; + } + } + + /// + /// Obtains a token for a user account, authenticating them through the device code authentication flow. + /// + /// The list of scopes for which the token will have access. + /// A controlling the request lifetime. + /// An which can be used to authenticate service client calls. + public override async Task 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 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); + } + + + } +} diff --git a/sdk/identity/Azure.Identity/src/DeviceCodeInfo.cs b/sdk/identity/Azure.Identity/src/DeviceCodeInfo.cs new file mode 100644 index 0000000000000..4148d00b25c5f --- /dev/null +++ b/sdk/identity/Azure.Identity/src/DeviceCodeInfo.cs @@ -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 +{ + /// + /// Details of the device code to present to a user to allow them to authenticate through the device code authentication flow. + /// + 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; + } + + /// + /// User code returned by the service + /// + public string UserCode { get; private set; } + + /// + /// Device code returned by the service + /// + public string DeviceCode { get; private set; } + +#pragma warning disable CA1056 // Uri properties should not be strings + + /// + /// Verification URL where the user must navigate to authenticate using the device code and credentials. + /// + public string VerificationUrl { get; private set; } + +#pragma warning restore CA1056 // Uri properties should not be strings + + /// + /// Time when the device code will expire. + /// + public DateTimeOffset ExpiresOn { get; private set; } + + /// + /// Polling interval time to check for completion of authentication flow. + /// + public long Interval { get; private set; } + + /// + /// User friendly text response that can be used for display purpose. + /// + public string Message { get; private set; } + + /// + /// Identifier of the client requesting device code. + /// + public string ClientId { get; private set; } + + /// + /// List of the scopes that would be held by token. + /// + public IReadOnlyCollection Scopes { get; private set; } + } +} diff --git a/sdk/identity/Azure.Identity/src/HttpExtensions.cs b/sdk/identity/Azure.Identity/src/HttpExtensions.cs new file mode 100644 index 0000000000000..a43695c151035 --- /dev/null +++ b/sdk/identity/Azure.Identity/src/HttpExtensions.cs @@ -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 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 ToPipelineRequestContentAsync(this HttpContent content) + { + if (content != null) + { + return HttpPipelineRequestContent.Create(await content.ReadAsStreamAsync().ConfigureAwait(false)); + } + + return null; + } + + } +} diff --git a/sdk/identity/Azure.Identity/src/HttpPipelineClientFactory.cs b/sdk/identity/Azure.Identity/src/HttpPipelineClientFactory.cs new file mode 100644 index 0000000000000..ecb2a7272424f --- /dev/null +++ b/sdk/identity/Azure.Identity/src/HttpPipelineClientFactory.cs @@ -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 +{ + /// + /// 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. + /// + internal class HttpPipelineClientFactory : IMsalHttpClientFactory + { + private HttpPipeline _pipeline; + + public HttpPipelineClientFactory(HttpPipeline pipeline) + { + _pipeline = pipeline; + } + + public HttpClient GetHttpClient() + { + return new HttpClient(new PipelineHttpMessageHandler(_pipeline)); + } + + /// + /// An HttpMessageHandler which delegates SendAsync to a specified HttpPipeline. + /// + private class PipelineHttpMessageHandler : HttpMessageHandler + { + private HttpPipeline _pipeline; + + public PipelineHttpMessageHandler(HttpPipeline pipeline) + { + _pipeline = pipeline; + } + + protected override async Task 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(); + } + } + } +} diff --git a/sdk/identity/Azure.Identity/tests/DeviceCodeCredentialTests.cs b/sdk/identity/Azure.Identity/tests/DeviceCodeCredentialTests.cs new file mode 100644 index 0000000000000..90426f9309324 --- /dev/null +++ b/sdk/identity/Azure.Identity/tests/DeviceCodeCredentialTests.cs @@ -0,0 +1,381 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Azure.Core; +using Azure.Core.Testing; +using NUnit.Framework; +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Azure.Identity.Tests +{ + public class DeviceCodeCredentialTests : ClientTestBase + { + public DeviceCodeCredentialTests(bool isAsync) : base(isAsync) + { + } + + private const string ClientId = "04b07795-8ddb-461a-bbee-02f9e1bf7b46"; + + private HashSet _requestedCodes = new HashSet(); + + private object _requestedCodesLock = new object(); + + private Task VerifyDeviceCode(DeviceCodeInfo code, string message) + { + Assert.AreEqual(message, code.Message); + + return Task.CompletedTask; + } + + private Task VerifyDeviceCodeAndCancel(DeviceCodeInfo code, string message, CancellationTokenSource cancelSource) + { + Assert.AreEqual(message, code.Message); + + cancelSource.Cancel(); + + return Task.CompletedTask; + } + + private async Task VerifyDeviceCodeCallbackCancellationToken(DeviceCodeInfo code, CancellationToken cancellationToken) + { + await Task.Delay(2000, cancellationToken); + + cancellationToken.ThrowIfCancellationRequested(); + } + + private class MockException : Exception + { + + } + private async Task ThrowingDeviceCodeCallback(DeviceCodeInfo code, CancellationToken cancellationToken) + { + await Task.CompletedTask; + + throw new MockException(); + } + + [Test] + public async Task AuthenticateWithDeviceCodeMockAsync() + { + var expectedCode = Guid.NewGuid().ToString(); + + var expectedToken = Guid.NewGuid().ToString(); + + var mockTransport = new MockTransport(request => ProcessMockRequest(request, expectedCode, expectedToken)); + + var options = new IdentityClientOptions() { Transport = mockTransport }; + + var cred = InstrumentClient(new DeviceCodeCredential(ClientId, (code, cancelToken) => VerifyDeviceCode(code, expectedCode), options)); + + AccessToken token = await cred.GetTokenAsync(new string[] { "https://vault.azure.net/.default" }); + + Assert.AreEqual(token.Token, expectedToken); + } + + [Test] + public async Task AuthenticateWithDeviceCodeMockAsync2() + { + var expectedCode = Guid.NewGuid().ToString(); + + var expectedToken = Guid.NewGuid().ToString(); + + var mockTransport = new MockTransport(request => ProcessMockRequest(request, expectedCode, expectedToken)); + + var options = new IdentityClientOptions() { Transport = mockTransport }; + + var cred = InstrumentClient(new DeviceCodeCredential(ClientId, (code, cancelToken) => VerifyDeviceCode(code, expectedCode), options)); + + AccessToken token = await cred.GetTokenAsync(new string[] { "https://vault.azure.net/.default" }); + + Assert.AreEqual(token.Token, expectedToken); + } + + [Test] + public void AuthenticateWithDeviceCodeMockVerifyMsalCancellationAsync() + { + var expectedCode = Guid.NewGuid().ToString(); + + var expectedToken = Guid.NewGuid().ToString(); + + var cancelSource = new CancellationTokenSource(); + + var mockTransport = new MockTransport(request => ProcessMockRequest(request, expectedCode, expectedToken)); + + var options = new IdentityClientOptions() { Transport = mockTransport }; + + var cred = InstrumentClient(new DeviceCodeCredential(ClientId, (code, cancelToken) => VerifyDeviceCodeAndCancel(code, expectedCode, cancelSource), options)); + + Assert.ThrowsAsync(async () => await cred.GetTokenAsync(new string[] { "https://vault.azure.net/.default" }, cancelSource.Token)); + } + + [Test] + public async Task AuthenticateWithDeviceCodeMockVerifyCallbackCancellationAsync() + { + var expectedCode = Guid.NewGuid().ToString(); + + var expectedToken = Guid.NewGuid().ToString(); + + var mockTransport = new MockTransport(request => ProcessMockRequest(request, expectedCode, expectedToken)); + + var options = new IdentityClientOptions() { Transport = mockTransport }; + + var cancelSource = new CancellationTokenSource(1000); + + var cred = InstrumentClient(new DeviceCodeCredential(ClientId, VerifyDeviceCodeCallbackCancellationToken, options)); + + Task getTokenTask = cred.GetTokenAsync(new string[] { "https://vault.azure.net/.default" }, cancelSource.Token); + + try + { + AccessToken token = await getTokenTask; + + Assert.Fail(); + } + catch(TaskCanceledException) + { + + } + } + + [Test] + public void AuthenticateWithDeviceCodeCallbackThrowsAsync() + { + var expectedCode = Guid.NewGuid().ToString(); + + var expectedToken = Guid.NewGuid().ToString(); + + var cancelSource = new CancellationTokenSource(); + + var mockTransport = new MockTransport(request => ProcessMockRequest(request, expectedCode, expectedToken)); + + var options = new IdentityClientOptions() { Transport = mockTransport }; + + var cred = InstrumentClient(new DeviceCodeCredential(ClientId, ThrowingDeviceCodeCallback, options)); + + Assert.ThrowsAsync(async () => await cred.GetTokenAsync(new string[] { "https://vault.azure.net/.default" }, cancelSource.Token)); + } + + private MockResponse ProcessMockRequest(MockRequest mockRequest, string code, string token) + { + string requestUrl = mockRequest.UriBuilder.Uri.AbsoluteUri; + + if (requestUrl.StartsWith("https://login.microsoftonline.com/common/discovery/instance")) + { + return DiscoveryInstanceResponse; + } + + if (requestUrl.StartsWith("https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration")) + { + return OpenIdConfigurationResponse; + } + + if (requestUrl.StartsWith("https://login.microsoftonline.com/organizations/oauth2/v2.0/devicecode")) + { + return CreateDeviceCodeResponse(code); + } + + if (requestUrl.StartsWith("https://login.microsoftonline.com/organizations/oauth2/v2.0/token")) + { + return CreateTokenResponse(code, token); + + } + + throw new InvalidOperationException(); + } + + private MockResponse CreateTokenResponse(string code, string token) + { + lock(_requestedCodesLock) + { + if(_requestedCodes.Add(code)) + { + return AuthorizationPendingResponse; + } + else + { + return CreateAuthorizationResponse(token); + } + } + } + + private MockResponse CreateDeviceCodeResponse(string code) + { + var response = new MockResponse(200).WithContent($@"{{ + ""user_code"": ""{code}"", + ""device_code"": ""{code}_{code}"", + ""verification_uri"": ""https://microsoft.com/devicelogin"", + ""expires_in"": 900, + ""interval"": 1, + ""message"": ""{code}"" +}}"); + + return response; + } + + private MockResponse CreateAuthorizationResponse(string accessToken) + { + var response = new MockResponse(200).WithContent(@$"{{ + ""token_type"": ""Bearer"", + ""scope"": ""https://vault.azure.net/user_impersonation https://vault.azure.net/.default"", + ""expires_in"": 3600, + ""ext_expires_in"": 3600, + ""access_token"": ""{accessToken}"", + ""refresh_token"": ""eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9-eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ-SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"", + ""foci"": ""1"", + ""id_token"": ""eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6InU0T2ZORlBId0VCb3NIanRyYXVPYlY4NExuWSJ9.eyJhdWQiOiJFMDFCNUY2NC03OEY1LTRGODgtQjI4Mi03QUUzOUI4QUM0QkQiLCJpc3MiOiJodHRwczovL2xvZ2luLm1pY3Jvc29mdG9ubGluZS5jb20vRDEwOUI0NkUtM0E5Ri00NDQwLTg2MjItMjVEQjQxOTg1MDUxL3YyLjAiLCJpYXQiOjE1NjM5OTA0MDEsIm5iZiI6MTU2Mzk5MDQwMSwiZXhwIjoxNTYzOTk0MzAxLCJhaW8iOiJRMVV3TlV4YVNFeG9aak5uUWpSd00zcFRNamRrV2pSTFNVcEdMMVV3TWt0a2FrZDFTRkJVVlZwMmVFODRNMFZ0VXk4Mlp6TjJLM1JrVVVzeVQyVXhNamxJWTNKQ1p6MGlMQ0p1WVcxbElqb2lVMk52ZEhRZ1UyTiIsIm5hbWUiOiJTb21lIFVzZXIiLCJvaWQiOiIyQ0M5QzNBOC0yNTA5LTQyMEYtQjAwQi02RTczQkM1MURDQjUiLCJwcmVmZXJyZWRfdXNlcm5hbWUiOiJzb21ldXNlckBtaWNyb3NvZnQuY29tIiwic3ViIjoiQ0p6ZFdJaU9pSkdXakY0UVVOS1JFRnBRWFp6IiwidGlkIjoiMjRGRTMxMUYtN0E3MS00RjgzLTkxNkEtOTQ3OEQ0NUMwNDI3IiwidXRpIjoidFFqSTRNaTAzUVVVek9VSTRRVU0wUWtRaUxDSnBjM01pT2lKb2RIUiIsInZlciI6IjIuMCJ9.eVyG1AL8jwnTo3m9mGsV4EDHa_8PN6rRPEN9E3cQzxNoPU9HZTFt1SgOnLB7n1a4J_E3iVoZ3VB5I-NdDBESRdlg1k4XlrWqtisxl3I7pvWVFZKEhwHYYQ_nZITNeCb48LfZNz-Mr4EZeX6oyUymha5tOomikBLLxP78LOTlbGQiFn9AjtV0LtMeoiDf-K9t-kgU-XwsVjCyFKFBQhcyv7zaBEpeA-Kzh3-HG7wZ-geteM5y-JF97nD_rJ8ow1FmvtDYy6MVcwuNTv2YYT8dn8s-SGB4vpNNignlL0QgYh2P2cIrPdhZVc2iQqYTn_FK_UFPqyb_MZSjl1QkXVhgJA"", + ""client_info"": ""eyJ1aWQiOiIyQ0M5QzNBOC0yNTA5LTQyMEYtQjAwQi02RTczQkM1MURDQjUiLCJ1dGlkIjoiNzJmOTg4YmYtODZmMS00MWFmLTkxYWItMmQ3Y2QwMTFkYjQ3In0"" +}}"); + return response; + } + + private static MockResponse DiscoveryInstanceResponse + { + get + { + return new MockResponse(200).WithContent(@" +{ + ""tenant_discovery_endpoint"": ""https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration"", + ""api-version"": ""1.1"", + ""metadata"": [ + { + ""preferred_network"": ""login.microsoftonline.com"", + ""preferred_cache"": ""login.windows.net"", + ""aliases"": [ + ""login.microsoftonline.com"", + ""login.windows.net"", + ""login.microsoft.com"", + ""sts.windows.net"" + ] +}, + { + ""preferred_network"": ""login.partner.microsoftonline.cn"", + ""preferred_cache"": ""login.partner.microsoftonline.cn"", + ""aliases"": [ + ""login.partner.microsoftonline.cn"", + ""login.chinacloudapi.cn"" + ] + }, + { + ""preferred_network"": ""login.microsoftonline.de"", + ""preferred_cache"": ""login.microsoftonline.de"", + ""aliases"": [ + ""login.microsoftonline.de"" + ] + }, + { + ""preferred_network"": ""login.microsoftonline.us"", + ""preferred_cache"": ""login.microsoftonline.us"", + ""aliases"": [ + ""login.microsoftonline.us"", + ""login.usgovcloudapi.net"" + ] + }, + { + ""preferred_network"": ""login-us.microsoftonline.com"", + ""preferred_cache"": ""login-us.microsoftonline.com"", + ""aliases"": [ + ""login-us.microsoftonline.com"" + ] + } + ] +}"); + } + } + + private static MockResponse OpenIdConfigurationResponse + { + get + { + return new MockResponse(200).WithContent(@"{ + ""authorization_endpoint"": ""https://login.microsoftonline.com/common/oauth2/v2.0/authorize"", + ""token_endpoint"": ""https://login.microsoftonline.com/common/oauth2/v2.0/token"", + ""token_endpoint_auth_methods_supported"": [ + ""client_secret_post"", + ""private_key_jwt"", + ""client_secret_basic"" + ], + ""jwks_uri"": ""https://login.microsoftonline.com/common/discovery/v2.0/keys"", + ""response_modes_supported"": [ + ""query"", + ""fragment"", + ""form_post"" + ], + ""subject_types_supported"": [ + ""pairwise"" + ], + ""id_token_signing_alg_values_supported"": [ + ""RS256"" + ], + ""http_logout_supported"": true, + ""frontchannel_logout_supported"": true, + ""end_session_endpoint"": ""https://login.microsoftonline.com/common/oauth2/v2.0/logout"", + ""response_types_supported"": [ + ""code"", + ""id_token"", + ""code id_token"", + ""id_token token"" + ], + ""scopes_supported"": [ + ""openid"", + ""profile"", + ""email"", + ""offline_access"" + ], + ""issuer"": ""https://login.microsoftonline.com/{tenantid}/v2.0"", + ""claims_supported"": [ + ""sub"", + ""iss"", + ""cloud_instance_name"", + ""cloud_instance_host_name"", + ""cloud_graph_host_name"", + ""msgraph_host"", + ""aud"", + ""exp"", + ""iat"", + ""auth_time"", + ""acr"", + ""nonce"", + ""preferred_username"", + ""name"", + ""tid"", + ""ver"", + ""at_hash"", + ""c_hash"", + ""email"" + ], + ""request_uri_parameter_supported"": false, + ""userinfo_endpoint"": ""https://graph.microsoft.com/oidc/userinfo"", + ""tenant_region_scope"": null, + ""cloud_instance_name"": ""microsoftonline.com"", + ""cloud_graph_host_name"": ""graph.windows.net"", + ""msgraph_host"": ""graph.microsoft.com"", + ""rbac_url"": ""https://pas.windows.net"" +}"); + } + } + + private static MockResponse AuthorizationPendingResponse + { + get + { + return new MockResponse(404).WithContent(@"{ + ""error"": ""authorization_pending"", + ""error_description"": ""AADSTS70016: Pending end-user authorization.\r\nTrace ID: c40ce91e-5009-4e64-9a10-7732b2500100\r\nCorrelation ID: 73a2edae-f747-44da-8ebf-7cba565fe49d\r\nTimestamp: 2019-07-24 17:49:13Z"", + ""error_codes"": [ + 70016 + ], + ""timestamp"": ""2019-07-24 17:49:13Z"", + ""trace_id"": ""c40ce91e-5009-4e64-9a10-7732b2500100"", + ""correlation_id"": ""73a2edae-f747-44da-8ebf-7cba565fe49d"" +}"); + } + } + } + + + +} diff --git a/sdk/identity/Azure.Identity/tests/MockExtensions.cs b/sdk/identity/Azure.Identity/tests/MockExtensions.cs new file mode 100644 index 0000000000000..04c3ed920a3c2 --- /dev/null +++ b/sdk/identity/Azure.Identity/tests/MockExtensions.cs @@ -0,0 +1,17 @@ +using Azure.Core.Testing; +using System; +using System.Collections.Generic; +using System.Text; + +namespace Azure.Identity.Tests +{ + internal static class MockExtensions + { + public static MockResponse WithContent(this MockResponse response, string content) + { + response.SetContent(content); + + return response; + } + } +}