// 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.Globalization; using System.Linq; using System.Net; using System.Net.Http; using System.Security.Claims; using System.Threading.Tasks; using System.Xml.Linq; using Microsoft.AspNet.Authentication.Cookies; using Microsoft.AspNet.Authentication.OpenIdConnect; using Microsoft.AspNet.Builder; using Microsoft.AspNet.Http; using Microsoft.AspNet.Http.Authentication; using Microsoft.AspNet.TestHost; using Microsoft.Framework.DependencyInjection; using Microsoft.Framework.WebEncoders; using Microsoft.IdentityModel.Protocols.OpenIdConnect; using Moq; using Shouldly; using Xunit; namespace Microsoft.AspNet.Authentication.Tests.OpenIdConnect { public class OpenIdConnectMiddlewareTests { 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 DefaultAuthority = @"https://example.com/common"; 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(options => { options.Authority = DefaultAuthority; options.ClientId = "Test Id"; options.Configuration = TestUtilities.DefaultOpenIdConnectConfiguration; options.AuthenticationMethod = OpenIdConnectAuthenticationMethod.FormPost; }); var transaction = await SendAsync(server, DefaultHost + Challenge); transaction.Response.StatusCode.ShouldBe(HttpStatusCode.OK); transaction.Response.Content.Headers.ContentType.MediaType.ShouldBe("text/html"); transaction.ResponseText.ShouldContain("form"); } [Fact] public async Task ChallengeWillSetDefaults() { var stateDataFormat = new AuthenticationPropertiesFormaterKeyValue(); var queryValues = ExpectedQueryValues.Defaults(DefaultAuthority); queryValues.State = OpenIdConnectAuthenticationDefaults.AuthenticationPropertiesKey + "=" + stateDataFormat.Protect(new AuthenticationProperties()); var server = CreateServer(options => { SetOptions(options, DefaultParameters(), queryValues); }); var transaction = await SendAsync(server, DefaultHost + Challenge); transaction.Response.StatusCode.ShouldBe(HttpStatusCode.Redirect); queryValues.CheckValues(transaction.Response.Headers.Location.AbsoluteUri, DefaultParameters()); } [Fact] public async Task ChallengeWillSetNonceCookie() { var server = CreateServer(options => { options.Authority = DefaultAuthority; options.ClientId = "Test Id"; options.Configuration = TestUtilities.DefaultOpenIdConnectConfiguration; }); var transaction = await SendAsync(server, DefaultHost + Challenge); transaction.SetCookie.Single().ShouldContain(OpenIdConnectAuthenticationDefaults.CookieNoncePrefix); } [Fact] public async Task ChallengeWillUseOptionsProperties() { var queryValues = new ExpectedQueryValues(DefaultAuthority); var server = CreateServer(options => { SetOptions(options, DefaultParameters(), queryValues); }); var transaction = await SendAsync(server, DefaultHost + Challenge); transaction.Response.StatusCode.ShouldBe(HttpStatusCode.Redirect); queryValues.CheckValues(transaction.Response.Headers.Location.AbsoluteUri, DefaultParameters()); } /// /// Tests RedirectToIdentityProviderContext replaces the OpenIdConnectMesssage correctly. /// /// Task [Theory] [InlineData(Challenge, OpenIdConnectRequestType.AuthenticationRequest)] [InlineData(Signout, OpenIdConnectRequestType.LogoutRequest)] public async Task ChallengeSettingMessage(string challenge, OpenIdConnectRequestType requestType) { var configuration = new OpenIdConnectConfiguration { AuthorizationEndpoint = ExpectedAuthorizeRequest, EndSessionEndpoint = ExpectedLogoutRequest }; var queryValues = new ExpectedQueryValues(DefaultAuthority, configuration) { RequestType = requestType }; var server = CreateServer(SetProtocolMessageOptions); var transaction = await SendAsync(server, DefaultHost + challenge); transaction.Response.StatusCode.ShouldBe(HttpStatusCode.Redirect); queryValues.CheckValues(transaction.Response.Headers.Location.AbsoluteUri, new string[] {}); } private static void SetProtocolMessageOptions(OpenIdConnectAuthenticationOptions options) { var mockOpenIdConnectMessage = new Mock(); mockOpenIdConnectMessage.Setup(m => m.CreateAuthenticationRequestUrl()).Returns(ExpectedAuthorizeRequest); mockOpenIdConnectMessage.Setup(m => m.CreateLogoutRequestUrl()).Returns(ExpectedLogoutRequest); options.AutomaticAuthentication = true; options.Events = new OpenIdConnectAuthenticationEvents() { OnRedirectToIdentityProvider = (context) => { context.ProtocolMessage = mockOpenIdConnectMessage.Object; return Task.FromResult(null); } }; } /// /// 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(DefaultAuthority); var stateDataFormat = new AuthenticationPropertiesFormaterKeyValue(); var properties = new AuthenticationProperties(); if (challenge == ChallengeWithProperties) { properties.Items.Add("item1", Guid.NewGuid().ToString()); } var server = CreateServer(options => { SetOptions(options, DefaultParameters(new string[] { OpenIdConnectParameterNames.State }), queryValues, stateDataFormat); options.AutomaticAuthentication = challenge.Equals(ChallengeWithOutContext); options.Events = new OpenIdConnectAuthenticationEvents() { OnRedirectToIdentityProvider = context => { context.ProtocolMessage.State = userState; return Task.FromResult(null); } }; }, null, properties); var transaction = await SendAsync(server, DefaultHost + challenge); transaction.Response.StatusCode.ShouldBe(HttpStatusCode.Redirect); if (challenge != ChallengeWithProperties) { if (userState != null) { properties.Items.Add(OpenIdConnectAuthenticationDefaults.UserstatePropertiesKey, userState); } properties.Items.Add(OpenIdConnectAuthenticationDefaults.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(DefaultAuthority); var queryValuesSetInEvent = new ExpectedQueryValues(DefaultAuthority); var server = CreateServer(options => { SetOptions(options, DefaultParameters(), queryValues); options.Events = new OpenIdConnectAuthenticationEvents() { 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 transaction = await SendAsync(server, DefaultHost + Challenge); transaction.Response.StatusCode.ShouldBe(HttpStatusCode.Redirect); queryValuesSetInEvent.CheckValues(transaction.Response.Headers.Location.AbsoluteUri, DefaultParameters()); } private void SetOptions(OpenIdConnectAuthenticationOptions options, List parameters, ExpectedQueryValues queryValues, ISecureDataFormat secureDataFormat = null) { foreach (var param in parameters) { if (param.Equals(OpenIdConnectParameterNames.ClientId)) options.ClientId = queryValues.ClientId; else if (param.Equals(OpenIdConnectParameterNames.RedirectUri)) options.RedirectUri = queryValues.RedirectUri; 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(); } private List DefaultParameters(string[] additionalParams = null) { var parameters = new List { OpenIdConnectParameterNames.ClientId, OpenIdConnectParameterNames.RedirectUri, OpenIdConnectParameterNames.Resource, OpenIdConnectParameterNames.ResponseMode, OpenIdConnectParameterNames.Scope, }; if (additionalParams != null) parameters.AddRange(additionalParams); return parameters; } private static void DefaultChallengeOptions(OpenIdConnectAuthenticationOptions options) { options.AuthenticationScheme = "OpenIdConnectHandlerTest"; options.AutomaticAuthentication = true; options.ClientId = Guid.NewGuid().ToString(); options.ConfigurationManager = TestUtilities.DefaultOpenIdConnectConfigurationManager; options.StateDataFormat = new AuthenticationPropertiesFormaterKeyValue(); } [Fact] public async Task SignOutWithDefaultRedirectUri() { var configuration = TestUtilities.DefaultOpenIdConnectConfiguration; var server = CreateServer(options => { options.Authority = DefaultAuthority; options.ClientId = "Test Id"; options.Configuration = configuration; }); var transaction = await SendAsync(server, DefaultHost + Signout); transaction.Response.StatusCode.ShouldBe(HttpStatusCode.Redirect); transaction.Response.Headers.Location.AbsoluteUri.ShouldBe(configuration.EndSessionEndpoint); } [Fact] public async Task SignOutWithCustomRedirectUri() { var configuration = TestUtilities.DefaultOpenIdConnectConfiguration; var server = CreateServer(options => { options.Authority = DefaultAuthority; options.ClientId = "Test Id"; options.Configuration = configuration; options.PostLogoutRedirectUri = "https://example.com/logout"; }); var transaction = await SendAsync(server, DefaultHost + Signout); transaction.Response.StatusCode.ShouldBe(HttpStatusCode.Redirect); transaction.Response.Headers.Location.AbsoluteUri.ShouldContain(UrlEncoder.Default.UrlEncode("https://example.com/logout")); } [Fact] public async Task SignOutWith_Specific_RedirectUri_From_Authentication_Properites() { var configuration = TestUtilities.DefaultOpenIdConnectConfiguration; var server = CreateServer(options => { options.Authority = DefaultAuthority; options.ClientId = "Test Id"; options.Configuration = configuration; options.PostLogoutRedirectUri = "https://example.com/logout"; }); var transaction = await SendAsync(server, "https://example.com/signout_with_specific_redirect_uri"); transaction.Response.StatusCode.ShouldBe(HttpStatusCode.Redirect); transaction.Response.Headers.Location.AbsoluteUri.ShouldContain(UrlEncoder.Default.UrlEncode("http://www.example.com/specific_redirect_uri")); } private static TestServer CreateServer(Action configureOptions, Func handler = null, AuthenticationProperties properties = null) { return TestServer.Create(app => { app.UseCookieAuthentication(options => { options.AuthenticationScheme = OpenIdConnectAuthenticationDefaults.AuthenticationScheme; }); app.UseOpenIdConnectAuthentication(configureOptions); app.Use(async (context, next) => { var req = context.Request; var res = context.Response; if (req.Path == new PathString(Challenge)) { await context.Authentication.ChallengeAsync(OpenIdConnectAuthenticationDefaults.AuthenticationScheme); } else if (req.Path == new PathString(ChallengeWithProperties)) { await context.Authentication.ChallengeAsync(OpenIdConnectAuthenticationDefaults.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(OpenIdConnectAuthenticationDefaults.AuthenticationScheme, new ClaimsPrincipal()); } else if (req.Path == new PathString(Signout)) { await context.Authentication.SignOutAsync(OpenIdConnectAuthenticationDefaults.AuthenticationScheme); } else if (req.Path == new PathString("/signout_with_specific_redirect_uri")) { await context.Authentication.SignOutAsync( OpenIdConnectAuthenticationDefaults.AuthenticationScheme, new AuthenticationProperties() { RedirectUri = "http://www.example.com/specific_redirect_uri" }); } else if (handler != null) { await handler(context); } else { await next(); } }); }, services => { services.AddAuthentication(); services.Configure(options => { options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme; }); }); } 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(".AspNet.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 public void NonceCookieExpirationTime() { DateTime utcNow = DateTime.UtcNow; GetNonceExpirationTime(noncePrefix + DateTime.MaxValue.Ticks.ToString(CultureInfo.InvariantCulture) + nonceDelimiter, TimeSpan.FromHours(1)).ShouldBe(DateTime.MaxValue); GetNonceExpirationTime(noncePrefix + DateTime.MinValue.Ticks.ToString(CultureInfo.InvariantCulture) + nonceDelimiter, TimeSpan.FromHours(1)).ShouldBe(DateTime.MinValue + TimeSpan.FromHours(1)); GetNonceExpirationTime(noncePrefix + utcNow.Ticks.ToString(CultureInfo.InvariantCulture) + nonceDelimiter, TimeSpan.FromHours(1)).ShouldBe(utcNow + TimeSpan.FromHours(1)); GetNonceExpirationTime(noncePrefix, TimeSpan.FromHours(1)).ShouldBe(DateTime.MinValue); GetNonceExpirationTime("", TimeSpan.FromHours(1)).ShouldBe(DateTime.MinValue); GetNonceExpirationTime(noncePrefix + noncePrefix, TimeSpan.FromHours(1)).ShouldBe(DateTime.MinValue); GetNonceExpirationTime(noncePrefix + utcNow.Ticks.ToString(CultureInfo.InvariantCulture) + nonceDelimiter + utcNow.Ticks.ToString(CultureInfo.InvariantCulture) + nonceDelimiter, TimeSpan.FromHours(1)).ShouldBe(utcNow + TimeSpan.FromHours(1)); GetNonceExpirationTime(utcNow.Ticks.ToString(CultureInfo.InvariantCulture) + nonceDelimiter + utcNow.Ticks.ToString(CultureInfo.InvariantCulture) + nonceDelimiter, TimeSpan.FromHours(1)).ShouldBe(DateTime.MinValue); } private static DateTime GetNonceExpirationTime(string keyname, TimeSpan nonceLifetime) { DateTime nonceTime = DateTime.MinValue; string timestamp = null; int endOfTimestamp; if (keyname.StartsWith(noncePrefix, StringComparison.Ordinal)) { timestamp = keyname.Substring(noncePrefix.Length); endOfTimestamp = timestamp.IndexOf('.'); if (endOfTimestamp != -1) { timestamp = timestamp.Substring(0, endOfTimestamp); try { nonceTime = DateTime.FromBinary(Convert.ToInt64(timestamp, CultureInfo.InvariantCulture)); if ((nonceTime >= DateTime.UtcNow) && ((DateTime.MaxValue - nonceTime) < nonceLifetime)) nonceTime = DateTime.MaxValue; else nonceTime += nonceLifetime; } catch { } } } return nonceTime; } } }