// 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.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Moq; using Xunit; namespace Microsoft.AspNetCore.Identity.Service.Test { public class ApplicationValidatorTest { public ApplicationErrorDescriber errorDescriber = new ApplicationErrorDescriber(); [Theory] [InlineData(null)] [InlineData("")] [InlineData("~/")] [InlineData("0123456789012345678901234567789001234567890")] public async Task ValidateApplication_FailsForInvalidNames(string name) { // Arrange var validator = new ApplicationValidator(new ApplicationErrorDescriber()); var application = CreateApplication(name: name, clientId: Guid.NewGuid().ToString()); var manager = CreateTestManager(); var expectedError = new List { errorDescriber.InvalidApplicationName(name) }; // Act var result = await validator.ValidateAsync(manager, application); // Assert Assert.False(result.Succeeded); Assert.Equal(expectedError, result.Errors, ErrorsComparer.Instance); } [Theory] [InlineData("TestApplication")] [InlineData("testapplication")] [InlineData("TESTAPPLICATION")] public async Task ValidateApplication_FailsForDuplicateApplicationNames(string name) { // Arrange var validator = new ApplicationValidator(new ApplicationErrorDescriber()); var application = CreateApplication("ApplicationId", name, Guid.NewGuid().ToString()); var manager = CreateTestManager(duplicateName: true); var expectedError = new List { errorDescriber.DuplicateApplicationName(name) }; // Act var result = await validator.ValidateAsync(manager, application); // Assert Assert.False(result.Succeeded); Assert.Equal(expectedError, result.Errors, ErrorsComparer.Instance); } [Theory] [InlineData(null)] [InlineData("")] [InlineData("~/")] [InlineData("0123456789012345678901234567789001234567890")] public async Task ValidateApplication_FailsForInvalidClientIds(string clientId) { // Arrange var validator = new ApplicationValidator(new ApplicationErrorDescriber()); var application = CreateApplication(clientId: clientId); var manager = CreateTestManager(); var expectedError = new List { errorDescriber.InvalidApplicationClientId(clientId) }; // Act var result = await validator.ValidateAsync(manager, application); // Assert Assert.False(result.Succeeded); Assert.Equal(expectedError, result.Errors, ErrorsComparer.Instance); } [Theory] [InlineData("ClientId")] [InlineData("clientid")] [InlineData("CLIENTID")] public async Task ValidateApplication_FailsForDuplicateClientIds(string clientId) { // Arrange var validator = new ApplicationValidator(new ApplicationErrorDescriber()); var application = CreateApplication("ApplicationId", "TestApplication", clientId); var manager = CreateTestManager(duplicateClientId: true); var expectedError = new List { errorDescriber.DuplicateApplicationClientId(clientId) }; // Act var result = await validator.ValidateAsync(manager, application); // Assert Assert.False(result.Succeeded); Assert.Equal(expectedError, result.Errors, ErrorsComparer.Instance); } [Fact] public async Task ValidateApplication_SucceedsWhenNameAndClientIdAreValid() { // Arrange var validator = new ApplicationValidator(new ApplicationErrorDescriber()); var application = CreateApplication(); var manager = CreateTestManager(); // Act var result = await validator.ValidateAsync(manager, application); // Assert Assert.True(result.Succeeded); } [Theory] [InlineData("urn:ietf:wg:oauth:2.0:oob")] [InlineData("URN:IETF:WG:OAUTH:2.0:OOB")] [InlineData("https://www.example.com/signout-oidc")] [InlineData("HTTPS://WWW.EXAMPLE.COM/SIGNOUT-OIDC")] public async Task ValidateLogoutUri_FailsIfTheApplicationAlreadyContainsTheUri(string logoutUri) { // Arrange var validator = new ApplicationValidator(new ApplicationErrorDescriber()); var application = CreateApplication(); var manager = CreateTestManager(); var expectedError = new List { errorDescriber.DuplicateLogoutUri(logoutUri) }; // Act var result = await validator.ValidateLogoutUriAsync(manager, application, logoutUri); // Assert Assert.False(result.Succeeded); Assert.Equal(expectedError, result.Errors, ErrorsComparer.Instance); } [Fact] public async Task ValidateLogoutUri_FailsIfTheUriIsRelative() { // Arrange var validator = new ApplicationValidator(new ApplicationErrorDescriber()); var application = CreateApplication(); var manager = CreateTestManager(); var expectedError = new List { errorDescriber.InvalidLogoutUri("/signout-oidc") }; // Act var result = await validator.ValidateLogoutUriAsync(manager, application, "/signout-oidc"); // Assert Assert.False(result.Succeeded); Assert.Equal(expectedError, result.Errors, ErrorsComparer.Instance); } [Fact] public async Task ValidateLogoutUri_FailsIfTheUriIsNotHttps() { // Arrange var validator = new ApplicationValidator(new ApplicationErrorDescriber()); var application = CreateApplication(); var manager = CreateTestManager(); var expectedError = new List { errorDescriber.NoHttpsUri("http://www.example.com/signout-oidc") }; // Act var result = await validator.ValidateLogoutUriAsync(manager, application, "http://www.example.com/signout-oidc"); // Assert Assert.False(result.Succeeded); Assert.Equal(expectedError, result.Errors, ErrorsComparer.Instance); } [Fact] public async Task ValidateLogoutUri_FailsIfTheUriIsNotInTheSameDomainAsTheOthers() { // Arrange var validator = new ApplicationValidator(new ApplicationErrorDescriber()); var application = CreateApplication(); var manager = CreateTestManager(); var expectedError = new List { errorDescriber.DifferentDomains() }; // Act var result = await validator.ValidateLogoutUriAsync(manager, application, "https://www.contoso.com/signout-oidc"); // Assert Assert.False(result.Succeeded); Assert.Equal(expectedError, result.Errors, ErrorsComparer.Instance); } [Fact] public async Task ValidateLogoutUri_FailsFailsForOtherNonHttpsUris() { // Arrange var validator = new ApplicationValidator(new ApplicationErrorDescriber()); var application = CreateApplication(); var manager = CreateTestManager(); var expectedError = new List { errorDescriber.NoHttpsUri("urn:self:aspnet:identity:integrated"), errorDescriber.DifferentDomains() }; // Act var result = await validator.ValidateLogoutUriAsync(manager, application, "urn:self:aspnet:identity:integrated"); // Assert Assert.False(result.Succeeded); Assert.Equal(expectedError, result.Errors, ErrorsComparer.Instance); } [Theory] [InlineData("https://www.example.com/another-path")] [InlineData("HTTPS://WWW.EXAMPLE.COM/ANOTHER-PATH")] public async Task ValidateLogoutUri_SucceedsForOtherUrisOnTheSameDomain(string logoutUri) { // Arrange var validator = new ApplicationValidator(new ApplicationErrorDescriber()); var application = CreateApplication(); var manager = CreateTestManager(); // Act var result = await validator.ValidateLogoutUriAsync(manager, application, logoutUri); // Assert Assert.True(result.Succeeded); } [Theory] [InlineData("urn:ietf:wg:oauth:2.0:oob")] [InlineData("URN:IETF:WG:OAUTH:2.0:OOB")] [InlineData("https://www.example.com/signin-oidc")] [InlineData("HTTPS://WWW.EXAMPLE.COM/SIGNIN-OIDC")] public async Task ValidateRedirectUri_FailsIfTheApplicationAlreadyContainsTheUri(string redirectUri) { // Arrange var validator = new ApplicationValidator(new ApplicationErrorDescriber()); var application = CreateApplication(); var manager = CreateTestManager(); var expectedError = new List { errorDescriber.DuplicateRedirectUri(redirectUri) }; // Act var result = await validator.ValidateRedirectUriAsync(manager, application, redirectUri); // Assert Assert.False(result.Succeeded); Assert.Equal(expectedError, result.Errors, ErrorsComparer.Instance); } [Fact] public async Task ValidateRedirectUri_FailsIfTheUriIsRelative() { // Arrange var validator = new ApplicationValidator(new ApplicationErrorDescriber()); var application = CreateApplication(); var manager = CreateTestManager(); var expectedError = new List { errorDescriber.InvalidRedirectUri("/signin-oidc") }; // Act var result = await validator.ValidateRedirectUriAsync(manager, application, "/signin-oidc"); // Assert Assert.False(result.Succeeded); Assert.Equal(expectedError, result.Errors, ErrorsComparer.Instance); } [Fact] public async Task ValidateRedirectUri_FailsIfTheUriIsNotHttps() { // Arrange var validator = new ApplicationValidator(new ApplicationErrorDescriber()); var application = CreateApplication(); var manager = CreateTestManager(); var expectedError = new List { errorDescriber.NoHttpsUri("http://www.example.com/signin-oidc") }; // Act var result = await validator.ValidateRedirectUriAsync(manager, application, "http://www.example.com/signin-oidc"); // Assert Assert.False(result.Succeeded); Assert.Equal(expectedError, result.Errors, ErrorsComparer.Instance); } [Fact] public async Task ValidateRedirectUri_FailsIfTheUriIsNotInTheSameDomainAsTheOthers() { // Arrange var validator = new ApplicationValidator(new ApplicationErrorDescriber()); var application = CreateApplication(); var manager = CreateTestManager(); var expectedError = new List { errorDescriber.DifferentDomains() }; // Act var result = await validator.ValidateRedirectUriAsync(manager, application, "https://www.contoso.com/signin-oidc"); // Assert Assert.False(result.Succeeded); Assert.Equal(expectedError, result.Errors, ErrorsComparer.Instance); } [Fact] public async Task ValidateRedirectUri_FailsForOtherNonHttpsUris() { // Arrange var validator = new ApplicationValidator(new ApplicationErrorDescriber()); var application = CreateApplication(); var manager = CreateTestManager(); var expectedError = new List { errorDescriber.NoHttpsUri("urn:self:aspnet:identity:integrated"), errorDescriber.DifferentDomains() }; // Act var result = await validator.ValidateRedirectUriAsync(manager, application, "urn:self:aspnet:identity:integrated"); // Assert Assert.False(result.Succeeded); Assert.Equal(expectedError, result.Errors, ErrorsComparer.Instance); } [Theory] [InlineData("https://www.example.com/another-path")] [InlineData("HTTPS://WWW.EXAMPLE.COM/ANOTHER-PATH")] public async Task ValidateRedirectUri_SucceedsForOtherUrisOnTheSameDomain(string redirectUri) { // Arrange var validator = new ApplicationValidator(new ApplicationErrorDescriber()); var application = CreateApplication(); var manager = CreateTestManager(); // Act var result = await validator.ValidateRedirectUriAsync(manager, application, redirectUri); // Assert Assert.True(result.Succeeded); } [Theory] [InlineData(null)] [InlineData("")] [InlineData("~/")] [InlineData("012345678901234567890")] public async Task ValidateScope_FailsForInvalidScopeValues(string scope) { // Arrange var validator = new ApplicationValidator(new ApplicationErrorDescriber()); var application = CreateApplication(); var manager = CreateTestManager(); var expectedError = new List { errorDescriber.InvalidScope(scope) }; // Act var result = await validator.ValidateScopeAsync(manager, application, scope); // Assert Assert.False(result.Succeeded); Assert.Equal(expectedError, result.Errors, ErrorsComparer.Instance); } [Theory] [InlineData("openid")] [InlineData("openID")] [InlineData("OPENID")] public async Task ValidateScope_FailsForDuplicateScopeValues(string scope) { // Arrange var validator = new ApplicationValidator(new ApplicationErrorDescriber()); var application = CreateApplication(); var manager = CreateTestManager(); var expectedError = new List { errorDescriber.DuplicateScope(scope) }; // Act var result = await validator.ValidateScopeAsync(manager, application, scope); // Assert Assert.False(result.Succeeded); Assert.Equal(expectedError, result.Errors, ErrorsComparer.Instance); } [Fact] public async Task ValidateScope_SucceedsWhenScopesAreValid() { // Arrange var validator = new ApplicationValidator(new ApplicationErrorDescriber()); var application = CreateApplication(); var manager = CreateTestManager(); // Act var result = await validator.ValidateScopeAsync(manager, application, "offline_access"); // Assert Assert.True(result.Succeeded); } private TestApplication CreateApplication( string id = "Id", string name = "TestApplication", string clientId = "ClientId") => new TestApplication { Id = id, Name = name, ClientId = clientId, RedirectUris = new List { "urn:ietf:wg:oauth:2.0:oob", "https://www.example.com/signin-oidc" }, LogoutUris = new List { "urn:ietf:wg:oauth:2.0:oob", "https://www.example.com/signout-oidc" }, Scopes = new List { "openid" } }; private ApplicationManager CreateTestManager(bool duplicateName = false, bool duplicateClientId = false) { var otherApplication = CreateApplication(); var store = new Mock>(); if (duplicateName) { store.Setup(s => s.FindByNameAsync(It.IsAny(), It.IsAny())) .ReturnsAsync(otherApplication); } if (duplicateClientId) { store.Setup(s => s.FindByClientIdAsync(It.IsAny(), It.IsAny())) .ReturnsAsync(otherApplication); } store.Setup(s => s.GetApplicationIdAsync(It.IsAny(), It.IsAny())) .ReturnsAsync, string>((a, ct) => a.Id); store.Setup(s => s.GetApplicationNameAsync(It.IsAny(), It.IsAny())) .ReturnsAsync, string>((a, ct) => a.Name); store.Setup(s => s.GetApplicationClientIdAsync(It.IsAny(), It.IsAny())) .ReturnsAsync, string>((a, ct) => a.ClientId); store.As>() .Setup(s => s.FindRegisteredUrisAsync(It.IsAny(), It.IsAny())) .ReturnsAsync(otherApplication.RedirectUris); store.As>() .Setup(s => s.FindRegisteredLogoutUrisAsync(It.IsAny(), It.IsAny())) .ReturnsAsync(otherApplication.LogoutUris); store.As>() .Setup(s => s.FindScopesAsync(It.IsAny(), It.IsAny())) .ReturnsAsync(otherApplication.Scopes); return new ApplicationManager( Options.Create(new ApplicationOptions()), store.Object, Mock.Of>(), Enumerable.Empty>(), Mock.Of>>(), new ApplicationErrorDescriber()); } private class ErrorsComparer : IEqualityComparer> { public static ErrorsComparer Instance = new ErrorsComparer(); public bool Equals( IEnumerable left, IEnumerable right) { var leftOrdered = left.OrderBy(o => o.Code).ThenBy(o => o.Description).ToArray(); var rightOrdered = right.OrderBy(o => o.Code).ThenBy(o => o.Description).ToArray(); return leftOrdered.Length == rightOrdered.Length && leftOrdered.Select((s, i) => s.Code.Equals(rightOrdered[i].Code) && s.Description.Equals(rightOrdered[i].Description)).All(a => a); } public int GetHashCode(IEnumerable obj) { return 1; } } public class TestApplication { public string Id { get; set; } public string Name { get; set; } public string ClientId { get; set; } public List RedirectUris { get; set; } public List LogoutUris { get; set; } public List Scopes { get; set; } } } }