From 89e3cc5b58d34dfc28885d090227953b6fa62975 Mon Sep 17 00:00:00 2001 From: Chris R Date: Tue, 7 Feb 2017 15:10:05 -0800 Subject: [PATCH] #489 Update MicrosoftAccount provider --- .../Constants.cs | 4 ++++ .../MicrosoftAccountAuthenticationHandler.cs | 16 ++++++------- .../MicrosoftAccountAuthenticationOptions.cs | 19 +++++++++++++++ .../MicrosoftAccountAuthenticatedContext.cs | 22 ++++++------------ .../MicrosoftAuthentication.cs | 23 +++++++++---------- .../MicrosoftAccountMiddlewareTests.cs | 17 ++++++-------- 6 files changed, 56 insertions(+), 45 deletions(-) diff --git a/src/Microsoft.Owin.Security.MicrosoftAccount/Constants.cs b/src/Microsoft.Owin.Security.MicrosoftAccount/Constants.cs index f5522960..e80df415 100644 --- a/src/Microsoft.Owin.Security.MicrosoftAccount/Constants.cs +++ b/src/Microsoft.Owin.Security.MicrosoftAccount/Constants.cs @@ -6,5 +6,9 @@ namespace Microsoft.Owin.Security.MicrosoftAccount internal static class Constants { internal const string DefaultAuthenticationType = "Microsoft"; + + internal const string AuthorizationEndpoint = "https://login.microsoftonline.com/common/oauth2/v2.0/authorize"; + internal const string TokenEndpoint = "https://login.microsoftonline.com/common/oauth2/v2.0/token"; + internal const string UserInformationEndpoint = "https://graph.microsoft.com/v1.0/me"; } } diff --git a/src/Microsoft.Owin.Security.MicrosoftAccount/MicrosoftAccountAuthenticationHandler.cs b/src/Microsoft.Owin.Security.MicrosoftAccount/MicrosoftAccountAuthenticationHandler.cs index 1efa2560..24e065b7 100644 --- a/src/Microsoft.Owin.Security.MicrosoftAccount/MicrosoftAccountAuthenticationHandler.cs +++ b/src/Microsoft.Owin.Security.MicrosoftAccount/MicrosoftAccountAuthenticationHandler.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Net.Http; +using System.Net.Http.Headers; using System.Security.Claims; using System.Threading.Tasks; using Microsoft.Owin.Infrastructure; @@ -15,9 +16,6 @@ namespace Microsoft.Owin.Security.MicrosoftAccount { internal class MicrosoftAccountAuthenticationHandler : AuthenticationHandler { - private const string TokenEndpoint = "https://login.live.com/oauth20_token.srf"; - private const string GraphApiEndpoint = "https://apis.live.net/v5.0/me"; - private readonly ILogger _logger; private readonly HttpClient _httpClient; @@ -79,7 +77,7 @@ namespace Microsoft.Owin.Security.MicrosoftAccount var requestContent = new FormUrlEncodedContent(tokenRequestParameters); - HttpResponseMessage response = await _httpClient.PostAsync(TokenEndpoint, requestContent, Request.CallCancelled); + HttpResponseMessage response = await _httpClient.PostAsync(Options.TokenEndpoint, requestContent, Request.CallCancelled); response.EnsureSuccessStatusCode(); string oauthTokenResponse = await response.Content.ReadAsStringAsync(); @@ -97,9 +95,11 @@ namespace Microsoft.Owin.Security.MicrosoftAccount return new AuthenticationTicket(null, properties); } - HttpResponseMessage graphResponse = await _httpClient.GetAsync( - GraphApiEndpoint + "?access_token=" + Uri.EscapeDataString(accessToken), Request.CallCancelled); + var graphRequest = new HttpRequestMessage(HttpMethod.Get, Options.UserInformationEndpoint); + graphRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); + var graphResponse = await _httpClient.SendAsync(graphRequest, Request.CallCancelled); graphResponse.EnsureSuccessStatusCode(); + string accountString = await graphResponse.Content.ReadAsStringAsync(); JObject accountInformation = JObject.Parse(accountString); @@ -165,13 +165,13 @@ namespace Microsoft.Owin.Security.MicrosoftAccount // LiveID requires a scope string, so if the user didn't set one we go for the least possible. if (string.IsNullOrWhiteSpace(scope)) { - scope = "wl.basic"; + scope = "https://graph.microsoft.com/user.read"; } string state = Options.StateDataFormat.Protect(extra); string authorizationEndpoint = - "https://login.live.com/oauth20_authorize.srf" + + Options.AuthorizationEndpoint + "?client_id=" + Uri.EscapeDataString(Options.ClientId) + "&scope=" + Uri.EscapeDataString(scope) + "&response_type=code" + diff --git a/src/Microsoft.Owin.Security.MicrosoftAccount/MicrosoftAccountAuthenticationOptions.cs b/src/Microsoft.Owin.Security.MicrosoftAccount/MicrosoftAccountAuthenticationOptions.cs index c53bb826..63e89ef6 100644 --- a/src/Microsoft.Owin.Security.MicrosoftAccount/MicrosoftAccountAuthenticationOptions.cs +++ b/src/Microsoft.Owin.Security.MicrosoftAccount/MicrosoftAccountAuthenticationOptions.cs @@ -27,6 +27,10 @@ namespace Microsoft.Owin.Security.MicrosoftAccount Scope = new List(); BackchannelTimeout = TimeSpan.FromSeconds(60); CookieManager = new CookieManager(); + + AuthorizationEndpoint = Constants.AuthorizationEndpoint; + TokenEndpoint = Constants.TokenEndpoint; + UserInformationEndpoint = Constants.UserInformationEndpoint; } /// @@ -62,6 +66,21 @@ namespace Microsoft.Owin.Security.MicrosoftAccount /// public string ClientSecret { get; set; } + /// + /// Gets or sets the URI where the client will be redirected to authenticate. + /// + public string AuthorizationEndpoint { get; set; } + + /// + /// Gets or sets the URI the middleware will access to exchange the OAuth token. + /// + public string TokenEndpoint { get; set; } + + /// + /// Gets or sets the URI the middleware will access to obtain the user information. + /// + public string UserInformationEndpoint { get; set; } + /// /// Gets or sets timeout value in milliseconds for back channel communications with Microsoft. /// diff --git a/src/Microsoft.Owin.Security.MicrosoftAccount/Provider/MicrosoftAccountAuthenticatedContext.cs b/src/Microsoft.Owin.Security.MicrosoftAccount/Provider/MicrosoftAccountAuthenticatedContext.cs index ed7c0680..25245fe6 100644 --- a/src/Microsoft.Owin.Security.MicrosoftAccount/Provider/MicrosoftAccountAuthenticatedContext.cs +++ b/src/Microsoft.Owin.Security.MicrosoftAccount/Provider/MicrosoftAccountAuthenticatedContext.cs @@ -52,17 +52,13 @@ namespace Microsoft.Owin.Security.MicrosoftAccount } Id = userId.ToString(); - Name = PropertyValueIfExists("name", userAsDictionary); - FirstName = PropertyValueIfExists("first_name", userAsDictionary); - LastName = PropertyValueIfExists("last_name", userAsDictionary); - - if (userAsDictionary.ContainsKey("emails")) + Name = PropertyValueIfExists("displayName", userAsDictionary); + FirstName = PropertyValueIfExists("givenName", userAsDictionary); + LastName = PropertyValueIfExists("surname", userAsDictionary); + Email = PropertyValueIfExists("mail", userAsDictionary); + if (Email == null) { - JToken emailsNode = user["emails"]; - foreach (var childAsProperty in emailsNode.OfType().Where(childAsProperty => childAsProperty.Name == "preferred")) - { - Email = childAsProperty.Value.ToString(); - } + Email = PropertyValueIfExists("userPrincipalName", userAsDictionary); } } @@ -72,17 +68,13 @@ namespace Microsoft.Owin.Security.MicrosoftAccount public JObject User { get; private set; } /// - /// Gets the access token provided by the Microsoft authenication service + /// Gets the access token provided by the Microsoft authentication service /// public string AccessToken { get; private set; } /// /// Gets the refresh token provided by Microsoft authentication service /// - /// - /// Refresh token is only available when wl.offline_access is request. - /// Otherwise, it is null. - /// public string RefreshToken { get; private set; } /// diff --git a/tests/FunctionalTests/Facts/Security/MicrosoftAccount/MicrosoftAuthentication.cs b/tests/FunctionalTests/Facts/Security/MicrosoftAccount/MicrosoftAuthentication.cs index 4cffa65f..b9ef9850 100644 --- a/tests/FunctionalTests/Facts/Security/MicrosoftAccount/MicrosoftAuthentication.cs +++ b/tests/FunctionalTests/Facts/Security/MicrosoftAccount/MicrosoftAuthentication.cs @@ -46,12 +46,12 @@ namespace FunctionalTests.Facts.Security.MicrosoftAccount // Unauthenticated request - verify Redirect url var response = await httpClient.GetAsync(applicationUrl); - Assert.Equal("https://login.live.com/oauth20_authorize.srf", response.Headers.Location.AbsoluteUri.Replace(response.Headers.Location.Query, string.Empty)); + Assert.Equal("https://login.microsoftonline.com/common/oauth2/v2.0/authorize", response.Headers.Location.AbsoluteUri.Replace(response.Headers.Location.Query, string.Empty)); var queryItems = response.Headers.Location.ParseQueryString(); Assert.Equal("code", queryItems["response_type"]); Assert.Equal("000000004C0F442C", queryItems["client_id"]); Assert.Equal(applicationUrl + "signin-microsoft", queryItems["redirect_uri"]); - Assert.Equal("wl.basic wl.signin", queryItems["scope"]); + Assert.Equal("https://graph.microsoft.com/user.read", queryItems["scope"]); Assert.Equal("ValidStateData", queryItems["state"]); Assert.Equal("custom", queryItems["custom_redirect_uri"]); @@ -173,9 +173,6 @@ namespace FunctionalTests.Facts.Security.MicrosoftAccount StateDataFormat = new CustomStateDataFormat(), }; - option.Scope.Add("wl.basic"); - option.Scope.Add("wl.signin"); - app.UseMicrosoftAccountAuthentication(option); app.UseExternalApplication("Microsoft"); } @@ -185,9 +182,12 @@ namespace FunctionalTests.Facts.Security.MicrosoftAccount { protected async override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { - var response = new HttpResponseMessage(); + var response = new HttpResponseMessage() + { + Content = new StringContent("") + }; - if (request.RequestUri.AbsoluteUri.StartsWith("https://login.live.com/oauth20_token.srf")) + if (request.RequestUri.AbsoluteUri.StartsWith("https://login.microsoftonline.com/common/oauth2/v2.0/token")) { var formData = request.Content.ReadAsFormDataAsync().Result; @@ -198,7 +198,7 @@ namespace FunctionalTests.Facts.Security.MicrosoftAccount if (formData["redirect_uri"] != null && formData["redirect_uri"].EndsWith("signin-microsoft") && formData["client_id"] == "000000004C0F442C" && formData["client_secret"] == "EkXbW-Vr6Rqzi6pugl1jWIBsDotKLmqR") { - response.Content = new StringContent("{\"token_type\":\"bearer\",\"expires_in\":3600,\"scope\":\"wl.basic\",\"access_token\":\"ValidAccessToken\",\"refresh_token\":\"ValidRefreshToken\",\"authentication_token\":\"ValidAuthenticationToken\"}"); + response.Content = new StringContent("{\"token_type\":\"bearer\",\"expires_in\":3600,\"scope\":\"https://graph.microsoft.com/user.read\",\"access_token\":\"ValidAccessToken\",\"refresh_token\":\"ValidRefreshToken\",\"authentication_token\":\"ValidAuthenticationToken\"}"); } } else if (formData["code"] == "InvalidCert") @@ -231,12 +231,11 @@ namespace FunctionalTests.Facts.Security.MicrosoftAccount } } } - else if (request.RequestUri.AbsoluteUri.StartsWith("https://apis.live.net/v5.0/me")) + else if (request.RequestUri.AbsoluteUri.StartsWith("https://graph.microsoft.com/v1.0/me")) { - var queryParameters = request.RequestUri.ParseQueryString(); - if (queryParameters["access_token"] == "ValidAccessToken") + if (request.Headers.Authorization.Parameter == "ValidAccessToken") { - response.Content = new StringContent("{\r \"id\": \"fccf9a24999f4f4f\", \r \"name\": \"Owinauthtester Owinauthtester\", \r \"first_name\": \"Owinauthtester\", \r \"last_name\": \"Owinauthtester\", \r \"link\": \"https://profile.live.com/\", \r \"gender\": null, \r \"locale\": \"en_US\", \r \"updated_time\": \"2013-08-27T22:18:14+0000\"\r}"); + response.Content = new StringContent("{\r \"id\": \"fccf9a24999f4f4f\", \r \"displayName\": \"Owinauthtester Owinauthtester\", \r \"givenName\": \"Owinauthtester\", \r \"surname\": \"Owinauthtester\", \r \"link\": \"https://profile.live.com/\", \r \"gender\": null, \r \"locale\": \"en_US\", \r \"updated_time\": \"2013-08-27T22:18:14+0000\"\r}"); } else { diff --git a/tests/Microsoft.Owin.Security.Tests/MicrosoftAccount/MicrosoftAccountMiddlewareTests.cs b/tests/Microsoft.Owin.Security.Tests/MicrosoftAccount/MicrosoftAccountMiddlewareTests.cs index 49431370..b9e3a757 100644 --- a/tests/Microsoft.Owin.Security.Tests/MicrosoftAccount/MicrosoftAccountMiddlewareTests.cs +++ b/tests/Microsoft.Owin.Security.Tests/MicrosoftAccount/MicrosoftAccountMiddlewareTests.cs @@ -65,7 +65,7 @@ namespace Microsoft.Owin.Security.Tests.MicrosoftAccount var transaction = await SendAsync(server, "http://example.com/challenge"); transaction.Response.StatusCode.ShouldBe(HttpStatusCode.Redirect); var location = transaction.Response.Headers.Location.AbsoluteUri; - location.ShouldContain("https://login.live.com/oauth20_authorize.srf"); + location.ShouldContain("https://login.microsoftonline.com/common/oauth2/v2.0/authorize"); location.ShouldContain("response_type=code"); location.ShouldContain("client_id="); location.ShouldContain("redirect_uri="); @@ -84,7 +84,7 @@ namespace Microsoft.Owin.Security.Tests.MicrosoftAccount { Sender = async req => { - if (req.RequestUri.AbsoluteUri == "https://login.live.com/oauth20_token.srf") + if (req.RequestUri.AbsoluteUri == "https://login.microsoftonline.com/common/oauth2/v2.0/token") { return await ReturnJsonResponse(new { @@ -94,18 +94,15 @@ namespace Microsoft.Owin.Security.Tests.MicrosoftAccount refresh_token = "Test Refresh Token" }); } - else if (req.RequestUri.GetLeftPart(UriPartial.Path) == "https://apis.live.net/v5.0/me") + else if (req.RequestUri.GetLeftPart(UriPartial.Path) == "https://graph.microsoft.com/v1.0/me") { return await ReturnJsonResponse(new { id = "Test User ID", - name = "Test Name", - first_name = "Test Given Name", - last_name = "Test Family Name", - emails = new - { - preferred = "Test email" - } + displayName = "Test Name", + givenName = "Test Given Name", + surname = "Test Family Name", + mail = "Test email" }); }