// 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.Text.Encodings.Web;
using System.Threading.Tasks;
using System.Xml.Linq;
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;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
using Xunit;
namespace Microsoft.AspNetCore.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(new OpenIdConnectOptions
{
Authority = 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(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 = 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.Contains(OpenIdConnectDefaults.CookieStatePrefix, secondCookie);
Assert.Contains("expires", secondCookie);
}
[Fact]
public async Task ChallengeWillUseOptionsProperties()
{
var queryValues = new ExpectedQueryValues(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(DefaultAuthority, configuration)
{
RequestType = OpenIdConnectRequestType.AuthenticationRequest
};
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.
///
/// Task
[Fact]
public async Task SignOutSettingMessage()
{
var configuration = new OpenIdConnectConfiguration
{
EndSessionEndpoint = ExpectedLogoutRequest
};
var queryValues = new ExpectedQueryValues(DefaultAuthority, configuration)
{
RequestType = OpenIdConnectRequestType.LogoutRequest
};
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()
{
OnRedirectToAuthenticationEndpoint = (context) =>
{
context.ProtocolMessage = fakeOpenIdRequestMessage;
return Task.FromResult(0);
},
OnRedirectToEndSessionEndpoint = (context) =>
{
context.ProtocolMessage = fakeOpenIdRequestMessage;
return Task.FromResult(0);
}
};
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(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()
{
OnRedirectToAuthenticationEndpoint = context =>
{
context.ProtocolMessage.State = userState;
context.ProtocolMessage.RedirectUri = queryValues.RedirectUri;
return Task.FromResult