diff --git a/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/AuthenticationPropertiesFormaterKeyValue.cs b/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/AuthenticationPropertiesFormaterKeyValue.cs deleted file mode 100644 index 1be4b80bca..0000000000 --- a/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/AuthenticationPropertiesFormaterKeyValue.cs +++ /dev/null @@ -1,70 +0,0 @@ -// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using System.Text; -using System.Text.Encodings.Web; -using Microsoft.AspNetCore.Http.Authentication; - -namespace Microsoft.AspNetCore.Authentication.Tests.OpenIdConnect -{ - /// - /// This formatter creates an easy to read string of the format: "'key1' 'value1' ..." - /// - public class AuthenticationPropertiesFormaterKeyValue : ISecureDataFormat - { - string _protectedString = Guid.NewGuid().ToString(); - - public string Protect(AuthenticationProperties data) - { - if (data == null || data.Items.Count == 0) - { - return "null"; - } - - var sb = new StringBuilder(); - foreach(var item in data.Items) - { - sb.Append(Uri.EscapeDataString(item.Key) + " " + Uri.EscapeDataString(item.Value) + " "); - } - - return sb.ToString(); - } - public string Protect(AuthenticationProperties data, string purpose) - { - return Protect(data); - } - - public AuthenticationProperties Unprotect(string protectedText) - { - if (string.IsNullOrEmpty(protectedText)) - { - return null; - } - - if (protectedText == "null") - { - return new AuthenticationProperties(); - } - - string[] items = protectedText.Split(' '); - if (items.Length % 2 != 0) - { - return null; - } - - var propeties = new AuthenticationProperties(); - for (int i = 0; i < items.Length - 1; i+=2) - { - propeties.Items.Add(items[i], items[i + 1]); - } - - return propeties; - } - - public AuthenticationProperties Unprotect(string protectedText, string purpose) - { - return Unprotect(protectedText); - } - } -} diff --git a/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/ExpectedQueryValues.cs b/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/ExpectedQueryValues.cs deleted file mode 100644 index 98df02ee61..0000000000 --- a/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/ExpectedQueryValues.cs +++ /dev/null @@ -1,175 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Text; -using System.Text.Encodings.Web; -using Microsoft.IdentityModel.Protocols.OpenIdConnect; -using Xunit; - -namespace Microsoft.AspNetCore.Authentication.Tests.OpenIdConnect -{ - /// - /// This helper class is used to check that query string parameters are as expected. - /// - public class ExpectedQueryValues - { - public ExpectedQueryValues(string authority, OpenIdConnectConfiguration configuration = null) - { - Authority = authority; - Configuration = configuration ?? TestUtilities.DefaultOpenIdConnectConfiguration; - } - - public static ExpectedQueryValues Defaults(string authority) - { - var result = new ExpectedQueryValues(authority); - result.Scope = OpenIdConnectScope.OpenIdProfile; - result.ResponseType = OpenIdConnectResponseType.CodeIdToken; - return result; - } - - public void CheckValues(string query, IEnumerable parameters) - { - var errors = new List(); - if (!query.StartsWith(ExpectedAuthority)) - { - errors.Add("ExpectedAuthority: " + ExpectedAuthority); - } - - foreach(var str in parameters) - { - if (str == OpenIdConnectParameterNames.ClientId) - { - if (!query.Contains(ExpectedClientId)) - errors.Add("ExpectedClientId: " + ExpectedClientId); - - continue; - } - - if (str == OpenIdConnectParameterNames.RedirectUri) - { - if(!query.Contains(ExpectedRedirectUri)) - errors.Add("ExpectedRedirectUri: " + ExpectedRedirectUri); - - continue; - } - - if (str == OpenIdConnectParameterNames.Resource) - { - if(!query.Contains(ExpectedResource)) - errors.Add("ExpectedResource: " + ExpectedResource); - - continue; - } - - if (str == OpenIdConnectParameterNames.ResponseMode) - { - if(!query.Contains(ExpectedResponseMode)) - errors.Add("ExpectedResponseMode: " + ExpectedResponseMode); - - continue; - } - - if (str == OpenIdConnectParameterNames.Scope) - { - if (!query.Contains(ExpectedScope)) - errors.Add("ExpectedScope: " + ExpectedScope); - - continue; - } - - if (str == OpenIdConnectParameterNames.State) - { - if (!query.Contains(ExpectedState)) - errors.Add("ExpectedState: " + ExpectedState); - - continue; - } - } - - if (errors.Count > 0) - { - var sb = new StringBuilder(); - sb.AppendLine("query string not as expected: " + Environment.NewLine + query + Environment.NewLine); - foreach (var str in errors) - { - sb.AppendLine(str); - } - - Debug.WriteLine(sb.ToString()); - Assert.True(false, sb.ToString()); - } - } - - public UrlEncoder Encoder { get; set; } = UrlEncoder.Default; - - public string Authority { get; set; } - - public string ClientId { get; set; } = Guid.NewGuid().ToString(); - - public string RedirectUri { get; set; } = Guid.NewGuid().ToString(); - - public OpenIdConnectRequestType RequestType { get; set; } = OpenIdConnectRequestType.Authentication; - - public string Resource { get; set; } = Guid.NewGuid().ToString(); - - public string ResponseMode { get; set; } = OpenIdConnectResponseMode.FormPost; - - public string ResponseType { get; set; } = Guid.NewGuid().ToString(); - - public string Scope { get; set; } = Guid.NewGuid().ToString(); - - public string State { get; set; } = Guid.NewGuid().ToString(); - - public string ExpectedAuthority - { - get - { - if (RequestType == OpenIdConnectRequestType.Token) - { - return Configuration?.EndSessionEndpoint ?? Authority + @"/oauth2/token"; - } - else if (RequestType == OpenIdConnectRequestType.Logout) - { - return Configuration?.TokenEndpoint ?? Authority + @"/oauth2/logout"; - } - - return Configuration?.AuthorizationEndpoint ?? Authority + (@"/oauth2/authorize"); - } - } - - public OpenIdConnectConfiguration Configuration { get; set; } - - public string ExpectedClientId - { - get { return OpenIdConnectParameterNames.ClientId + "=" + Encoder.Encode(ClientId); } - } - - public string ExpectedRedirectUri - { - get { return OpenIdConnectParameterNames.RedirectUri + "=" + Encoder.Encode(RedirectUri); } - } - - public string ExpectedResource - { - get { return OpenIdConnectParameterNames.Resource + "=" + Encoder.Encode(Resource); } - } - - public string ExpectedResponseMode - { - get { return OpenIdConnectParameterNames.ResponseMode + "=" + Encoder.Encode(ResponseMode); } - } - - public string ExpectedScope - { - get { return OpenIdConnectParameterNames.Scope + "=" + Encoder.Encode(Scope); } - } - - public string ExpectedState - { - get { return Encoder.Encode(State); } - } - } -} diff --git a/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/Infrastructure/TestDefaultValues.cs b/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/Infrastructure/TestDefaultValues.cs deleted file mode 100644 index de921bb96a..0000000000 --- a/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/Infrastructure/TestDefaultValues.cs +++ /dev/null @@ -1,10 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -namespace Microsoft.AspNetCore.Authentication.Tests.OpenIdConnect.Infrastructre -{ - internal class TestDefaultValues - { - public static readonly string DefaultAuthority = @"https://example.com/common"; - } -} \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/MockOpenIdConnectMessage.cs b/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/MockOpenIdConnectMessage.cs new file mode 100644 index 0000000000..432980f771 --- /dev/null +++ b/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/MockOpenIdConnectMessage.cs @@ -0,0 +1,21 @@ +using Microsoft.IdentityModel.Protocols.OpenIdConnect; + +namespace Microsoft.AspNetCore.Authentication.Tests.OpenIdConnect +{ + internal class MockOpenIdConnectMessage : OpenIdConnectMessage + { + public string TestAuthorizeEndpoint { get; set; } + + public string TestLogoutRequest { get; set; } + + public override string CreateAuthenticationRequestUrl() + { + return TestAuthorizeEndpoint ?? base.CreateAuthenticationRequestUrl(); + } + + public override string CreateLogoutRequestUrl() + { + return TestLogoutRequest ?? base.CreateLogoutRequestUrl(); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/OpenIdConnectChallengeTests.cs b/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/OpenIdConnectChallengeTests.cs new file mode 100644 index 0000000000..b2e2514d61 --- /dev/null +++ b/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/OpenIdConnectChallengeTests.cs @@ -0,0 +1,322 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Linq; +using System.Net; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication.OpenIdConnect; +using Microsoft.AspNetCore.Http.Authentication; +using Microsoft.IdentityModel.Protocols.OpenIdConnect; +using Xunit; + +namespace Microsoft.AspNetCore.Authentication.Tests.OpenIdConnect +{ + public class OpenIdConnectChallengeTests + { + [Fact] + public async Task ChallengeIsIssuedCorrectly() + { + var settings = new TestSettings( + opt => opt.AuthenticationMethod = OpenIdConnectRedirectBehavior.RedirectGet); + + var server = settings.CreateTestServer(); + var transaction = await TestTransaction.SendAsync(server, ChallengeEndpoint); + + var res = transaction.Response; + Assert.Equal(HttpStatusCode.Redirect, res.StatusCode); + Assert.NotNull(res.Headers.Location); + + settings.ValidateChallengeRedirect( + res.Headers.Location, + OpenIdConnectParameterNames.ClientId, + OpenIdConnectParameterNames.ResponseType, + OpenIdConnectParameterNames.ResponseMode, + OpenIdConnectParameterNames.Scope, + OpenIdConnectParameterNames.RedirectUri); + } + + /* + Example of a form post + +
+ + + + + + + + +
+ + + */ + [Fact] + public async Task ChallengeIssueedCorrectlyForFormPost() + { + var settings = new TestSettings( + opt => opt.AuthenticationMethod = OpenIdConnectRedirectBehavior.FormPost); + + var server = settings.CreateTestServer(); + var transaction = await TestTransaction.SendAsync(server, ChallengeEndpoint); + + var res = transaction.Response; + Assert.Equal(HttpStatusCode.OK, res.StatusCode); + Assert.Equal("text/html", transaction.Response.Content.Headers.ContentType.MediaType); + + var body = await res.Content.ReadAsStringAsync(); + settings.ValidateChallengeFormPost( + body, + OpenIdConnectParameterNames.ClientId, + OpenIdConnectParameterNames.ResponseType, + OpenIdConnectParameterNames.ResponseMode, + OpenIdConnectParameterNames.Scope, + OpenIdConnectParameterNames.RedirectUri); + } + + [Theory] + [InlineData("sample_user_state")] + [InlineData(null)] + public async Task ChallengeCanSetUserStateThroughProperties(string userState) + { + var settings = new TestSettings(); + + var properties = new AuthenticationProperties(); + properties.Items.Add(OpenIdConnectDefaults.UserstatePropertiesKey, userState); + + var server = TestServerBuilder.CreateServer(settings.Options, handler: null, properties: properties); + var transaction = await TestTransaction.SendAsync(server, TestDefaultValues.TestHost + TestServerBuilder.ChallengeWithProperties); + + var res = transaction.Response; + Assert.Equal(HttpStatusCode.Redirect, res.StatusCode); + Assert.NotNull(res.Headers.Location); + + var values = settings.ValidateChallengeRedirect(res.Headers.Location); + var actualState = values[OpenIdConnectParameterNames.State]; + var actualProperties = settings.Options.StateDataFormat.Unprotect(actualState); + + Assert.Equal(userState ?? string.Empty, actualProperties.Items[OpenIdConnectDefaults.UserstatePropertiesKey]); + } + + [Theory] + [InlineData("sample_user_state")] + [InlineData(null)] + public async Task OnRedirectToIdentityProviderEventCanSetState(string userState) + { + var settings = new TestSettings(opt => + { + opt.Events = new OpenIdConnectEvents() + { + OnRedirectToIdentityProvider = context => + { + context.ProtocolMessage.State = userState; + return Task.FromResult(0); + } + }; + }); + + var server = settings.CreateTestServer(); + var transaction = await TestTransaction.SendAsync(server, ChallengeEndpoint); + + var res = transaction.Response; + Assert.Equal(HttpStatusCode.Redirect, res.StatusCode); + Assert.NotNull(res.Headers.Location); + + var values = settings.ValidateChallengeRedirect(res.Headers.Location); + var actualState = values[OpenIdConnectParameterNames.State]; + var actualProperties = settings.Options.StateDataFormat.Unprotect(actualState); + + if (userState != null) + { + Assert.Equal(userState, actualProperties.Items[OpenIdConnectDefaults.UserstatePropertiesKey]); + } + else + { + Assert.False(actualProperties.Items.ContainsKey(OpenIdConnectDefaults.UserstatePropertiesKey)); + } + } + + [Fact] + public async Task OnRedirectToIdentityProviderEventIsHit() + { + var eventIsHit = false; + var settings = new TestSettings( + opts => + { + opts.Events = new OpenIdConnectEvents() + { + OnRedirectToIdentityProvider = context => + { + eventIsHit = true; + return Task.FromResult(0); + } + }; + } + ); + + var server = settings.CreateTestServer(); + var transaction = await TestTransaction.SendAsync(server, ChallengeEndpoint); + + Assert.True(eventIsHit); + + var res = transaction.Response; + Assert.Equal(HttpStatusCode.Redirect, res.StatusCode); + Assert.NotNull(res.Headers.Location); + + settings.ValidateChallengeRedirect( + res.Headers.Location, + OpenIdConnectParameterNames.ClientId, + OpenIdConnectParameterNames.ResponseType, + OpenIdConnectParameterNames.ResponseMode, + OpenIdConnectParameterNames.Scope, + OpenIdConnectParameterNames.RedirectUri); + } + + + [Fact] + public async Task OnRedirectToIdentityProviderEventCanReplaceValues() + { + var newClientId = Guid.NewGuid().ToString(); + + var settings = new TestSettings( + opts => + { + opts.Events = new OpenIdConnectEvents() + { + OnRedirectToIdentityProvider = context => + { + context.ProtocolMessage.ClientId = newClientId; + + return Task.FromResult(0); + } + }; + } + ); + + var server = settings.CreateTestServer(); + var transaction = await TestTransaction.SendAsync(server, ChallengeEndpoint); + + var res = transaction.Response; + Assert.Equal(HttpStatusCode.Redirect, res.StatusCode); + Assert.NotNull(res.Headers.Location); + + settings.ValidateChallengeRedirect( + res.Headers.Location, + OpenIdConnectParameterNames.ResponseType, + OpenIdConnectParameterNames.ResponseMode, + OpenIdConnectParameterNames.Scope, + OpenIdConnectParameterNames.RedirectUri); + + var actual = res.Headers.Location.Query.Trim('?').Split('&').Single(seg => seg.StartsWith($"{OpenIdConnectParameterNames.ClientId}=")); + Assert.Equal($"{OpenIdConnectParameterNames.ClientId}={newClientId}", actual); + } + + [Fact] + public async Task OnRedirectToIdentityProviderEventCanReplaceMessage() + { + var newMessage = new MockOpenIdConnectMessage + { + TestAuthorizeEndpoint = $"http://example.com/{Guid.NewGuid()}/oauth2/signin" + }; + + var settings = new TestSettings( + opts => + { + opts.Events = new OpenIdConnectEvents() + { + OnRedirectToIdentityProvider = context => + { + context.ProtocolMessage = newMessage; + + return Task.FromResult(0); + } + }; + } + ); + + var server = settings.CreateTestServer(); + var transaction = await TestTransaction.SendAsync(server, ChallengeEndpoint); + + var res = transaction.Response; + Assert.Equal(HttpStatusCode.Redirect, res.StatusCode); + Assert.NotNull(res.Headers.Location); + + // The CreateAuthenticationRequestUrl method is overridden MockOpenIdConnectMessage where + // query string is not generated and the authorization endpoint is replaced. + Assert.Equal(newMessage.TestAuthorizeEndpoint, res.Headers.Location.AbsoluteUri); + } + [Fact] + public async Task OnRedirectToIdentityProviderEventHandlesResponse() + { + var settings = new TestSettings( + opts => + { + opts.Events = new OpenIdConnectEvents() + { + OnRedirectToIdentityProvider = context => + { + context.HandleResponse(); + return Task.FromResult(0); + } + }; + } + ); + + var server = settings.CreateTestServer(); + var transaction = await TestTransaction.SendAsync(server, ChallengeEndpoint); + + var res = transaction.Response; + Assert.Equal(HttpStatusCode.OK, res.StatusCode); + Assert.Null(res.Headers.Location); + } + + // This test can be further refined. When one auth middleware skips, the authentication responsibility + // will be flowed to the next one. A dummy auth middleware can be added to ensure the correct logic. + [Fact] + public async Task OnRedirectToIdentityProviderEventSkipResponse() + { + var settings = new TestSettings( + opts => + { + opts.Events = new OpenIdConnectEvents() + { + OnRedirectToIdentityProvider = context => + { + context.SkipToNextMiddleware(); + return Task.FromResult(0); + } + }; + } + ); + + var server = settings.CreateTestServer(); + var transaction = await TestTransaction.SendAsync(server, ChallengeEndpoint); + + var res = transaction.Response; + Assert.Equal(HttpStatusCode.OK, res.StatusCode); + Assert.Null(res.Headers.Location); + } + + [Fact] + public async Task ChallengeSetsNonceAndStateCookies() + { + var settings = new TestSettings(); + var server = settings.CreateTestServer(); + var transaction = await TestTransaction.SendAsync(server, ChallengeEndpoint); + + var firstCookie = transaction.SetCookie.First(); + Assert.Contains(OpenIdConnectDefaults.CookieNoncePrefix, firstCookie); + Assert.Contains("expires", firstCookie); + + var secondCookie = transaction.SetCookie.Skip(1).First(); + Assert.StartsWith(".AspNetCore.Correlation.OpenIdConnect.", secondCookie); + Assert.Contains("expires", secondCookie); + } + + private static string ChallengeEndpoint => TestDefaultValues.TestHost + TestServerBuilder.Challenge; + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/OpenIdConnectConfigurationTests.cs b/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/OpenIdConnectConfigurationTests.cs index 4ada6ff859..3603c2bf40 100644 --- a/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/OpenIdConnectConfigurationTests.cs +++ b/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/OpenIdConnectConfigurationTests.cs @@ -2,7 +2,6 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; -using Microsoft.AspNetCore.Authentication.Tests.OpenIdConnect.Infrastructre; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.TestHost; diff --git a/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/OpenIdConnectMiddlewareTests.cs b/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/OpenIdConnectMiddlewareTests.cs index 5853e38967..4a750ea41d 100644 --- a/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/OpenIdConnectMiddlewareTests.cs +++ b/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/OpenIdConnectMiddlewareTests.cs @@ -2,24 +2,11 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; -using System.Collections.Generic; using System.Globalization; -using System.Linq; using System.Net; -using System.Net.Http; -using System.Security.Claims; using System.Text.Encodings.Web; using System.Threading.Tasks; -using System.Xml.Linq; -using Microsoft.AspNetCore.Authentication.Cookies; -using Microsoft.AspNetCore.Authentication.OpenIdConnect; -using Microsoft.AspNetCore.Authentication.Tests.OpenIdConnect.Infrastructre; using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Authentication; -using Microsoft.AspNetCore.TestHost; -using Microsoft.Extensions.DependencyInjection; using Microsoft.IdentityModel.Protocols.OpenIdConnect; using Xunit; @@ -29,318 +16,47 @@ namespace Microsoft.AspNetCore.Authentication.Tests.OpenIdConnect { static string noncePrefix = "OpenIdConnect." + "Nonce."; static string nonceDelimiter = "."; - const string Challenge = "/challenge"; - const string ChallengeWithOutContext = "/challengeWithOutContext"; - const string ChallengeWithProperties = "/challengeWithProperties"; const string DefaultHost = @"https://example.com"; - const string ExpectedAuthorizeRequest = @"https://example.com/common/oauth2/signin"; - const string ExpectedLogoutRequest = @"https://example.com/common/oauth2/logout"; const string Logout = "/logout"; - const string Signin = "/signin"; - const string Signout = "/signout"; - - [Fact] - public async Task ChallengeWillIssueHtmlFormWhenEnabled() - { - var server = CreateServer(new OpenIdConnectOptions - { - Authority = TestDefaultValues.DefaultAuthority, - ClientId = "Test Id", - Configuration = TestUtilities.DefaultOpenIdConnectConfiguration, - AuthenticationMethod = OpenIdConnectRedirectBehavior.FormPost - }); - var transaction = await SendAsync(server, DefaultHost + Challenge); - Assert.Equal(HttpStatusCode.OK, transaction.Response.StatusCode); - Assert.Equal("text/html", transaction.Response.Content.Headers.ContentType.MediaType); - Assert.Contains("form", transaction.ResponseText); - } - - [Fact] - public async Task ChallengeWillSetDefaults() - { - var stateDataFormat = new AuthenticationPropertiesFormaterKeyValue(); - var queryValues = ExpectedQueryValues.Defaults(TestDefaultValues.DefaultAuthority); - queryValues.State = OpenIdConnectDefaults.AuthenticationPropertiesKey + "=" + stateDataFormat.Protect(new AuthenticationProperties()); - var server = CreateServer(GetOptions(DefaultParameters(), queryValues)); - - var transaction = await SendAsync(server, DefaultHost + Challenge); - Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); - queryValues.CheckValues(transaction.Response.Headers.Location.AbsoluteUri, DefaultParameters()); - } - - [Fact] - public async Task ChallengeWillSetNonceAndStateCookies() - { - var server = CreateServer(new OpenIdConnectOptions - { - Authority = TestDefaultValues.DefaultAuthority, - ClientId = "Test Id", - Configuration = TestUtilities.DefaultOpenIdConnectConfiguration - }); - var transaction = await SendAsync(server, DefaultHost + Challenge); - - var firstCookie = transaction.SetCookie.First(); - Assert.Contains(OpenIdConnectDefaults.CookieNoncePrefix, firstCookie); - Assert.Contains("expires", firstCookie); - - var secondCookie = transaction.SetCookie.Skip(1).First(); - Assert.StartsWith(".AspNetCore.Correlation.OpenIdConnect.", secondCookie); - Assert.Contains("expires", secondCookie); - } - - [Fact] - public async Task ChallengeWillUseOptionsProperties() - { - var queryValues = new ExpectedQueryValues(TestDefaultValues.DefaultAuthority); - var server = CreateServer(GetOptions(DefaultParameters(), queryValues)); - - var transaction = await SendAsync(server, DefaultHost + Challenge); - Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); - queryValues.CheckValues(transaction.Response.Headers.Location.AbsoluteUri, DefaultParameters()); - } - - /// - /// Tests RedirectForAuthenticationContext replaces the OpenIdConnectMesssage correctly. - /// - /// Task - [Fact] - public async Task ChallengeSettingMessage() - { - var configuration = new OpenIdConnectConfiguration - { - AuthorizationEndpoint = ExpectedAuthorizeRequest, - }; - - var queryValues = new ExpectedQueryValues(TestDefaultValues.DefaultAuthority, configuration) - { - RequestType = OpenIdConnectRequestType.Authentication - }; - var server = CreateServer(GetProtocolMessageOptions()); - var transaction = await SendAsync(server, DefaultHost + Challenge); - Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); - queryValues.CheckValues(transaction.Response.Headers.Location.AbsoluteUri, new string[] { }); - } /// /// Tests RedirectForSignOutContext replaces the OpenIdConnectMesssage correctly. - /// + /// summary> /// Task [Fact] public async Task SignOutSettingMessage() { - var configuration = new OpenIdConnectConfiguration + var setting = new TestSettings(opt => { - EndSessionEndpoint = ExpectedLogoutRequest - }; - - var queryValues = new ExpectedQueryValues(TestDefaultValues.DefaultAuthority, configuration) - { - RequestType = OpenIdConnectRequestType.Logout - }; - var server = CreateServer(GetProtocolMessageOptions()); - var transaction = await SendAsync(server, DefaultHost + Signout); - Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); - queryValues.CheckValues(transaction.Response.Headers.Location.AbsoluteUri, new string[] { }); - } - - private static OpenIdConnectOptions GetProtocolMessageOptions() - { - var options = new OpenIdConnectOptions(); - var fakeOpenIdRequestMessage = new FakeOpenIdConnectMessage(ExpectedAuthorizeRequest, ExpectedLogoutRequest); - options.AutomaticChallenge = true; - options.Events = new OpenIdConnectEvents() - { - OnRedirectToIdentityProvider = (context) => + opt.Configuration = new OpenIdConnectConfiguration { - context.ProtocolMessage = fakeOpenIdRequestMessage; - return Task.FromResult(0); - }, - OnRedirectToIdentityProviderForSignOut = (context) => - { - context.ProtocolMessage = fakeOpenIdRequestMessage; - return Task.FromResult(0); - } - }; - options.ClientId = "Test Id"; - options.Configuration = TestUtilities.DefaultOpenIdConnectConfiguration; - return options; - } - - private class FakeOpenIdConnectMessage : OpenIdConnectMessage - { - private readonly string _authorizeRequest; - private readonly string _logoutRequest; - - public FakeOpenIdConnectMessage(string authorizeRequest, string logoutRequest) - { - _authorizeRequest = authorizeRequest; - _logoutRequest = logoutRequest; - } - - public override string CreateAuthenticationRequestUrl() - { - return _authorizeRequest; - } - - public override string CreateLogoutRequestUrl() - { - return _logoutRequest; - } - } - - /// - /// Tests for users who want to add 'state'. There are two ways to do it. - /// 1. Users set 'state' (OpenIdConnectMessage.State) in the event. The runtime appends to that state. - /// 2. Users add to the AuthenticationProperties (context.AuthenticationProperties), values will be serialized. - /// - /// - /// - [Theory, MemberData("StateDataSet")] - public async Task ChallengeSettingState(string userState, string challenge) - { - var queryValues = new ExpectedQueryValues(TestDefaultValues.DefaultAuthority); - var stateDataFormat = new AuthenticationPropertiesFormaterKeyValue(); - var properties = new AuthenticationProperties(); - if (challenge == ChallengeWithProperties) - { - properties.Items.Add("item1", Guid.NewGuid().ToString()); - } - - var options = GetOptions(DefaultParameters(new string[] { OpenIdConnectParameterNames.State }), queryValues, stateDataFormat); - options.AutomaticChallenge = challenge.Equals(ChallengeWithOutContext); - options.Events = new OpenIdConnectEvents() - { - OnRedirectToIdentityProvider = context => - { - context.ProtocolMessage.State = userState; - context.ProtocolMessage.RedirectUri = queryValues.RedirectUri; - return Task.FromResult(null); - } - - }; - var server = CreateServer(options, null, properties); - - var transaction = await SendAsync(server, DefaultHost + challenge); - Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); - - if (challenge != ChallengeWithProperties) - { - if (userState != null) - { - properties.Items.Add(OpenIdConnectDefaults.UserstatePropertiesKey, userState); - } - properties.Items.Add(OpenIdConnectDefaults.RedirectUriForCodePropertiesKey, queryValues.RedirectUri); - } - - queryValues.State = stateDataFormat.Protect(properties); - queryValues.CheckValues(transaction.Response.Headers.Location.AbsoluteUri, DefaultParameters(new string[] { OpenIdConnectParameterNames.State })); - } - - public static TheoryData StateDataSet - { - get - { - var dataset = new TheoryData(); - dataset.Add(Guid.NewGuid().ToString(), Challenge); - dataset.Add(null, Challenge); - dataset.Add(Guid.NewGuid().ToString(), ChallengeWithOutContext); - dataset.Add(null, ChallengeWithOutContext); - dataset.Add(Guid.NewGuid().ToString(), ChallengeWithProperties); - dataset.Add(null, ChallengeWithProperties); - - return dataset; - } - } - - [Fact] - public async Task ChallengeWillUseEvents() - { - var queryValues = new ExpectedQueryValues(TestDefaultValues.DefaultAuthority); - var queryValuesSetInEvent = new ExpectedQueryValues(TestDefaultValues.DefaultAuthority); - var options = GetOptions(DefaultParameters(), queryValues); - options.Events = new OpenIdConnectEvents() - { - OnRedirectToIdentityProvider = context => - { - context.ProtocolMessage.ClientId = queryValuesSetInEvent.ClientId; - context.ProtocolMessage.RedirectUri = queryValuesSetInEvent.RedirectUri; - context.ProtocolMessage.Resource = queryValuesSetInEvent.Resource; - context.ProtocolMessage.Scope = queryValuesSetInEvent.Scope; - return Task.FromResult(null); - } - }; - var server = CreateServer(options); - - var transaction = await SendAsync(server, DefaultHost + Challenge); - Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); - queryValuesSetInEvent.CheckValues(transaction.Response.Headers.Location.AbsoluteUri, DefaultParameters()); - } - - private OpenIdConnectOptions GetOptions(List parameters, ExpectedQueryValues queryValues, ISecureDataFormat secureDataFormat = null) - { - var options = new OpenIdConnectOptions(); - foreach (var param in parameters) - { - if (param.Equals(OpenIdConnectParameterNames.ClientId)) - options.ClientId = queryValues.ClientId; - else if (param.Equals(OpenIdConnectParameterNames.Resource)) - options.Resource = queryValues.Resource; - else if (param.Equals(OpenIdConnectParameterNames.Scope)) - { - options.Scope.Clear(); - - foreach (var scope in queryValues.Scope.Split(' ')) - { - options.Scope.Add(scope); - } - } - } - - options.Authority = queryValues.Authority; - options.Configuration = queryValues.Configuration; - options.StateDataFormat = secureDataFormat ?? new AuthenticationPropertiesFormaterKeyValue(); - - return options; - } - - private List DefaultParameters(string[] additionalParams = null) - { - var parameters = - new List - { - OpenIdConnectParameterNames.ClientId, - OpenIdConnectParameterNames.Resource, - OpenIdConnectParameterNames.ResponseMode, - OpenIdConnectParameterNames.Scope, + EndSessionEndpoint = "https://example.com/signout_test/signout_request" }; + }); - if (additionalParams != null) - parameters.AddRange(additionalParams); + var server = setting.CreateTestServer(); - return parameters; - } + var transaction = await TestTransaction.SendAsync(server, DefaultHost + TestServerBuilder.Signout); + var res = transaction.Response; - private static void DefaultChallengeOptions(OpenIdConnectOptions options) - { - options.AuthenticationScheme = "OpenIdConnectHandlerTest"; - options.AutomaticChallenge = true; - options.ClientId = Guid.NewGuid().ToString(); - options.ConfigurationManager = TestUtilities.DefaultOpenIdConnectConfigurationManager; - options.StateDataFormat = new AuthenticationPropertiesFormaterKeyValue(); + Assert.Equal(HttpStatusCode.Redirect, res.StatusCode); + Assert.NotNull(res.Headers.Location); + + setting.ValidateSignoutRedirect(transaction.Response.Headers.Location); } [Fact] public async Task SignOutWithDefaultRedirectUri() { - var configuration = TestUtilities.DefaultOpenIdConnectConfiguration; - var server = CreateServer(new OpenIdConnectOptions + var configuration = TestDefaultValues.CreateDefaultOpenIdConnectConfiguration(); + var server = TestServerBuilder.CreateServer(new OpenIdConnectOptions { Authority = TestDefaultValues.DefaultAuthority, ClientId = "Test Id", Configuration = configuration }); - var transaction = await SendAsync(server, DefaultHost + Signout); + var transaction = await TestTransaction.SendAsync(server, DefaultHost + TestServerBuilder.Signout); Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); Assert.Equal(configuration.EndSessionEndpoint, transaction.Response.Headers.Location.AbsoluteUri); } @@ -348,8 +64,8 @@ namespace Microsoft.AspNetCore.Authentication.Tests.OpenIdConnect [Fact] public async Task SignOutWithCustomRedirectUri() { - var configuration = TestUtilities.DefaultOpenIdConnectConfiguration; - var server = CreateServer(new OpenIdConnectOptions + var configuration = TestDefaultValues.CreateDefaultOpenIdConnectConfiguration(); + var server = TestServerBuilder.CreateServer(new OpenIdConnectOptions { Authority = TestDefaultValues.DefaultAuthority, ClientId = "Test Id", @@ -357,7 +73,7 @@ namespace Microsoft.AspNetCore.Authentication.Tests.OpenIdConnect PostLogoutRedirectUri = "https://example.com/logout" }); - var transaction = await SendAsync(server, DefaultHost + Signout); + var transaction = await TestTransaction.SendAsync(server, DefaultHost + TestServerBuilder.Signout); Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); Assert.Contains(UrlEncoder.Default.Encode("https://example.com/logout"), transaction.Response.Headers.Location.AbsoluteUri); } @@ -365,8 +81,8 @@ namespace Microsoft.AspNetCore.Authentication.Tests.OpenIdConnect [Fact] public async Task SignOutWith_Specific_RedirectUri_From_Authentication_Properites() { - var configuration = TestUtilities.DefaultOpenIdConnectConfiguration; - var server = CreateServer(new OpenIdConnectOptions + var configuration = TestDefaultValues.CreateDefaultOpenIdConnectConfiguration(); + var server = TestServerBuilder.CreateServer(new OpenIdConnectOptions { Authority = TestDefaultValues.DefaultAuthority, ClientId = "Test Id", @@ -374,136 +90,13 @@ namespace Microsoft.AspNetCore.Authentication.Tests.OpenIdConnect PostLogoutRedirectUri = "https://example.com/logout" }); - var transaction = await SendAsync(server, "https://example.com/signout_with_specific_redirect_uri"); + var transaction = await TestTransaction.SendAsync(server, "https://example.com/signout_with_specific_redirect_uri"); Assert.Equal(HttpStatusCode.Redirect, transaction.Response.StatusCode); Assert.Contains(UrlEncoder.Default.Encode("http://www.example.com/specific_redirect_uri"), transaction.Response.Headers.Location.AbsoluteUri); } - private static TestServer CreateServer(OpenIdConnectOptions options, Func handler = null, AuthenticationProperties properties = null) - { - var builder = new WebHostBuilder() - .Configure(app => - { - app.UseCookieAuthentication(new CookieAuthenticationOptions - { - AuthenticationScheme = CookieAuthenticationDefaults.AuthenticationScheme - }); - app.UseOpenIdConnectAuthentication(options); - app.Use(async (context, next) => - { - var req = context.Request; - var res = context.Response; - - if (req.Path == new PathString(Challenge)) - { - await context.Authentication.ChallengeAsync(OpenIdConnectDefaults.AuthenticationScheme); - } - else if (req.Path == new PathString(ChallengeWithProperties)) - { - await context.Authentication.ChallengeAsync(OpenIdConnectDefaults.AuthenticationScheme, properties); - } - else if (req.Path == new PathString(ChallengeWithOutContext)) - { - res.StatusCode = 401; - } - else if (req.Path == new PathString(Signin)) - { - // REVIEW: this used to just be res.SignIn() - await context.Authentication.SignInAsync(OpenIdConnectDefaults.AuthenticationScheme, new ClaimsPrincipal()); - } - else if (req.Path == new PathString(Signout)) - { - await context.Authentication.SignOutAsync(OpenIdConnectDefaults.AuthenticationScheme); - } - else if (req.Path == new PathString("/signout_with_specific_redirect_uri")) - { - await context.Authentication.SignOutAsync( - OpenIdConnectDefaults.AuthenticationScheme, - new AuthenticationProperties() { RedirectUri = "http://www.example.com/specific_redirect_uri" }); - } - else if (handler != null) - { - await handler(context); - } - else - { - await next(); - } - }); - }) - .ConfigureServices(services => - { - services.AddAuthentication(); - services.Configure(authOptions => - { - authOptions.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme; - }); - }); - return new TestServer(builder); - } - - private static async Task SendAsync(TestServer server, string uri, string cookieHeader = null) - { - var request = new HttpRequestMessage(HttpMethod.Get, uri); - if (!string.IsNullOrEmpty(cookieHeader)) - { - request.Headers.Add("Cookie", cookieHeader); - } - - var transaction = new Transaction - { - Request = request, - Response = await server.CreateClient().SendAsync(request), - }; - - if (transaction.Response.Headers.Contains("Set-Cookie")) - { - transaction.SetCookie = transaction.Response.Headers.GetValues("Set-Cookie").ToList(); - } - - transaction.ResponseText = await transaction.Response.Content.ReadAsStringAsync(); - if (transaction.Response.Content != null && - transaction.Response.Content.Headers.ContentType != null && - transaction.Response.Content.Headers.ContentType.MediaType == "text/xml") - { - transaction.ResponseElement = XElement.Parse(transaction.ResponseText); - } - - return transaction; - } - - private class Transaction - { - public HttpRequestMessage Request { get; set; } - - public HttpResponseMessage Response { get; set; } - - public IList SetCookie { get; set; } - - public string ResponseText { get; set; } - - public XElement ResponseElement { get; set; } - - public string AuthenticationCookieValue - { - get - { - if (SetCookie != null && SetCookie.Count > 0) - { - var authCookie = SetCookie.SingleOrDefault(c => c.Contains(".AspNetCore.Cookie=")); - if (authCookie != null) - { - return authCookie.Substring(0, authCookie.IndexOf(';')); - } - } - - return null; - } - } - } - - [Fact] // Test Cases for calculating the expiration time of cookie from cookie name + [Fact] public void NonceCookieExpirationTime() { DateTime utcNow = DateTime.UtcNow; diff --git a/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/TestDefaultValues.cs b/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/TestDefaultValues.cs new file mode 100644 index 0000000000..c3e92a9042 --- /dev/null +++ b/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/TestDefaultValues.cs @@ -0,0 +1,48 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNetCore.Builder; +using Microsoft.IdentityModel.Protocols; +using Microsoft.IdentityModel.Protocols.OpenIdConnect; + +namespace Microsoft.AspNetCore.Authentication.Tests.OpenIdConnect +{ + internal class TestDefaultValues + { + public static readonly string DefaultAuthority = @"https://login.microsoftonline.com/common"; + + public static readonly string TestHost = @"https://example.com"; + + public static OpenIdConnectOptions CreateOpenIdConnectOptions() => + new OpenIdConnectOptions + { + Authority = TestDefaultValues.DefaultAuthority, + ClientId = Guid.NewGuid().ToString(), + Configuration = TestDefaultValues.CreateDefaultOpenIdConnectConfiguration() + }; + + public static OpenIdConnectOptions CreateOpenIdConnectOptions(Action update) + { + var options = CreateOpenIdConnectOptions(); + + if (update != null) + { + update(options); + } + + return options; + } + + public static OpenIdConnectConfiguration CreateDefaultOpenIdConnectConfiguration() => + new OpenIdConnectConfiguration() + { + AuthorizationEndpoint = DefaultAuthority + "/oauth2/authorize", + EndSessionEndpoint = DefaultAuthority + "/oauth2/endsessionendpoint", + TokenEndpoint = DefaultAuthority + "/oauth2/token" + }; + + public static IConfigurationManager CreateDefaultOpenIdConnectConfigurationManager() => + new StaticConfigurationManager(CreateDefaultOpenIdConnectConfiguration()); + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/TestServerBuilder.cs b/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/TestServerBuilder.cs new file mode 100644 index 0000000000..f8ab6fdb09 --- /dev/null +++ b/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/TestServerBuilder.cs @@ -0,0 +1,97 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.Authentication.OpenIdConnect; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Authentication; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Authentication.Tests.OpenIdConnect +{ + internal class TestServerBuilder + { + public static readonly string Challenge = "/challenge"; + public static readonly string ChallengeWithOutContext = "/challengeWithOutContext"; + public static readonly string ChallengeWithProperties = "/challengeWithProperties"; + public static readonly string Signin = "/signin"; + public static readonly string Signout = "/signout"; + + public static TestServer CreateServer(OpenIdConnectOptions options) + { + return CreateServer(options, handler: null, properties: null); + } + + public static TestServer CreateServer( + OpenIdConnectOptions options, + Func handler, + AuthenticationProperties properties) + { + var builder = new WebHostBuilder() + .Configure(app => + { + app.UseCookieAuthentication(new CookieAuthenticationOptions + { + AuthenticationScheme = CookieAuthenticationDefaults.AuthenticationScheme + }); + + app.UseOpenIdConnectAuthentication(options); + + app.Use(async (context, next) => + { + var req = context.Request; + var res = context.Response; + + if (req.Path == new PathString(Challenge)) + { + await context.Authentication.ChallengeAsync(OpenIdConnectDefaults.AuthenticationScheme); + } + else if (req.Path == new PathString(ChallengeWithProperties)) + { + await context.Authentication.ChallengeAsync(OpenIdConnectDefaults.AuthenticationScheme, properties); + } + else if (req.Path == new PathString(ChallengeWithOutContext)) + { + res.StatusCode = 401; + } + else if (req.Path == new PathString(Signin)) + { + // REVIEW: this used to just be res.SignIn() + await context.Authentication.SignInAsync(OpenIdConnectDefaults.AuthenticationScheme, new ClaimsPrincipal()); + } + else if (req.Path == new PathString(Signout)) + { + await context.Authentication.SignOutAsync(OpenIdConnectDefaults.AuthenticationScheme); + } + else if (req.Path == new PathString("/signout_with_specific_redirect_uri")) + { + await context.Authentication.SignOutAsync( + OpenIdConnectDefaults.AuthenticationScheme, + new AuthenticationProperties() { RedirectUri = "http://www.example.com/specific_redirect_uri" }); + } + else if (handler != null) + { + await handler(context); + } + else + { + await next(); + } + }); + }) + .ConfigureServices(services => + { + services.AddAuthentication(); + services.Configure(authOptions => authOptions.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme); + }); + + return new TestServer(builder); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/TestSettings.cs b/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/TestSettings.cs new file mode 100644 index 0000000000..47605dccea --- /dev/null +++ b/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/TestSettings.cs @@ -0,0 +1,230 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Text.Encodings.Web; +using System.Xml.Linq; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.TestHost; +using Microsoft.IdentityModel.Protocols.OpenIdConnect; +using Xunit; + +namespace Microsoft.AspNetCore.Authentication.Tests.OpenIdConnect +{ + /// + /// This helper class is used to check that query string parameters are as expected. + /// + internal class TestSettings + { + private readonly OpenIdConnectOptions _options; + + public TestSettings() : this(configure: null) + { + } + + public TestSettings(Action configure) + { + _options = TestDefaultValues.CreateOpenIdConnectOptions(configure); + } + + public TestSettings(OpenIdConnectOptions options) + { + _options = options; + } + + public OpenIdConnectOptions Options => _options; + + public UrlEncoder Encoder => UrlEncoder.Default; + + public string ExpectedState { get; set; } + + public TestServer CreateTestServer() => TestServerBuilder.CreateServer(Options); + + public IDictionary ValidateChallengeFormPost(string responseBody, params string[] parametersToValidate) + { + IDictionary formInputs = null; + var errors = new List(); + var xdoc = XDocument.Parse(responseBody.Replace("doctype", "DOCTYPE")); + var forms = xdoc.Descendants("form"); + if (forms.Count() != 1) + { + errors.Add("Only one form element is expected in response body."); + } + else + { + formInputs = forms.Single() + .Elements("input") + .ToDictionary(elem => elem.Attribute("name").Value, + elem => elem.Attribute("value").Value); + + ValidateParameters(formInputs, parametersToValidate, errors, htmlEncoded: false); + } + + if (errors.Any()) + { + var buf = new StringBuilder(); + buf.AppendLine($"The challenge form post is not valid."); + // buf.AppendLine(); + + foreach (var error in errors) + { + buf.AppendLine(error); + } + + Debug.WriteLine(buf.ToString()); + Assert.True(false, buf.ToString()); + } + + return formInputs; + } + + public IDictionary ValidateChallengeRedirect(Uri redirectUri, params string[] parametersToValidate) => + ValidateRedirectCore(redirectUri, OpenIdConnectRequestType.Authentication, parametersToValidate); + + public IDictionary ValidateSignoutRedirect(Uri redirectUri, params string[] parametersToValidate) => + ValidateRedirectCore(redirectUri, OpenIdConnectRequestType.Logout, parametersToValidate); + + private IDictionary ValidateRedirectCore(Uri redirectUri, OpenIdConnectRequestType requestType, string[] parametersToValidate) + { + var errors = new List(); + + // Validate the authority + ValidateExpectedAuthority(redirectUri.AbsoluteUri, errors, requestType); + + // Convert query to dictionary + var queryDict = string.IsNullOrEmpty(redirectUri.Query) ? + new Dictionary() : + redirectUri.Query.TrimStart('?').Split('&').Select(part => part.Split('=')).ToDictionary(parts => parts[0], parts => parts[1]); + + // Validate the query string parameters + ValidateParameters(queryDict, parametersToValidate, errors, htmlEncoded: true); + + if (errors.Any()) + { + var buf = new StringBuilder(); + buf.AppendLine($"The redirect uri is not valid."); + buf.AppendLine(redirectUri.AbsoluteUri); + + foreach (var error in errors) + { + buf.AppendLine(error); + } + + Debug.WriteLine(buf.ToString()); + Assert.True(false, buf.ToString()); + } + + return queryDict; + } + + private void ValidateParameters( + IDictionary actualValues, + IEnumerable parametersToValidate, + ICollection errors, + bool htmlEncoded) + { + foreach (var paramToValidate in parametersToValidate) + { + switch (paramToValidate) + { + case OpenIdConnectParameterNames.ClientId: + ValidateClientId(actualValues, errors, htmlEncoded); + break; + case OpenIdConnectParameterNames.ResponseType: + ValidateResponseType(actualValues, errors, htmlEncoded); + break; + case OpenIdConnectParameterNames.ResponseMode: + ValidateResponseMode(actualValues, errors, htmlEncoded); + break; + case OpenIdConnectParameterNames.Scope: + ValidateScope(actualValues, errors, htmlEncoded); + break; + case OpenIdConnectParameterNames.RedirectUri: + ValidateRedirectUri(actualValues, errors, htmlEncoded); + break; + case OpenIdConnectParameterNames.Resource: + ValidateResource(actualValues, errors, htmlEncoded); + break; + case OpenIdConnectParameterNames.State: + ValidateState(actualValues, errors, htmlEncoded); + break; + default: + throw new InvalidOperationException($"Unknown parameter \"{paramToValidate}\"."); + } + } + } + + private void ValidateExpectedAuthority(string absoluteUri, ICollection errors, OpenIdConnectRequestType requestType) + { + string expectedAuthority; + switch (requestType) + { + case OpenIdConnectRequestType.Token: + expectedAuthority = _options.Configuration?.TokenEndpoint ?? _options.Authority + @"/oauth2/token"; + break; + case OpenIdConnectRequestType.Logout: + expectedAuthority = _options.Configuration?.EndSessionEndpoint ?? _options.Authority + @"/oauth2/logout"; + break; + default: + expectedAuthority = _options.Configuration?.AuthorizationEndpoint ?? _options.Authority + @"/oauth2/authorize"; + break; + } + + if (!absoluteUri.StartsWith(expectedAuthority)) + { + errors.Add($"ExpectedAuthority: {expectedAuthority}"); + } + } + + private void ValidateClientId(IDictionary actualQuery, ICollection errors, bool htmlEncoded) => + ValidateQueryParameter(OpenIdConnectParameterNames.ClientId, _options.ClientId, actualQuery, errors, htmlEncoded); + + private void ValidateResponseType(IDictionary actualQuery, ICollection errors, bool htmlEncoded) => + ValidateQueryParameter(OpenIdConnectParameterNames.ResponseType, _options.ResponseType, actualQuery, errors, htmlEncoded); + + private void ValidateResponseMode(IDictionary actualQuery, ICollection errors, bool htmlEncoded) => + ValidateQueryParameter(OpenIdConnectParameterNames.ResponseMode, _options.ResponseMode, actualQuery, errors, htmlEncoded); + + private void ValidateScope(IDictionary actualQuery, ICollection errors, bool htmlEncoded) => + ValidateQueryParameter(OpenIdConnectParameterNames.Scope, string.Join(" ", _options.Scope), actualQuery, errors, htmlEncoded); + + private void ValidateRedirectUri(IDictionary actualQuery, ICollection errors, bool htmlEncoded) => + ValidateQueryParameter(OpenIdConnectParameterNames.RedirectUri, TestDefaultValues.TestHost + _options.CallbackPath, actualQuery, errors, htmlEncoded); + + private void ValidateResource(IDictionary actualQuery, ICollection errors, bool htmlEncoded) => + ValidateQueryParameter(OpenIdConnectParameterNames.RedirectUri, _options.Resource, actualQuery, errors, htmlEncoded); + + private void ValidateState(IDictionary actualQuery, ICollection errors, bool htmlEncoded) => + ValidateQueryParameter(OpenIdConnectParameterNames.State, ExpectedState, actualQuery, errors, htmlEncoded); + + private void ValidateQueryParameter( + string parameterName, + string expectedValue, + IDictionary actualQuery, + ICollection errors, + bool htmlEncoded) + { + string actualValue; + if (actualQuery.TryGetValue(parameterName, out actualValue)) + { + if (htmlEncoded) + { + expectedValue = Encoder.Encode(expectedValue); + } + + if (actualValue != expectedValue) + { + errors.Add($"Query parameter {parameterName}'s expected value is {expectedValue} but its actual value is {actualValue}"); + } + } + else + { + errors.Add($"Query parameter {parameterName} is missing"); + } + } + } +} diff --git a/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/TestTransaction.cs b/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/TestTransaction.cs new file mode 100644 index 0000000000..3bbd3152b3 --- /dev/null +++ b/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/TestTransaction.cs @@ -0,0 +1,77 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Threading.Tasks; +using System.Xml.Linq; +using Microsoft.AspNetCore.TestHost; + +namespace Microsoft.AspNetCore.Authentication.Tests.OpenIdConnect +{ + internal class TestTransaction + { + public static Task SendAsync(TestServer server, string url) + { + return SendAsync(server, url, cookieHeader: null); + } + + public static async Task SendAsync(TestServer server, string uri, string cookieHeader) + { + var request = new HttpRequestMessage(HttpMethod.Get, uri); + if (!string.IsNullOrEmpty(cookieHeader)) + { + request.Headers.Add("Cookie", cookieHeader); + } + + var transaction = new TestTransaction + { + Request = request, + Response = await server.CreateClient().SendAsync(request), + }; + + if (transaction.Response.Headers.Contains("Set-Cookie")) + { + transaction.SetCookie = transaction.Response.Headers.GetValues("Set-Cookie").ToList(); + } + + transaction.ResponseText = await transaction.Response.Content.ReadAsStringAsync(); + if (transaction.Response.Content != null && + transaction.Response.Content.Headers.ContentType != null && + transaction.Response.Content.Headers.ContentType.MediaType == "text/xml") + { + transaction.ResponseElement = XElement.Parse(transaction.ResponseText); + } + + return transaction; + } + + public HttpRequestMessage Request { get; set; } + + public HttpResponseMessage Response { get; set; } + + public IList SetCookie { get; set; } + + public string ResponseText { get; set; } + + public XElement ResponseElement { get; set; } + + public string AuthenticationCookieValue + { + get + { + if (SetCookie != null && SetCookie.Count > 0) + { + var authCookie = SetCookie.SingleOrDefault(c => c.Contains(".AspNetCore.Cookie=")); + if (authCookie != null) + { + return authCookie.Substring(0, authCookie.IndexOf(';')); + } + } + + return null; + } + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/TestUtilities.cs b/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/TestUtilities.cs deleted file mode 100644 index 6247c85b43..0000000000 --- a/test/Microsoft.AspNetCore.Authentication.Test/OpenIdConnect/TestUtilities.cs +++ /dev/null @@ -1,37 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using Microsoft.IdentityModel.Protocols; -using Microsoft.IdentityModel.Protocols.OpenIdConnect; - -namespace Microsoft.AspNetCore.Authentication.Tests.OpenIdConnect -{ - /// - /// These utilities are designed to test openidconnect related flows - /// - public class TestUtilities - { - public const string DefaultHost = @"http://localhost"; - - public static IConfigurationManager DefaultOpenIdConnectConfigurationManager - { - get - { - return new StaticConfigurationManager(DefaultOpenIdConnectConfiguration); - } - } - - public static OpenIdConnectConfiguration DefaultOpenIdConnectConfiguration - { - get - { - return new OpenIdConnectConfiguration() - { - AuthorizationEndpoint = @"https://login.microsoftonline.com/common/oauth2/authorize", - EndSessionEndpoint = @"https://login.microsoftonline.com/common/oauth2/endsessionendpoint", - TokenEndpoint = @"https://login.microsoftonline.com/common/oauth2/token", - }; - } - } - } -}