From 904ff0e06043edc2ae924947fff61f031b1a0c4c Mon Sep 17 00:00:00 2001 From: Javier Calvarro Nelson Date: Mon, 29 May 2017 19:25:32 -0700 Subject: [PATCH] Added validation, storage tests --- .../ApplicationStore.cs | 113 +- .../IdentityServiceApplication.cs | 1 - .../IdentityServiceSpecificationTestBase.cs | 729 +++++++- .../ApplicationErrorDescriber.cs | 111 ++ .../ApplicationManager.cs | 651 +++++--- .../ApplicationOptions.cs | 28 + .../ApplicationValidator.cs | 232 +++ .../IApplicationClaimStore.cs | 8 +- .../IApplicationValidator.cs | 5 + ...ntityServiceServiceCollectionExtensions.cs | 2 + .../InMemoryEFApplicationStoreTest.cs | 2 +- .../ApplicationStoreTest.cs | 91 +- .../InMemoryStore.cs | 6 + .../ApplicationManagerTest.cs | 1461 +++++++++++++++++ .../ApplicationValidatorTest.cs | 525 ++++++ .../ClientApplicationValidatorTest.cs | 8 +- 16 files changed, 3661 insertions(+), 312 deletions(-) create mode 100644 src/Microsoft.AspNetCore.Identity.Service/ApplicationErrorDescriber.cs create mode 100644 src/Microsoft.AspNetCore.Identity.Service/ApplicationOptions.cs create mode 100644 src/Microsoft.AspNetCore.Identity.Service/ApplicationValidator.cs create mode 100644 test/Microsoft.AspNetCore.Identity.Service.Test/ApplicationManagerTest.cs create mode 100644 test/Microsoft.AspNetCore.Identity.Service.Test/ApplicationValidatorTest.cs diff --git a/src/Microsoft.AspNetCore.Identity.Service.EntityFrameworkCore/ApplicationStore.cs b/src/Microsoft.AspNetCore.Identity.Service.EntityFrameworkCore/ApplicationStore.cs index f2690e73b8..7b51d0e54f 100644 --- a/src/Microsoft.AspNetCore.Identity.Service.EntityFrameworkCore/ApplicationStore.cs +++ b/src/Microsoft.AspNetCore.Identity.Service.EntityFrameworkCore/ApplicationStore.cs @@ -28,13 +28,16 @@ namespace Microsoft.AspNetCore.Identity.Service.EntityFrameworkCore { private bool _disposed; - public ApplicationStore(TContext context) + public ApplicationStore(TContext context, ApplicationErrorDescriber errorDescriber) { Context = context; + ErrorDescriber = errorDescriber; } public TContext Context { get; } + public ApplicationErrorDescriber ErrorDescriber { get; } + public DbSet ApplicationsSet => Context.Set(); public DbSet Scopes => Context.Set(); @@ -83,7 +86,7 @@ namespace Microsoft.AspNetCore.Identity.Service.EntityFrameworkCore } catch (DbUpdateConcurrencyException) { - return IdentityServiceResult.Failed(new IdentityServiceError() { Description = "Concurrency failure" }); + return IdentityServiceResult.Failed(ErrorDescriber.ConcurrencyFailure()); } return IdentityServiceResult.Success; } @@ -104,7 +107,7 @@ namespace Microsoft.AspNetCore.Identity.Service.EntityFrameworkCore } catch (DbUpdateConcurrencyException) { - return IdentityServiceResult.Failed(new IdentityServiceError() { Description = "Concurrency failure" }); + return IdentityServiceResult.Failed(ErrorDescriber.ConcurrencyFailure()); } return IdentityServiceResult.Success; } @@ -118,16 +121,34 @@ namespace Microsoft.AspNetCore.Identity.Service.EntityFrameworkCore { cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); + var oldQueryBehavior = Context.ChangeTracker.QueryTrackingBehavior; - return Applications.SingleOrDefaultAsync(a => a.ClientId == clientId, cancellationToken); + try + { + Context.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking; + return Applications.SingleOrDefaultAsync(a => a.ClientId == clientId, cancellationToken); + } + finally + { + Context.ChangeTracker.QueryTrackingBehavior = oldQueryBehavior; + } } public Task FindByNameAsync(string name, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); + var oldQueryBehavior = Context.ChangeTracker.QueryTrackingBehavior; - return Applications.SingleOrDefaultAsync(a => a.Name == name, cancellationToken); + try + { + Context.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking; + return Applications.SingleOrDefaultAsync(a => a.Name == name, cancellationToken); + } + finally + { + Context.ChangeTracker.QueryTrackingBehavior = oldQueryBehavior; + } } public async Task> FindByUserIdAsync(string userId, CancellationToken cancellationToken) @@ -195,7 +216,7 @@ namespace Microsoft.AspNetCore.Identity.Service.EntityFrameworkCore return redirectUris; } - public async Task RegisterRedirectUriAsync(TApplication app, string redirectUri, CancellationToken cancellationToken) + public Task RegisterRedirectUriAsync(TApplication app, string redirectUri, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); @@ -209,17 +230,9 @@ namespace Microsoft.AspNetCore.Identity.Service.EntityFrameworkCore throw new ArgumentNullException(nameof(redirectUri)); } - var existingRedirectUri = await RedirectUris.SingleOrDefaultAsync( - ru => ru.ApplicationId.Equals(app.Id) && ru.Value.Equals(redirectUri) && !ru.IsLogout); - if (existingRedirectUri != null) - { - return IdentityServiceResult.Failed( - new IdentityServiceError { Description = "A route with the same value already exists." }); - } - RedirectUris.Add(CreateRedirectUri(app, redirectUri, isLogout: false)); - return IdentityServiceResult.Success; + return Task.FromResult(IdentityServiceResult.Success); } private TRedirectUri CreateRedirectUri(TApplication app, string redirectUri, bool isLogout) @@ -249,12 +262,7 @@ namespace Microsoft.AspNetCore.Identity.Service.EntityFrameworkCore } var registeredUri = await RedirectUris - .SingleOrDefaultAsync(ru => ru.ApplicationId.Equals(app.Id) && ru.Value.Equals(redirectUri) && !ru.IsLogout); - if (registeredUri == null) - { - return IdentityServiceResult.Failed( - new IdentityServiceError { Description = "We were unable to find the redirect uri to unregister." }); - } + .SingleAsync(ru => ru.ApplicationId.Equals(app.Id) && ru.Value.Equals(redirectUri) && !ru.IsLogout); RedirectUris.Remove(registeredUri); @@ -285,13 +293,7 @@ namespace Microsoft.AspNetCore.Identity.Service.EntityFrameworkCore } var existingRedirectUri = await RedirectUris - .SingleOrDefaultAsync(ru => ru.ApplicationId.Equals(app.Id) && ru.Value.Equals(oldRedirectUri) && !ru.IsLogout); - - if (existingRedirectUri == null) - { - return IdentityServiceResult.Failed( - new IdentityServiceError { Description = "We were unable to find the registered redirect uri to update." }); - } + .SingleAsync(ru => ru.ApplicationId.Equals(app.Id) && ru.Value.Equals(oldRedirectUri) && !ru.IsLogout); existingRedirectUri.Value = newRedirectUri; @@ -315,7 +317,7 @@ namespace Microsoft.AspNetCore.Identity.Service.EntityFrameworkCore return redirectUris; } - public async Task RegisterLogoutRedirectUriAsync(TApplication app, string redirectUri, CancellationToken cancellationToken) + public Task RegisterLogoutRedirectUriAsync(TApplication app, string redirectUri, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); @@ -329,17 +331,9 @@ namespace Microsoft.AspNetCore.Identity.Service.EntityFrameworkCore throw new ArgumentNullException(nameof(redirectUri)); } - var existingRedirectUri = await RedirectUris.SingleOrDefaultAsync( - ru => ru.ApplicationId.Equals(app.Id) && ru.Value.Equals(redirectUri) && ru.IsLogout); - if (existingRedirectUri != null) - { - return IdentityServiceResult.Failed( - new IdentityServiceError { Description = "A route with the same value already exists." }); - } - RedirectUris.Add(CreateRedirectUri(app, redirectUri, isLogout: true)); - return IdentityServiceResult.Success; + return Task.FromResult(IdentityServiceResult.Success); } public async Task UnregisterLogoutRedirectUriAsync(TApplication app, string redirectUri, CancellationToken cancellationToken) @@ -357,12 +351,7 @@ namespace Microsoft.AspNetCore.Identity.Service.EntityFrameworkCore } var registeredUri = await RedirectUris - .SingleOrDefaultAsync(ru => ru.ApplicationId.Equals(app.Id) && ru.Value.Equals(redirectUri) && ru.IsLogout); - if (registeredUri == null) - { - return IdentityServiceResult.Failed( - new IdentityServiceError { Description = "We were unable to find the redirect uri to unregister." }); - } + .SingleAsync(ru => ru.ApplicationId.Equals(app.Id) && ru.Value.Equals(redirectUri) && ru.IsLogout); RedirectUris.Remove(registeredUri); @@ -389,13 +378,7 @@ namespace Microsoft.AspNetCore.Identity.Service.EntityFrameworkCore } var existingRedirectUri = await RedirectUris - .SingleOrDefaultAsync(ru => ru.ApplicationId.Equals(app.Id) && ru.Value.Equals(oldRedirectUri) && ru.IsLogout); - - if (existingRedirectUri == null) - { - return IdentityServiceResult.Failed( - new IdentityServiceError { Description = "We were unable to find the registered redirect uri to update." }); - } + .SingleAsync(ru => ru.ApplicationId.Equals(app.Id) && ru.Value.Equals(oldRedirectUri) && ru.IsLogout); existingRedirectUri.Value = newRedirectUri; @@ -513,7 +496,7 @@ namespace Microsoft.AspNetCore.Identity.Service.EntityFrameworkCore return scopes; } - public async Task AddScopeAsync(TApplication application, string scope, CancellationToken cancellationToken) + public Task AddScopeAsync(TApplication application, string scope, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); ThrowIfDisposed(); @@ -527,17 +510,9 @@ namespace Microsoft.AspNetCore.Identity.Service.EntityFrameworkCore throw new ArgumentNullException(nameof(scope)); } - var existingScope = await Scopes.SingleOrDefaultAsync( - ru => ru.ApplicationId.Equals(application.Id) && ru.Value.Equals(scope)); - if (existingScope != null) - { - return IdentityServiceResult.Failed( - new IdentityServiceError { Description = "A scope with the same value already exists." }); - } - Scopes.Add(CreateScope(application, scope)); - return IdentityServiceResult.Success; + return Task.FromResult(IdentityServiceResult.Success); } private TScope CreateScope(TApplication application, string scope) @@ -569,13 +544,7 @@ namespace Microsoft.AspNetCore.Identity.Service.EntityFrameworkCore } var existingScope = await Scopes - .SingleOrDefaultAsync(s => s.ApplicationId.Equals(application.Id) && s.Value.Equals(oldScope)); - if (existingScope == null) - { - return IdentityServiceResult.Failed( - new IdentityServiceError { Description = "We were unable to find the scope to update." }); - } - + .SingleAsync(s => s.ApplicationId.Equals(application.Id) && s.Value.Equals(oldScope)); existingScope.Value = newScope; return IdentityServiceResult.Success; @@ -596,13 +565,7 @@ namespace Microsoft.AspNetCore.Identity.Service.EntityFrameworkCore } var existingScope = await Scopes - .SingleOrDefaultAsync(ru => ru.ApplicationId.Equals(application.Id) && ru.Value.Equals(scope)); - if (existingScope == null) - { - return IdentityServiceResult.Failed( - new IdentityServiceError { Description = "We were unable to find the scope to remove." }); - } - + .SingleAsync(ru => ru.ApplicationId.Equals(application.Id) && ru.Value.Equals(scope)); Scopes.Remove(existingScope); return IdentityServiceResult.Success; diff --git a/src/Microsoft.AspNetCore.Identity.Service.EntityFrameworkCore/IdentityServiceApplication.cs b/src/Microsoft.AspNetCore.Identity.Service.EntityFrameworkCore/IdentityServiceApplication.cs index ebd572881f..9cac3d77c0 100644 --- a/src/Microsoft.AspNetCore.Identity.Service.EntityFrameworkCore/IdentityServiceApplication.cs +++ b/src/Microsoft.AspNetCore.Identity.Service.EntityFrameworkCore/IdentityServiceApplication.cs @@ -25,7 +25,6 @@ namespace Microsoft.AspNetCore.Identity.Service IdentityServiceRedirectUri> where TApplicationKey : IEquatable where TUserKey : IEquatable - { } diff --git a/src/Microsoft.AspNetCore.Identity.Service.Specification.Tests/IdentityServiceSpecificationTestBase.cs b/src/Microsoft.AspNetCore.Identity.Service.Specification.Tests/IdentityServiceSpecificationTestBase.cs index 5408415257..d2b9874ce6 100644 --- a/src/Microsoft.AspNetCore.Identity.Service.Specification.Tests/IdentityServiceSpecificationTestBase.cs +++ b/src/Microsoft.AspNetCore.Identity.Service.Specification.Tests/IdentityServiceSpecificationTestBase.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Security.Claims; using System.Threading.Tasks; using Microsoft.AspNetCore.Identity.Test; using Microsoft.Extensions.Configuration; @@ -53,6 +54,8 @@ namespace Microsoft.AspNetCore.Identity.Service.Specification.Tests new IdentityBuilder(typeof(TestUser), typeof(TestRole), services) .AddApplications(options => { }); + services.AddSingleton, ClaimValidator>(); + AddApplicationStore(services, context); services.AddLogging(); services.AddSingleton>>(new TestLogger>()); @@ -135,6 +138,65 @@ namespace Microsoft.AspNetCore.Identity.Service.Specification.Tests Assert.NotNull(await manager.FindByIdAsync(await manager.GetApplicationIdAsync(application))); } + /// + /// Test. + /// + /// Task + [Fact] + public async Task CanFindByName() + { + if (ShouldSkipDbTests()) + { + return; + } + var manager = CreateManager(); + var application = CreateTestApplication(); + IdentityServiceResultAssert.IsSuccess(await manager.CreateAsync(application)); + Assert.NotNull(await manager.FindByNameAsync(await manager.GetApplicationNameAsync(application))); + } + + /// + /// Test. + /// + /// Task + [Fact] + public async Task SetApplicationName() + { + if (ShouldSkipDbTests()) + { + return; + } + var manager = CreateManager(); + var application = CreateTestApplication(); + var name = await manager.GetApplicationNameAsync(application); + IdentityServiceResultAssert.IsSuccess(await manager.CreateAsync(application)); + var newName = Guid.NewGuid().ToString(); + Assert.Null(await manager.FindByNameAsync(newName)); + IdentityServiceResultAssert.IsSuccess(await manager.SetApplicationNameAsync(application, newName)); + Assert.Null(await manager.FindByNameAsync(name)); + Assert.NotNull(await manager.FindByNameAsync(newName)); + } + + /// + /// Test. + /// + /// Task + [Fact] + public async Task SetApplicationName_ValidatesNewName() + { + if (ShouldSkipDbTests()) + { + return; + } + var manager = CreateManager(); + var application = CreateTestApplication(); + var name = await manager.GetApplicationNameAsync(application); + IdentityServiceResultAssert.IsSuccess(await manager.CreateAsync(application)); + var newName = ""; + Assert.Null(await manager.FindByNameAsync(newName)); + IdentityServiceResultAssert.IsFailure(await manager.SetApplicationNameAsync(application, newName)); + } + /// /// Test. /// @@ -175,18 +237,18 @@ namespace Microsoft.AspNetCore.Identity.Service.Specification.Tests } var registeredUris = await manager.FindRegisteredUrisAsync(application); - foreach (var uri in redirectUris) + foreach (var uri in registeredUris) { - Assert.Contains(uri, registeredUris); + Assert.Contains(uri, redirectUris); } } /// /// Test. /// - /// Task + /// [Fact] - public async Task CanGetScopesForApplication() + public async Task RegisterRedirectUrisForApplicationValidatesUris() { if (ShouldSkipDbTests()) { @@ -195,26 +257,669 @@ namespace Microsoft.AspNetCore.Identity.Service.Specification.Tests var manager = CreateManager(); var application = CreateTestApplication(); - var scopes = GenerateScopes(nameof(CanGetScopesForApplication), 2); + var redirect = ""; IdentityServiceResultAssert.IsSuccess(await manager.CreateAsync(application)); - foreach (var redirect in scopes) + Assert.Empty(await manager.FindRegisteredUrisAsync(application)); + IdentityServiceResultAssert.IsFailure(await manager.RegisterRedirectUriAsync(application, redirect)); + Assert.Empty(await manager.FindRegisteredUrisAsync(application)); + } + + /// + /// Test. + /// + /// + [Fact] + public async Task CanUpdateRedirectUri() + { + if (ShouldSkipDbTests()) { - await manager.AddScopeAsync(application, redirect); + return; } - var applicationScopes = await manager.FindScopesAsync(application); - foreach (var scope in scopes) + var manager = CreateManager(); + var application = CreateTestApplication(); + var redirect = GenerateRedirectUris("login", 2).ToArray(); + + IdentityServiceResultAssert.IsSuccess(await manager.CreateAsync(application)); + Assert.Empty(await manager.FindRegisteredUrisAsync(application)); + IdentityServiceResultAssert.IsSuccess(await manager.RegisterRedirectUriAsync(application, redirect[0])); + Assert.Equal(redirect[0], (await manager.FindRegisteredUrisAsync(application)).First()); + IdentityServiceResultAssert.IsSuccess(await manager.UpdateRedirectUriAsync(application, redirect[0], redirect[1])); + Assert.Equal(redirect[1], (await manager.FindRegisteredUrisAsync(application)).First()); + } + + /// + /// Test. + /// + /// + [Fact] + public async Task UpdateRedirectUriValidatesRedirectUri() + { + if (ShouldSkipDbTests()) { - Assert.Contains(scope, applicationScopes); + return; + } + + var manager = CreateManager(); + var application = CreateTestApplication(); + var redirect = GenerateRedirectUris("login", 1).ToArray(); + + IdentityServiceResultAssert.IsSuccess(await manager.CreateAsync(application)); + Assert.Empty(await manager.FindRegisteredUrisAsync(application)); + IdentityServiceResultAssert.IsSuccess(await manager.RegisterRedirectUriAsync(application, redirect[0])); + Assert.Equal(redirect[0], (await manager.FindRegisteredUrisAsync(application)).First()); + IdentityServiceResultAssert.IsFailure(await manager.UpdateRedirectUriAsync(application, redirect[0], "")); + } + + /// + /// Test. + /// + /// + [Fact] + public async Task UpdateRedirectUriFailsIfItDoesNotFindTheUri() + { + if (ShouldSkipDbTests()) + { + return; + } + + var manager = CreateManager(); + var application = CreateTestApplication(); + var redirect = GenerateRedirectUris("login", 2).ToArray(); + + IdentityServiceResultAssert.IsSuccess(await manager.CreateAsync(application)); + Assert.Empty(await manager.FindRegisteredUrisAsync(application)); + IdentityServiceResultAssert.IsFailure(await manager.UpdateRedirectUriAsync(application, redirect[0], redirect[1])); + } + + /// + /// Test. + /// + /// + [Fact] + public async Task CanUnregisterRedirectUri() + { + if (ShouldSkipDbTests()) + { + return; + } + + var manager = CreateManager(); + var application = CreateTestApplication(); + var redirect = GenerateRedirectUris("login", 1).Single(); + + IdentityServiceResultAssert.IsSuccess(await manager.CreateAsync(application)); + Assert.Empty(await manager.FindRegisteredUrisAsync(application)); + IdentityServiceResultAssert.IsSuccess(await manager.RegisterRedirectUriAsync(application, redirect)); + Assert.Equal(redirect, (await manager.FindRegisteredUrisAsync(application)).Single()); + IdentityServiceResultAssert.IsSuccess(await manager.UnregisterRedirectUriAsync(application, redirect)); + Assert.Empty(await manager.FindRegisteredUrisAsync(application)); + } + + /// + /// Test. + /// + /// + [Fact] + public async Task UnregisterRedirectUriFailsIfItDoesNotFindTheUri() + { + if (ShouldSkipDbTests()) + { + return; + } + + var manager = CreateManager(); + var application = CreateTestApplication(); + var redirect = GenerateRedirectUris("login", 1).ToArray(); + + IdentityServiceResultAssert.IsSuccess(await manager.CreateAsync(application)); + Assert.Empty(await manager.FindRegisteredUrisAsync(application)); + IdentityServiceResultAssert.IsFailure(await manager.UnregisterRedirectUriAsync(application, redirect[0])); + } + + /// + /// Test. + /// + /// Task + [Fact] + public async Task CanGetLogoutUrisForApplication() + { + if (ShouldSkipDbTests()) + { + return; + } + + var manager = CreateManager(); + var application = CreateTestApplication(); + var logoutUris = GenerateRedirectUris(nameof(CanGetLogoutUrisForApplication), 2); + + IdentityServiceResultAssert.IsSuccess(await manager.CreateAsync(application)); + foreach (var logout in logoutUris) + { + await manager.RegisterLogoutUriAsync(application, logout); + } + + var registeredLogoutUris = await manager.FindRegisteredLogoutUrisAsync(application); + foreach (var uri in registeredLogoutUris) + { + Assert.Contains(uri, logoutUris); } } + /// + /// Test. + /// + /// + [Fact] + public async Task RegisterLogoutUrisForApplicationValidatesUris() + { + if (ShouldSkipDbTests()) + { + return; + } + + var manager = CreateManager(); + var application = CreateTestApplication(); + var redirect = ""; + + IdentityServiceResultAssert.IsSuccess(await manager.CreateAsync(application)); + Assert.Empty(await manager.FindRegisteredLogoutUrisAsync(application)); + IdentityServiceResultAssert.IsFailure(await manager.RegisterLogoutUriAsync(application, redirect)); + Assert.Empty(await manager.FindRegisteredLogoutUrisAsync(application)); + } + + /// + /// Test. + /// + /// + [Fact] + public async Task CanUpdateLogoutUri() + { + if (ShouldSkipDbTests()) + { + return; + } + + var manager = CreateManager(); + var application = CreateTestApplication(); + var redirect = GenerateRedirectUris("logout", 2).ToArray(); + + IdentityServiceResultAssert.IsSuccess(await manager.CreateAsync(application)); + Assert.Empty(await manager.FindRegisteredLogoutUrisAsync(application)); + IdentityServiceResultAssert.IsSuccess(await manager.RegisterLogoutUriAsync(application, redirect[0])); + Assert.Equal(redirect[0], (await manager.FindRegisteredLogoutUrisAsync(application)).First()); + IdentityServiceResultAssert.IsSuccess(await manager.UpdateLogoutUriAsync(application, redirect[0], redirect[1])); + Assert.Equal(redirect[1], (await manager.FindRegisteredLogoutUrisAsync(application)).First()); + } + + /// + /// Test. + /// + /// + [Fact] + public async Task UpdateLogoutUriValidatesRedirectUri() + { + if (ShouldSkipDbTests()) + { + return; + } + + var manager = CreateManager(); + var application = CreateTestApplication(); + var redirect = GenerateRedirectUris("logout", 1).ToArray(); + + IdentityServiceResultAssert.IsSuccess(await manager.CreateAsync(application)); + Assert.Empty(await manager.FindRegisteredLogoutUrisAsync(application)); + IdentityServiceResultAssert.IsSuccess(await manager.RegisterLogoutUriAsync(application, redirect[0])); + Assert.Equal(redirect[0], (await manager.FindRegisteredLogoutUrisAsync(application)).First()); + IdentityServiceResultAssert.IsFailure(await manager.UpdateLogoutUriAsync(application, redirect[0], "")); + } + + /// + /// Test. + /// + /// + [Fact] + public async Task UpdateLogoutUriFailsIfItDoesNotFindTheUri() + { + if (ShouldSkipDbTests()) + { + return; + } + + var manager = CreateManager(); + var application = CreateTestApplication(); + var redirect = GenerateRedirectUris("logout", 2).ToArray(); + + IdentityServiceResultAssert.IsSuccess(await manager.CreateAsync(application)); + Assert.Empty(await manager.FindRegisteredLogoutUrisAsync(application)); + IdentityServiceResultAssert.IsFailure(await manager.UpdateLogoutUriAsync(application, redirect[0], redirect[1])); + } + + /// + /// Test. + /// + /// + [Fact] + public async Task CanUnregisterLogoutUri() + { + if (ShouldSkipDbTests()) + { + return; + } + + var manager = CreateManager(); + var application = CreateTestApplication(); + var redirect = GenerateRedirectUris("logout", 1).Single(); + + IdentityServiceResultAssert.IsSuccess(await manager.CreateAsync(application)); + Assert.Empty(await manager.FindRegisteredLogoutUrisAsync(application)); + IdentityServiceResultAssert.IsSuccess(await manager.RegisterLogoutUriAsync(application, redirect)); + Assert.Equal(redirect, (await manager.FindRegisteredLogoutUrisAsync(application)).Single()); + IdentityServiceResultAssert.IsSuccess(await manager.UnregisterLogoutUriAsync(application, redirect)); + Assert.Empty(await manager.FindRegisteredLogoutUrisAsync(application)); + } + + /// + /// Test. + /// + /// + [Fact] + public async Task UnregisterLogoutUriFailsIfItDoesNotFindTheUri() + { + if (ShouldSkipDbTests()) + { + return; + } + + var manager = CreateManager(); + var application = CreateTestApplication(); + var redirect = GenerateRedirectUris("logout", 1).ToArray(); + + IdentityServiceResultAssert.IsSuccess(await manager.CreateAsync(application)); + Assert.Empty(await manager.FindRegisteredLogoutUrisAsync(application)); + IdentityServiceResultAssert.IsFailure(await manager.UnregisterLogoutUriAsync(application, redirect[0])); + } + + /// + /// Test. + /// + /// Task + [Fact] + public async Task CanGetScopes() + { + if (ShouldSkipDbTests()) + { + return; + } + + var manager = CreateManager(); + var application = CreateTestApplication(); + var scopes = GenerateScopes(nameof(CanGetScopes), 2); + + IdentityServiceResultAssert.IsSuccess(await manager.CreateAsync(application)); + foreach (var scope in scopes) + { + await manager.AddScopeAsync(application, scope); + } + + var applicationScopes = await manager.FindScopesAsync(application); + foreach (var scope in applicationScopes) + { + Assert.Contains(scope, scopes); + } + } + + /// + /// Test. + /// + /// + [Fact] + public async Task CanAddScopes() + { + if (ShouldSkipDbTests()) + { + return; + } + + var manager = CreateManager(); + var application = CreateTestApplication(); + var scope = "offline_access"; + + IdentityServiceResultAssert.IsSuccess(await manager.CreateAsync(application)); + Assert.Empty(await manager.FindScopesAsync(application)); + IdentityServiceResultAssert.IsSuccess(await manager.AddScopeAsync(application, scope)); + Assert.NotEmpty(await manager.FindScopesAsync(application)); + } + + /// + /// Test. + /// + /// + [Fact] + public async Task AddScopesValidatesScopes() + { + if (ShouldSkipDbTests()) + { + return; + } + + var manager = CreateManager(); + var application = CreateTestApplication(); + var scope = ""; + + IdentityServiceResultAssert.IsSuccess(await manager.CreateAsync(application)); + Assert.Empty(await manager.FindScopesAsync(application)); + IdentityServiceResultAssert.IsFailure(await manager.AddScopeAsync(application, scope)); + Assert.Empty(await manager.FindScopesAsync(application)); + } + + /// + /// Test. + /// + /// + [Fact] + public async Task CanUpdateScopes() + { + if (ShouldSkipDbTests()) + { + return; + } + + var manager = CreateManager(); + var application = CreateTestApplication(); + var scopes = GenerateScopes("UpdateScopes", 2).ToArray(); + + IdentityServiceResultAssert.IsSuccess(await manager.CreateAsync(application)); + Assert.Empty(await manager.FindScopesAsync(application)); + IdentityServiceResultAssert.IsSuccess(await manager.AddScopeAsync(application, scopes[0])); + Assert.Equal(scopes[0], (await manager.FindScopesAsync(application)).First()); + IdentityServiceResultAssert.IsSuccess(await manager.UpdateScopeAsync(application, scopes[0], scopes[1])); + Assert.Equal(scopes[1], (await manager.FindScopesAsync(application)).First()); + } + + /// + /// Test. + /// + /// + [Fact] + public async Task UpdateScopeValidatesScope() + { + if (ShouldSkipDbTests()) + { + return; + } + + var manager = CreateManager(); + var application = CreateTestApplication(); + var scopes = GenerateScopes("ValidateScope", 1).ToArray(); + + IdentityServiceResultAssert.IsSuccess(await manager.CreateAsync(application)); + Assert.Empty(await manager.FindScopesAsync(application)); + IdentityServiceResultAssert.IsSuccess(await manager.AddScopeAsync(application, scopes[0])); + Assert.Equal(scopes[0], (await manager.FindScopesAsync(application)).First()); + IdentityServiceResultAssert.IsFailure(await manager.UpdateScopeAsync(application, scopes[0], "")); + } + + /// + /// Test. + /// + /// + [Fact] + public async Task UpdateScopeFailsIfItDoesNotFindTheScope() + { + if (ShouldSkipDbTests()) + { + return; + } + + var manager = CreateManager(); + var application = CreateTestApplication(); + var scope = GenerateScopes("UpdateScopeNoScope", 2).ToArray(); + + IdentityServiceResultAssert.IsSuccess(await manager.CreateAsync(application)); + Assert.Empty(await manager.FindScopesAsync(application)); + IdentityServiceResultAssert.IsFailure(await manager.UpdateScopeAsync(application, scope[0], scope[1])); + } + + /// + /// Test. + /// + /// + [Fact] + public async Task CanRemoveScope() + { + if (ShouldSkipDbTests()) + { + return; + } + + var manager = CreateManager(); + var application = CreateTestApplication(); + var scope = GenerateScopes(nameof(CanRemoveScope), 1).Single(); + + IdentityServiceResultAssert.IsSuccess(await manager.CreateAsync(application)); + Assert.Empty(await manager.FindScopesAsync(application)); + IdentityServiceResultAssert.IsSuccess(await manager.AddScopeAsync(application, scope)); + Assert.Equal(scope, (await manager.FindScopesAsync(application)).Single()); + IdentityServiceResultAssert.IsSuccess(await manager.RemoveScopeAsync(application, scope)); + Assert.Empty(await manager.FindScopesAsync(application)); + } + + /// + /// Test. + /// + /// + [Fact] + public async Task RemoveScopeFailsIfItDoesNotFindTheScope() + { + if (ShouldSkipDbTests()) + { + return; + } + + var manager = CreateManager(); + var application = CreateTestApplication(); + var scope = GenerateScopes("RemoveScopeValidates", 1).ToArray(); + + IdentityServiceResultAssert.IsSuccess(await manager.CreateAsync(application)); + Assert.Empty(await manager.FindScopesAsync(application)); + IdentityServiceResultAssert.IsFailure(await manager.RemoveScopeAsync(application, scope[0])); + } + + /// + /// Test. + /// + /// Task + [Fact] + public async Task CanGetClaimsForApplication() + { + if (ShouldSkipDbTests()) + { + return; + } + + var manager = CreateManager(); + var application = CreateTestApplication(); + var claims = GenerateClaims(nameof(CanGetClaimsForApplication), 2); + + IdentityServiceResultAssert.IsSuccess(await manager.CreateAsync(application)); + foreach (var claim in claims) + { + await manager.AddClaimAsync(application, claim); + } + + var applicationClaims = await manager.GetClaimsAsync(application); + foreach (var claim in applicationClaims) + { + Assert.Contains(claim, claims, ClaimComparer.Instance); + } + } + + /// + /// Test. + /// + /// + [Fact] + public async Task CanAddClaims() + { + if (ShouldSkipDbTests()) + { + return; + } + + var manager = CreateManager(); + var application = CreateTestApplication(); + var claim = new Claim("type", "value"); + + IdentityServiceResultAssert.IsSuccess(await manager.CreateAsync(application)); + Assert.Empty(await manager.GetClaimsAsync(application)); + IdentityServiceResultAssert.IsSuccess(await manager.AddClaimAsync(application, claim)); + Assert.NotEmpty(await manager.GetClaimsAsync(application)); + } + + /// + /// Test. + /// + /// + [Fact] + public async Task AddClaimsValidatesClaims() + { + if (ShouldSkipDbTests()) + { + return; + } + + var manager = CreateManager(); + var application = CreateTestApplication(); + var scope = new Claim("fail", "fail"); + + IdentityServiceResultAssert.IsSuccess(await manager.CreateAsync(application)); + Assert.Empty(await manager.GetClaimsAsync(application)); + IdentityServiceResultAssert.IsFailure(await manager.AddClaimAsync(application, scope)); + Assert.Empty(await manager.GetClaimsAsync(application)); + } + + /// + /// Test. + /// + /// + [Fact] + public async Task CanUpdateClaims() + { + if (ShouldSkipDbTests()) + { + return; + } + + var manager = CreateManager(); + var application = CreateTestApplication(); + var claims = GenerateClaims("UpdateClaims", 2).ToArray(); + + IdentityServiceResultAssert.IsSuccess(await manager.CreateAsync(application)); + Assert.Empty(await manager.GetClaimsAsync(application)); + IdentityServiceResultAssert.IsSuccess(await manager.AddClaimAsync(application, claims[0])); + Assert.Equal(claims[0], (await manager.GetClaimsAsync(application)).First(), ClaimComparer.Instance); + IdentityServiceResultAssert.IsSuccess(await manager.ReplaceClaimAsync(application, claims[0], claims[1])); + Assert.Equal(claims[1], (await manager.GetClaimsAsync(application)).First(), ClaimComparer.Instance); + } + + /// + /// Test. + /// + /// + [Fact] + public async Task ReplaceClaimValidatesClaim() + { + if (ShouldSkipDbTests()) + { + return; + } + + var manager = CreateManager(); + var application = CreateTestApplication(); + var claims = GenerateClaims("ValidateClaim", 1).ToArray(); + + IdentityServiceResultAssert.IsSuccess(await manager.CreateAsync(application)); + Assert.Empty(await manager.GetClaimsAsync(application)); + IdentityServiceResultAssert.IsSuccess(await manager.AddClaimAsync(application, claims[0])); + Assert.Equal(claims[0], (await manager.GetClaimsAsync(application)).First(), ClaimComparer.Instance); + IdentityServiceResultAssert.IsFailure(await manager.ReplaceClaimAsync(application, claims[0], new Claim("fail", "fail"))); + } + + /// + /// Test. + /// + /// + [Fact] + public async Task CanRemoveClaim() + { + if (ShouldSkipDbTests()) + { + return; + } + + var manager = CreateManager(); + var application = CreateTestApplication(); + var claim = GenerateClaims(nameof(CanRemoveClaim), 1).Single(); + + IdentityServiceResultAssert.IsSuccess(await manager.CreateAsync(application)); + Assert.Empty(await manager.GetClaimsAsync(application)); + IdentityServiceResultAssert.IsSuccess(await manager.AddClaimAsync(application, claim)); + Assert.Equal(claim, (await manager.GetClaimsAsync(application)).Single(),ClaimComparer.Instance); + IdentityServiceResultAssert.IsSuccess(await manager.RemoveClaimAsync(application, claim)); + Assert.Empty(await manager.GetClaimsAsync(application)); + } + private IEnumerable GenerateRedirectUris(string prefix, int count) => - Enumerable.Range(0, count).Select(i => $"https://www.example.com/{prefix}/{count}"); + Enumerable.Range(0, count).Select(i => $"https://www.example.com/{prefix}/{i}"); private IEnumerable GenerateScopes(string prefix, int count) => - Enumerable.Range(0, count).Select(i => $"{prefix}_{count}"); + Enumerable.Range(0, count).Select(i => $"{prefix}_{i}"); + + private IEnumerable GenerateClaims(string prefix, int count) => + Enumerable.Range(0, count).Select(i => new Claim($"{prefix}_type_{i}", $"{prefix}_value_{i}")); + + private class ClaimComparer : IEqualityComparer + { + public static readonly ClaimComparer Instance = new ClaimComparer(); + + public bool Equals(Claim x, Claim y) => x?.Type == y?.Type && x?.Value == y?.Value; + + public int GetHashCode(Claim obj) + { + throw new NotImplementedException(); + } + } + + private class ClaimValidator : IApplicationValidator + { + public Task ValidateAsync(ApplicationManager manager, TApplication application) + { + return Task.FromResult(IdentityServiceResult.Success); + } + + public Task ValidateClaimAsync(ApplicationManager manager, TApplication application, Claim claim) + { + return Task.FromResult(claim.Type.Equals("fail") ? IdentityServiceResult.Failed(new IdentityServiceError()) : IdentityServiceResult.Success); + } + + public Task ValidateLogoutUriAsync(ApplicationManager manager, TApplication application, string logoutUri) + { + return Task.FromResult(IdentityServiceResult.Success); + } + + public Task ValidateRedirectUriAsync(ApplicationManager manager, TApplication application, string redirectUri) + { + return Task.FromResult(IdentityServiceResult.Success); + } + + public Task ValidateScopeAsync(ApplicationManager manager, TApplication application, string scope) + { + return Task.FromResult(IdentityServiceResult.Success); + } + } private class TestUser { } private class TestRole { } diff --git a/src/Microsoft.AspNetCore.Identity.Service/ApplicationErrorDescriber.cs b/src/Microsoft.AspNetCore.Identity.Service/ApplicationErrorDescriber.cs new file mode 100644 index 0000000000..9a7ec1626e --- /dev/null +++ b/src/Microsoft.AspNetCore.Identity.Service/ApplicationErrorDescriber.cs @@ -0,0 +1,111 @@ +// 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; + +namespace Microsoft.AspNetCore.Identity.Service +{ + public class ApplicationErrorDescriber + { + public virtual IdentityServiceError InvalidApplicationName(string applicationName) => new IdentityServiceError + { + Code = nameof(InvalidApplicationName), + Description = $"The application name '{applicationName}' is not valid." + }; + + public virtual IdentityServiceError DuplicateApplicationName(string applicationName) => new IdentityServiceError + { + Code = nameof(DuplicateApplicationName), + Description = $"An application with name '{applicationName}' already exists." + }; + + public virtual IdentityServiceError InvalidApplicationClientId(string clientId) => new IdentityServiceError + { + Code = nameof(InvalidApplicationClientId), + Description = $"The application client ID '{clientId}' is not valid." + }; + + public virtual IdentityServiceError DuplicateApplicationClientId(string clientId) => new IdentityServiceError + { + Code = nameof(DuplicateApplicationClientId), + Description = $"An application with client ID '{clientId}' already exists." + }; + + public virtual IdentityServiceError DuplicateLogoutUri(string logoutUri) => new IdentityServiceError + { + Code = nameof(DuplicateLogoutUri), + Description = $"The application already contains a logout uri '{logoutUri}'." + }; + + public virtual IdentityServiceError InvalidLogoutUri(string logoutUri) => new IdentityServiceError + { + Code = nameof(InvalidLogoutUri), + Description = $"The logout uri '{logoutUri}' is not valid." + }; + + public virtual IdentityServiceError NoHttpsUri(string logoutUri) => new IdentityServiceError + { + Code = nameof(NoHttpsUri), + Description = $"The uri '{logoutUri}' must use https." + }; + + public virtual IdentityServiceError DifferentDomains() => new IdentityServiceError + { + Code = nameof(DifferentDomains), + Description = $"All the URIs in an application must have the same domain." + }; + + public virtual IdentityServiceError DuplicateRedirectUri(string redirectUri) => new IdentityServiceError + { + Code = nameof(DuplicateRedirectUri), + Description = $"The application already contains a redirect uri '{redirectUri}'." + }; + + public virtual IdentityServiceError InvalidRedirectUri(string redirectUri) => new IdentityServiceError + { + Code = nameof(InvalidRedirectUri), + Description = $"The redirect URI '{redirectUri}' is not valid." + }; + + public virtual IdentityServiceError InvalidScope(string scope) => new IdentityServiceError + { + Code = nameof(InvalidScope), + Description = $"The scope '{scope}' is not valid." + }; + + public virtual IdentityServiceError DuplicateScope(string scope) => new IdentityServiceError + { + Code = nameof(DuplicateScope), + Description = $"The application already contains a scope '{scope}'." + }; + + public virtual IdentityServiceError ApplicationAlreadyHasClientSecret() => new IdentityServiceError { + Code = nameof(ApplicationAlreadyHasClientSecret), + Description = $"The application already has a client secret." + }; + + public virtual IdentityServiceError RedirectUriNotFound(string redirectUri) => new IdentityServiceError + { + Code = nameof(RedirectUriNotFound), + Description = $"The redirect uri '{redirectUri}' can not be found." + }; + + public virtual IdentityServiceError LogoutUriNotFound(string logoutUri) => new IdentityServiceError + { + Code = nameof(LogoutUriNotFound), + Description = $"The logout uri '{logoutUri}' can not be found." + }; + + public virtual IdentityServiceError ConcurrencyFailure() => new IdentityServiceError + { + Code = nameof(ConcurrencyFailure), + Description = $"Optimistic concurrency failure, object has been modified." + }; + + public virtual IdentityServiceError ScopeNotFound(string scope) => new IdentityServiceError + { + Code = nameof(ScopeNotFound), + Description = $"The scope '{scope}' can not be found." + }; + } +} diff --git a/src/Microsoft.AspNetCore.Identity.Service/ApplicationManager.cs b/src/Microsoft.AspNetCore.Identity.Service/ApplicationManager.cs index ca9ce8d12d..622f921cb0 100644 --- a/src/Microsoft.AspNetCore.Identity.Service/ApplicationManager.cs +++ b/src/Microsoft.AspNetCore.Identity.Service/ApplicationManager.cs @@ -8,6 +8,7 @@ using System.Security.Claims; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; namespace Microsoft.AspNetCore.Identity.Service { @@ -16,20 +17,26 @@ namespace Microsoft.AspNetCore.Identity.Service private bool _disposed; public ApplicationManager( + IOptions options, IApplicationStore store, IPasswordHasher passwordHasher, IEnumerable> applicationValidators, - ILogger> logger) + ILogger> logger, + ApplicationErrorDescriber errorDescriber) { + Options = options.Value; Store = store; PasswordHasher = passwordHasher; ApplicationValidators = applicationValidators; + ErrorDescriber = errorDescriber; Logger = Logger; } + public ApplicationOptions Options { get; } public IApplicationStore Store { get; set; } public IPasswordHasher PasswordHasher { get; set; } public IEnumerable> ApplicationValidators { get; set; } + public ApplicationErrorDescriber ErrorDescriber { get; } public ILogger> Logger { get; set; } public CancellationToken CancellationToken { get; set; } @@ -60,16 +67,51 @@ namespace Microsoft.AspNetCore.Identity.Service return Store.FindByIdAsync(applicationId, CancellationToken); } + public Task GetApplicationIdAsync(TApplication application) + { + ThrowIfDisposed(); + return Store.GetApplicationIdAsync(application, CancellationToken); + } + public Task FindByClientIdAsync(string clientId) { return Store.FindByClientIdAsync(clientId, CancellationToken.None); } + public Task GetApplicationClientIdAsync(TApplication application) + { + ThrowIfDisposed(); + return Store.GetApplicationClientIdAsync(application, CancellationToken); + } + public Task FindByNameAsync(string name) { return Store.FindByNameAsync(name, CancellationToken.None); } + public Task GetApplicationNameAsync(TApplication application) + { + ThrowIfDisposed(); + if (application == null) + { + throw new ArgumentNullException(nameof(application)); + } + + return Store.GetApplicationNameAsync(application, CancellationToken); + } + + public async Task SetApplicationNameAsync(TApplication application, string name) + { + ThrowIfDisposed(); + if (name == null) + { + throw new ArgumentNullException(nameof(name)); + } + + await Store.SetApplicationNameAsync(application, name, CancellationToken); + return await UpdateAsync(application); + } + public virtual async Task CreateAsync(TApplication application) { ThrowIfDisposed(); @@ -87,17 +129,6 @@ namespace Microsoft.AspNetCore.Identity.Service return await Store.CreateAsync(application, CancellationToken); } - public Task GetApplicationNameAsync(TApplication application) - { - ThrowIfDisposed(); - if (application == null) - { - throw new ArgumentNullException(nameof(application)); - } - - return Store.GetApplicationNameAsync(application, CancellationToken); - } - public virtual async Task DeleteAsync(TApplication application) { ThrowIfDisposed(); @@ -126,48 +157,23 @@ namespace Microsoft.AspNetCore.Identity.Service return await Store.UpdateAsync(application, CancellationToken); } - private async Task ValidateApplicationAsync(TApplication application) - { - var errors = new List(); - foreach (var v in ApplicationValidators) - { - var result = await v.ValidateAsync(this, application); - if (!result.Succeeded) - { - errors.AddRange(result.Errors); - } - } - - if (errors.Count > 0) - { - return IdentityServiceResult.Failed(errors.ToArray()); - } - - return IdentityServiceResult.Success; - } - - public Task GetApplicationIdAsync(TApplication application) - { - ThrowIfDisposed(); - return Store.GetApplicationIdAsync(application, CancellationToken); - } - - public Task GetApplicationClientIdAsync(TApplication application) - { - ThrowIfDisposed(); - return Store.GetApplicationClientIdAsync(application, CancellationToken); - } - - public void Dispose() - { - _disposed = true; - } - public Task GenerateClientSecretAsync() { return Task.FromResult(CryptographyHelpers.GenerateHighEntropyValue(byteLength: 32)); } + public Task HasClientSecretAsync(TApplication application) + { + ThrowIfDisposed(); + if (application == null) + { + throw new ArgumentNullException(nameof(application)); + } + + var store = GetClientSecretStore(); + return store.HasClientSecretAsync(application, CancellationToken); + } + public async Task AddClientSecretAsync(TApplication application, string clientSecret) { ThrowIfDisposed(); @@ -180,9 +186,9 @@ namespace Microsoft.AspNetCore.Identity.Service var hash = await store.GetClientSecretHashAsync(application, CancellationToken); if (hash != null) { - Logger.LogWarning(1, "User {clientId} already has a password.", await GetApplicationClientIdAsync(application)); - return IdentityServiceResult.Failed(); + return IdentityServiceResult.Failed(ErrorDescriber.ApplicationAlreadyHasClientSecret()); } + var result = await UpdateClientSecretHashAsync(store, application, clientSecret); if (!result.Succeeded) { @@ -210,54 +216,6 @@ namespace Microsoft.AspNetCore.Identity.Service return await UpdateAsync(application); } - public async Task RegisterLogoutUriAsync(TApplication application, string logoutUri) - { - ThrowIfDisposed(); - var redirectStore = GetRedirectUriStore(); - var result = await redirectStore.RegisterLogoutRedirectUriAsync(application, logoutUri, CancellationToken); - if (!result.Succeeded) - { - return result; - } - - return await redirectStore.UpdateAsync(application, CancellationToken); - } - - public async Task UnregisterLogoutUriAsync(TApplication application, string logoutUri) - { - ThrowIfDisposed(); - if (application == null) - { - throw new ArgumentNullException(nameof(application)); - } - - if (logoutUri == null) - { - throw new ArgumentNullException(nameof(logoutUri)); - } - - var redirectStore = GetRedirectUriStore(); - var result = await redirectStore.UnregisterLogoutRedirectUriAsync(application, logoutUri, CancellationToken); - if (!result.Succeeded) - { - return result; - } - - return await redirectStore.UpdateAsync(application, CancellationToken); - } - - public async Task SetApplicationNameAsync(TApplication application, string name) - { - ThrowIfDisposed(); - if (name == null) - { - throw new ArgumentNullException(nameof(name)); - } - - await Store.SetApplicationNameAsync(application, name, CancellationToken); - return await UpdateAsync(application); - } - public async Task RemoveClientSecretAsync(TApplication application) { ThrowIfDisposed(); @@ -276,88 +234,6 @@ namespace Microsoft.AspNetCore.Identity.Service return await UpdateAsync(application); } - private IRedirectUriStore GetRedirectUriStore() - { - if (Store is IRedirectUriStore cast) - { - return cast; - } - - throw new NotSupportedException(); - } - - public async Task RegisterRedirectUriAsync(TApplication application, string redirectUri) - { - ThrowIfDisposed(); - var redirectStore = GetRedirectUriStore(); - var result = await redirectStore.RegisterRedirectUriAsync(application, redirectUri, CancellationToken); - if (!result.Succeeded) - { - return result; - } - - return await redirectStore.UpdateAsync(application, CancellationToken); - } - - public Task> FindRegisteredUrisAsync(TApplication application) - { - var redirectStore = GetRedirectUriStore(); - return redirectStore.FindRegisteredUrisAsync(application, CancellationToken); - } - - public Task> FindRegisteredLogoutUrisAsync(TApplication application) - { - var redirectStore = GetRedirectUriStore(); - return redirectStore.FindRegisteredLogoutUrisAsync(application, CancellationToken); - } - - public async Task UnregisterRedirectUriAsync(TApplication application, string redirectUri) - { - ThrowIfDisposed(); - if (application == null) - { - throw new ArgumentNullException(nameof(application)); - } - - if (redirectUri == null) - { - throw new ArgumentNullException(nameof(redirectUri)); - } - - var redirectStore = GetRedirectUriStore(); - var result = await redirectStore.UnregisterRedirectUriAsync(application, redirectUri, CancellationToken); - if (!result.Succeeded) - { - return result; - } - - return await redirectStore.UpdateAsync(application, CancellationToken); - } - - public Task HasClientSecretAsync(TApplication application) - { - ThrowIfDisposed(); - if (application == null) - { - throw new ArgumentNullException(nameof(application)); - } - - var store = GetClientSecretStore(); - return store.HasClientSecretAsync(application, CancellationToken); - } - - public async Task UpdateRedirectUriAsync(TApplication application, string oldRedirectUri, string newRedirectUri) - { - var redirectStore = GetRedirectUriStore(); - var result = await redirectStore.UpdateRedirectUriAsync(application, oldRedirectUri, newRedirectUri, CancellationToken); - if (!result.Succeeded) - { - return result; - } - - return await redirectStore.UpdateAsync(application, CancellationToken); - } - public async Task ValidateClientCredentialsAsync(string clientId, string clientSecret) { var application = await FindByClientIdAsync(clientId); @@ -413,14 +289,248 @@ namespace Microsoft.AspNetCore.Identity.Service return PasswordHasher.VerifyHashedPassword(application, hash, clientSecret); } - private IApplicationClientSecretStore GetClientSecretStore() + public Task> FindRegisteredUrisAsync(TApplication application) { - if (Store is IApplicationClientSecretStore cast) + var redirectStore = GetRedirectUriStore(); + return redirectStore.FindRegisteredUrisAsync(application, CancellationToken); + } + + private async Task FindRegisteredUriAsync(TApplication application, string redirectUri) + { + var uris = await FindRegisteredUrisAsync(application); + foreach (var uri in uris) { - return cast; + if (string.Equals(uri, redirectUri, StringComparison.Ordinal)) + { + return redirectUri; + } } - throw new NotSupportedException(); + return null; + } + + public async Task RegisterRedirectUriAsync(TApplication application, string redirectUri) + { + ThrowIfDisposed(); + var redirectStore = GetRedirectUriStore(); + + var validation = await ValidateRedirectUriAsync(application, redirectUri); + if (!validation.Succeeded) + { + return validation; + } + + var result = await redirectStore.RegisterRedirectUriAsync(application, redirectUri, CancellationToken); + if (!result.Succeeded) + { + return result; + } + + return await redirectStore.UpdateAsync(application, CancellationToken); + } + + public async Task UpdateRedirectUriAsync(TApplication application, string oldRedirectUri, string newRedirectUri) + { + var redirectStore = GetRedirectUriStore(); + + var registeredUri = await FindRegisteredUriAsync(application, oldRedirectUri); + if (registeredUri == null) + { + return IdentityServiceResult.Failed(ErrorDescriber.RedirectUriNotFound(oldRedirectUri)); + } + + var validation = await ValidateRedirectUriAsync(application, newRedirectUri); + if (!validation.Succeeded) + { + return validation; + } + + var result = await redirectStore.UpdateRedirectUriAsync(application, oldRedirectUri, newRedirectUri, CancellationToken); + if (!result.Succeeded) + { + return result; + } + + return await redirectStore.UpdateAsync(application, CancellationToken); + } + + public async Task UnregisterRedirectUriAsync(TApplication application, string redirectUri) + { + ThrowIfDisposed(); + if (application == null) + { + throw new ArgumentNullException(nameof(application)); + } + + if (redirectUri == null) + { + throw new ArgumentNullException(nameof(redirectUri)); + } + + var registeredUri = await FindRegisteredUriAsync(application, redirectUri); + if (registeredUri == null) + { + return IdentityServiceResult.Failed(ErrorDescriber.RedirectUriNotFound(redirectUri)); + } + + var redirectStore = GetRedirectUriStore(); + var result = await redirectStore.UnregisterRedirectUriAsync(application, redirectUri, CancellationToken); + if (!result.Succeeded) + { + return result; + } + + return await redirectStore.UpdateAsync(application, CancellationToken); + } + + private async Task ValidateRedirectUriAsync(TApplication application, string redirectUri) + { + var errors = new List(); + foreach (var v in ApplicationValidators) + { + var result = await v.ValidateRedirectUriAsync(this, application, redirectUri); + if (!result.Succeeded) + { + errors.AddRange(result.Errors); + } + } + + if (errors.Count > 0) + { + return IdentityServiceResult.Failed(errors.ToArray()); + } + + return IdentityServiceResult.Success; + } + + public Task> FindRegisteredLogoutUrisAsync(TApplication application) + { + var redirectStore = GetRedirectUriStore(); + return redirectStore.FindRegisteredLogoutUrisAsync(application, CancellationToken); + } + + private async Task FindRegisteredLogoutUriAsync(TApplication application, string redirectUri) + { + var uris = await FindRegisteredLogoutUrisAsync(application); + foreach (var uri in uris) + { + if (string.Equals(uri, redirectUri, StringComparison.Ordinal)) + { + return redirectUri; + } + } + + return null; + } + + public async Task RegisterLogoutUriAsync(TApplication application, string logoutUri) + { + ThrowIfDisposed(); + var redirectStore = GetRedirectUriStore(); + + var validation = await ValidateLogoutUriAsync(application, logoutUri); + if (!validation.Succeeded) + { + return validation; + } + + var result = await redirectStore.RegisterLogoutRedirectUriAsync(application, logoutUri, CancellationToken); + if (!result.Succeeded) + { + return result; + } + + return await redirectStore.UpdateAsync(application, CancellationToken); + } + + public async Task UpdateLogoutUriAsync(TApplication application, string oldLogoutUri, string newLogoutUri) + { + ThrowIfDisposed(); + if (application == null) + { + throw new ArgumentNullException(nameof(application)); + } + + if (oldLogoutUri == null) + { + throw new ArgumentNullException(nameof(oldLogoutUri)); + } + + if (newLogoutUri == null) + { + throw new ArgumentNullException(nameof(newLogoutUri)); + } + + var redirectUriStore = GetRedirectUriStore(); + + var registeredUri = await FindRegisteredLogoutUriAsync(application, oldLogoutUri); + if (registeredUri == null) + { + return IdentityServiceResult.Failed(ErrorDescriber.LogoutUriNotFound(oldLogoutUri)); + } + + var validation = await ValidateLogoutUriAsync(application, newLogoutUri); + if (!validation.Succeeded) + { + return validation; + } + + var result = await redirectUriStore.UpdateLogoutRedirectUriAsync(application, oldLogoutUri, newLogoutUri, CancellationToken); + if (!result.Succeeded) + { + return result; + } + + return await UpdateAsync(application); + } + + public async Task UnregisterLogoutUriAsync(TApplication application, string logoutUri) + { + ThrowIfDisposed(); + if (application == null) + { + throw new ArgumentNullException(nameof(application)); + } + + if (logoutUri == null) + { + throw new ArgumentNullException(nameof(logoutUri)); + } + + var redirectStore = GetRedirectUriStore(); + var registeredUri = await FindRegisteredLogoutUriAsync(application, logoutUri); + if (registeredUri == null) + { + return IdentityServiceResult.Failed(ErrorDescriber.LogoutUriNotFound(logoutUri)); + } + + var result = await redirectStore.UnregisterLogoutRedirectUriAsync(application, logoutUri, CancellationToken); + if (!result.Succeeded) + { + return result; + } + + return await redirectStore.UpdateAsync(application, CancellationToken); + } + + private async Task ValidateLogoutUriAsync(TApplication application, string redirectUri) + { + var errors = new List(); + foreach (var v in ApplicationValidators) + { + var result = await v.ValidateLogoutUriAsync(this, application, redirectUri); + if (!result.Succeeded) + { + errors.AddRange(result.Errors); + } + } + + if (errors.Count > 0) + { + return IdentityServiceResult.Failed(errors.ToArray()); + } + + return IdentityServiceResult.Success; } public Task> FindScopesAsync(TApplication application) @@ -429,22 +539,31 @@ namespace Microsoft.AspNetCore.Identity.Service return scopeStore.FindScopesAsync(application, CancellationToken); } + private async Task FindScopeAsync(TApplication application, string scope) + { + var scopes = await FindScopesAsync(application); + foreach (var foundScope in scopes) + { + if (string.Equals(scope, foundScope, StringComparison.Ordinal)) + { + return foundScope; + } + } + + return null; + } + public async Task AddScopeAsync(TApplication application, string scope) { var scopeStore = GetScopeStore(); - var result = await scopeStore.AddScopeAsync(application, scope, CancellationToken); - if (!result.Succeeded) + + var validation = await ValidateScopeAsync(application, scope); + if (!validation.Succeeded) { - return result; + return validation; } - return await scopeStore.UpdateAsync(application, CancellationToken); - } - - public async Task RemoveScopeAsync(TApplication application, string scope) - { - var scopeStore = GetScopeStore(); - var result = await scopeStore.RemoveScopeAsync(application, scope, CancellationToken); + var result = await scopeStore.AddScopeAsync(application, scope, CancellationToken); if (!result.Succeeded) { return result; @@ -456,6 +575,18 @@ namespace Microsoft.AspNetCore.Identity.Service public async Task UpdateScopeAsync(TApplication application, string oldScope, string newScope) { var scopeStore = GetScopeStore(); + var scope = await FindScopeAsync(application, oldScope); + if (scope == null) + { + return IdentityServiceResult.Failed(ErrorDescriber.ScopeNotFound(oldScope)); + } + + var validation = await ValidateScopeAsync(application, newScope); + if (!validation.Succeeded) + { + return validation; + } + var result = await scopeStore.UpdateScopeAsync(application, oldScope, newScope, CancellationToken); if (!result.Succeeded) { @@ -465,14 +596,42 @@ namespace Microsoft.AspNetCore.Identity.Service return await scopeStore.UpdateAsync(application, CancellationToken); } - private IApplicationScopeStore GetScopeStore() + public async Task RemoveScopeAsync(TApplication application, string scope) { - if (Store is IApplicationScopeStore cast) + var scopeStore = GetScopeStore(); + var foundScope = await FindScopeAsync(application, scope); + if (foundScope == null) { - return cast; + return IdentityServiceResult.Failed(ErrorDescriber.ScopeNotFound(scope)); } - throw new NotSupportedException(); + var result = await scopeStore.RemoveScopeAsync(application, scope, CancellationToken); + if (!result.Succeeded) + { + return result; + } + + return await scopeStore.UpdateAsync(application, CancellationToken); + } + + private async Task ValidateScopeAsync(TApplication application, string scope) + { + var errors = new List(); + foreach (var v in ApplicationValidators) + { + var result = await v.ValidateScopeAsync(this, application, scope); + if (!result.Succeeded) + { + errors.AddRange(result.Errors); + } + } + + if (errors.Count > 0) + { + return IdentityServiceResult.Failed(errors.ToArray()); + } + + return IdentityServiceResult.Success; } public virtual Task AddClaimAsync(TApplication application, Claim claim) @@ -503,6 +662,15 @@ namespace Microsoft.AspNetCore.Identity.Service throw new ArgumentNullException(nameof(application)); } + foreach (var claim in claims) + { + var validation = await ValidateClaimAsync(application, claim); + if (!validation.Succeeded) + { + return validation; + } + } + await claimStore.AddClaimsAsync(application, claims, CancellationToken); return await UpdateAsync(application); } @@ -524,6 +692,12 @@ namespace Microsoft.AspNetCore.Identity.Service throw new ArgumentNullException(nameof(application)); } + var validation = await ValidateClaimAsync(application, newClaim); + if (!validation.Succeeded) + { + return validation; + } + await claimStore.ReplaceClaimAsync(application, claim, newClaim, CancellationToken); return await UpdateAsync(application); } @@ -560,6 +734,26 @@ namespace Microsoft.AspNetCore.Identity.Service return await UpdateAsync(application); } + private async Task ValidateClaimAsync(TApplication application, Claim claim) + { + var errors = new List(); + foreach (var v in ApplicationValidators) + { + var result = await v.ValidateClaimAsync(this, application, claim); + if (!result.Succeeded) + { + errors.AddRange(result.Errors); + } + } + + if (errors.Count > 0) + { + return IdentityServiceResult.Failed(errors.ToArray()); + } + + return IdentityServiceResult.Success; + } + public virtual async Task> GetClaimsAsync(TApplication application) { ThrowIfDisposed(); @@ -571,32 +765,34 @@ namespace Microsoft.AspNetCore.Identity.Service return await claimStore.GetClaimsAsync(application, CancellationToken); } - public async Task UpdateLogoutUriAsync(TApplication application, string oldLogoutUri, string newLogoutUri) + private IRedirectUriStore GetRedirectUriStore() { - ThrowIfDisposed(); - if (application == null) + if (Store is IRedirectUriStore cast) { - throw new ArgumentNullException(nameof(application)); + return cast; } - if (oldLogoutUri == null) + throw new NotSupportedException(); + } + + private IApplicationClientSecretStore GetClientSecretStore() + { + if (Store is IApplicationClientSecretStore cast) { - throw new ArgumentNullException(nameof(oldLogoutUri)); + return cast; } - if (newLogoutUri == null) + throw new NotSupportedException(); + } + + private IApplicationScopeStore GetScopeStore() + { + if (Store is IApplicationScopeStore cast) { - throw new ArgumentNullException(nameof(newLogoutUri)); + return cast; } - var redirectUriStore = GetRedirectUriStore(); - var result = await redirectUriStore.UpdateLogoutRedirectUriAsync(application, oldLogoutUri, newLogoutUri, CancellationToken); - if (!result.Succeeded) - { - return result; - } - - return await UpdateAsync(application); + throw new NotSupportedException(); } private IApplicationClaimStore GetApplicationClaimStore() @@ -616,5 +812,30 @@ namespace Microsoft.AspNetCore.Identity.Service throw new ObjectDisposedException(GetType().Name); } } + + private async Task ValidateApplicationAsync(TApplication application) + { + var errors = new List(); + foreach (var v in ApplicationValidators) + { + var result = await v.ValidateAsync(this, application); + if (!result.Succeeded) + { + errors.AddRange(result.Errors); + } + } + + if (errors.Count > 0) + { + return IdentityServiceResult.Failed(errors.ToArray()); + } + + return IdentityServiceResult.Success; + } + + public void Dispose() + { + _disposed = true; + } } } diff --git a/src/Microsoft.AspNetCore.Identity.Service/ApplicationOptions.cs b/src/Microsoft.AspNetCore.Identity.Service/ApplicationOptions.cs new file mode 100644 index 0000000000..8ae80722ba --- /dev/null +++ b/src/Microsoft.AspNetCore.Identity.Service/ApplicationOptions.cs @@ -0,0 +1,28 @@ +// 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; +using System.Collections.Generic; + +namespace Microsoft.AspNetCore.Identity.Service +{ + public class ApplicationOptions + { + public string AllowedNameCharacters { get; set; } = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._@+"; + public int? MaxApplicationNameLength { get; set; } = 36; + public string AllowedClientIdCharacters { get; set; } = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._@+"; + public int? MaxClientIdLength { get; set; } = 36; + public string AllowedScopeCharacters { get; set; } = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._@+"; + public int? MaxScopeLength { get; set; } = 16; + + public IList AllowedRedirectUris { get; set; } = new List + { + "urn:ietf:wg:oauth:2.0:oob" + }; + + public IList AllowedLogoutUris { get; set; } = new List + { + "urn:ietf:wg:oauth:2.0:oob" + }; + } +} diff --git a/src/Microsoft.AspNetCore.Identity.Service/ApplicationValidator.cs b/src/Microsoft.AspNetCore.Identity.Service/ApplicationValidator.cs new file mode 100644 index 0000000000..c7fb5487ba --- /dev/null +++ b/src/Microsoft.AspNetCore.Identity.Service/ApplicationValidator.cs @@ -0,0 +1,232 @@ +// 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.Security.Claims; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Identity.Service +{ + public class ApplicationValidator : IApplicationValidator + where TApplication : class + { + public ApplicationValidator(ApplicationErrorDescriber errorDescriber) + { + ErrorDescriber = errorDescriber; + } + + public ApplicationErrorDescriber ErrorDescriber { get; } + + public async Task ValidateAsync( + ApplicationManager manager, + TApplication application) + { + var errors = new List(); + await ValidateNameAsync(manager, application, errors); + await ValidateClientIdAsync(manager, application, errors); + + return errors.Count > 0 ? IdentityServiceResult.Failed(errors.ToArray()) : IdentityServiceResult.Success; + } + + private async Task ValidateNameAsync( + ApplicationManager manager, + TApplication application, + IList errors) + { + var applicationName = await manager.GetApplicationNameAsync(application); + if (string.IsNullOrWhiteSpace(applicationName)) + { + errors.Add(ErrorDescriber.InvalidApplicationName(applicationName)); + } + else if (!string.IsNullOrEmpty(manager.Options.AllowedNameCharacters) && + applicationName.Any(c => !manager.Options.AllowedNameCharacters.Contains(c))) + { + errors.Add(ErrorDescriber.InvalidApplicationName(applicationName)); + } + else if (manager.Options.MaxApplicationNameLength.HasValue && + applicationName.Length > manager.Options.MaxApplicationNameLength) + { + errors.Add(ErrorDescriber.InvalidApplicationName(applicationName)); + } + else + { + var otherApplication = await manager.FindByNameAsync(applicationName); + if (otherApplication != null && + !string.Equals( + await manager.GetApplicationIdAsync(otherApplication), + await manager.GetApplicationIdAsync(application), + StringComparison.Ordinal)) + { + errors.Add(ErrorDescriber.DuplicateApplicationName(applicationName)); + } + } + } + + private async Task ValidateClientIdAsync( + ApplicationManager manager, + TApplication application, + IList errors) + { + var clientId = await manager.GetApplicationClientIdAsync(application); + if (string.IsNullOrWhiteSpace(clientId)) + { + errors.Add(ErrorDescriber.InvalidApplicationClientId(clientId)); + } + else if (!string.IsNullOrEmpty(manager.Options.AllowedClientIdCharacters) && + clientId.Any(c => !manager.Options.AllowedClientIdCharacters.Contains(c))) + { + errors.Add(ErrorDescriber.InvalidApplicationClientId(clientId)); + } + else if (manager.Options.MaxApplicationNameLength.HasValue && + clientId.Length > manager.Options.MaxApplicationNameLength) + { + errors.Add(ErrorDescriber.InvalidApplicationClientId(clientId)); + } + else + { + var otherApplication = await manager.FindByClientIdAsync(clientId); + if (otherApplication != null && + !string.Equals( + await manager.GetApplicationIdAsync(otherApplication), + await manager.GetApplicationIdAsync(application), + StringComparison.Ordinal)) + { + errors.Add(ErrorDescriber.DuplicateApplicationClientId(clientId)); + } + } + } + + public async Task ValidateLogoutUriAsync( + ApplicationManager manager, + TApplication application, + string logoutUri) + { + var errors = new List(); + + var logoutUris = await manager.FindRegisteredLogoutUrisAsync(application); + if (logoutUris.Contains(logoutUri, StringComparer.OrdinalIgnoreCase)) + { + errors.Add(ErrorDescriber.DuplicateLogoutUri(logoutUri)); + } + + if (!manager.Options.AllowedLogoutUris.Contains(logoutUri, StringComparer.OrdinalIgnoreCase)) + { + if (!Uri.TryCreate(logoutUri, UriKind.Absolute, out var parsedUri)) + { + errors.Add(ErrorDescriber.InvalidLogoutUri(logoutUri)); + } + else + { + if (!parsedUri.Scheme.Equals("https", StringComparison.OrdinalIgnoreCase)) + { + errors.Add(ErrorDescriber.NoHttpsUri(logoutUri)); + } + + var redirectUris = await manager.FindRegisteredUrisAsync(application); + var regularRedirectUris = redirectUris.Except(manager.Options.AllowedRedirectUris, StringComparer.Ordinal); + var regularLogoutUris = logoutUris.Except(manager.Options.AllowedLogoutUris, StringComparer.Ordinal); + + var allApplicationUris = regularLogoutUris.Concat(regularRedirectUris); + + foreach (var nonSpecialUri in allApplicationUris) + { + var existingUri = new Uri(nonSpecialUri, UriKind.Absolute); + if (!parsedUri.Host.Equals(existingUri.Host, StringComparison.OrdinalIgnoreCase)) + { + errors.Add(ErrorDescriber.DifferentDomains()); + break; + } + } + } + } + + return errors.Count > 0 ? IdentityServiceResult.Failed(errors.ToArray()) : IdentityServiceResult.Success; + } + + public async Task ValidateRedirectUriAsync( + ApplicationManager manager, + TApplication application, + string redirectUri) + { + var errors = new List(); + + var redirectUris = await manager.FindRegisteredUrisAsync(application); + if (redirectUris.Contains(redirectUri, StringComparer.OrdinalIgnoreCase)) + { + errors.Add(ErrorDescriber.DuplicateRedirectUri(redirectUri)); + } + + if (!manager.Options.AllowedRedirectUris.Contains(redirectUri, StringComparer.OrdinalIgnoreCase)) + { + if (!Uri.TryCreate(redirectUri, UriKind.Absolute, out var parsedUri)) + { + errors.Add(ErrorDescriber.InvalidRedirectUri(redirectUri)); + } + else + { + if (!parsedUri.Scheme.Equals("https", StringComparison.OrdinalIgnoreCase)) + { + errors.Add(ErrorDescriber.NoHttpsUri(redirectUri)); + } + + var logoutUris = await manager.FindRegisteredUrisAsync(application); + var regularLogoutUris = logoutUris.Except(manager.Options.AllowedLogoutUris, StringComparer.Ordinal); + var regularRedirectUris = redirectUris.Except(manager.Options.AllowedRedirectUris, StringComparer.Ordinal); + + var allApplicationUris = regularRedirectUris.Concat(regularLogoutUris); + + foreach (var nonSpecialUri in allApplicationUris) + { + var existingUri = new Uri(nonSpecialUri, UriKind.Absolute); + if (!parsedUri.Host.Equals(existingUri.Host, StringComparison.OrdinalIgnoreCase)) + { + errors.Add(ErrorDescriber.DifferentDomains()); + break; + } + } + } + } + + return errors.Count > 0 ? IdentityServiceResult.Failed(errors.ToArray()) : IdentityServiceResult.Success; + } + + public async Task ValidateScopeAsync( + ApplicationManager manager, + TApplication application, + string scope) + { + var errors = new List(); + if (string.IsNullOrWhiteSpace(scope)) + { + errors.Add(ErrorDescriber.InvalidScope(scope)); + } + else if (!string.IsNullOrEmpty(manager.Options.AllowedScopeCharacters) && + scope.Any(c => !manager.Options.AllowedScopeCharacters.Contains(c))) + { + errors.Add(ErrorDescriber.InvalidScope(scope)); + } + else if (manager.Options.MaxScopeLength.HasValue && + scope.Length > manager.Options.MaxScopeLength) + { + errors.Add(ErrorDescriber.InvalidScope(scope)); + } + else + { + var scopes = await manager.FindScopesAsync(application); + if (scopes != null && scopes.Contains(scope, StringComparer.OrdinalIgnoreCase)) + { + errors.Add(ErrorDescriber.DuplicateScope(scope)); + } + } + + return errors.Count > 0 ? IdentityServiceResult.Failed(errors.ToArray()) : IdentityServiceResult.Success; + } + + public Task ValidateClaimAsync(ApplicationManager manager, TApplication application, Claim claim) + { + return Task.FromResult(IdentityServiceResult.Success); + } + } +} diff --git a/src/Microsoft.AspNetCore.Identity.Service/IApplicationClaimStore.cs b/src/Microsoft.AspNetCore.Identity.Service/IApplicationClaimStore.cs index cf4b0b0ec7..3ef3a3e8c1 100644 --- a/src/Microsoft.AspNetCore.Identity.Service/IApplicationClaimStore.cs +++ b/src/Microsoft.AspNetCore.Identity.Service/IApplicationClaimStore.cs @@ -10,9 +10,9 @@ namespace Microsoft.AspNetCore.Identity.Service { public interface IApplicationClaimStore : IApplicationStore where TApplication : class { - Task> GetClaimsAsync(TApplication user, CancellationToken cancellationToken); - Task AddClaimsAsync(TApplication user, IEnumerable claims, CancellationToken cancellationToken); - Task ReplaceClaimAsync(TApplication user, Claim claim, Claim newClaim, CancellationToken cancellationToken); - Task RemoveClaimsAsync(TApplication user, IEnumerable claims, CancellationToken cancellationToken); + Task> GetClaimsAsync(TApplication application, CancellationToken cancellationToken); + Task AddClaimsAsync(TApplication application, IEnumerable claims, CancellationToken cancellationToken); + Task ReplaceClaimAsync(TApplication application, Claim claim, Claim newClaim, CancellationToken cancellationToken); + Task RemoveClaimsAsync(TApplication application, IEnumerable claims, CancellationToken cancellationToken); } } \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Identity.Service/IApplicationValidator.cs b/src/Microsoft.AspNetCore.Identity.Service/IApplicationValidator.cs index 2940fda849..b83c0373d8 100644 --- a/src/Microsoft.AspNetCore.Identity.Service/IApplicationValidator.cs +++ b/src/Microsoft.AspNetCore.Identity.Service/IApplicationValidator.cs @@ -1,6 +1,7 @@ // 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.Security.Claims; using System.Threading.Tasks; namespace Microsoft.AspNetCore.Identity.Service @@ -9,5 +10,9 @@ namespace Microsoft.AspNetCore.Identity.Service where TApplication : class { Task ValidateAsync(ApplicationManager manager, TApplication application); + Task ValidateScopeAsync(ApplicationManager manager, TApplication application, string scope); + Task ValidateRedirectUriAsync(ApplicationManager manager, TApplication application, string redirectUri); + Task ValidateLogoutUriAsync(ApplicationManager manager, TApplication application, string logoutUri); + Task ValidateClaimAsync(ApplicationManager manager, TApplication application, Claim claim); } } diff --git a/src/Microsoft.AspNetCore.Identity.Service/IdentityServiceServiceCollectionExtensions.cs b/src/Microsoft.AspNetCore.Identity.Service/IdentityServiceServiceCollectionExtensions.cs index c483b47c76..02cc7b4d24 100644 --- a/src/Microsoft.AspNetCore.Identity.Service/IdentityServiceServiceCollectionExtensions.cs +++ b/src/Microsoft.AspNetCore.Identity.Service/IdentityServiceServiceCollectionExtensions.cs @@ -93,6 +93,8 @@ namespace Microsoft.Extensions.DependencyInjection services.AddSingleton, PasswordHasher>(); services.AddScoped(); services.AddScoped(); + services.AddSingleton, ApplicationValidator>(); + services.AddSingleton(); // Session services.AddTransient>(); diff --git a/test/Microsoft.AspNetCore.Identity.Service.EntityFrameworkCore.InMemory.Test/InMemoryEFApplicationStoreTest.cs b/test/Microsoft.AspNetCore.Identity.Service.EntityFrameworkCore.InMemory.Test/InMemoryEFApplicationStoreTest.cs index e6b3d1f087..686690c245 100644 --- a/test/Microsoft.AspNetCore.Identity.Service.EntityFrameworkCore.InMemory.Test/InMemoryEFApplicationStoreTest.cs +++ b/test/Microsoft.AspNetCore.Identity.Service.EntityFrameworkCore.InMemory.Test/InMemoryEFApplicationStoreTest.cs @@ -14,7 +14,7 @@ namespace Microsoft.AspNetCore.Identity.Service.EntityFrameworkCore.InMemory.Tes protected override void AddApplicationStore(IServiceCollection services, object context = null) { services.AddSingleton>( - new ApplicationStore, IdentityServiceApplicationClaim, IdentityServiceRedirectUri, InMemoryContext, string, string>((InMemoryContext)context)); + new ApplicationStore, IdentityServiceApplicationClaim, IdentityServiceRedirectUri, InMemoryContext, string, string>((InMemoryContext)context, new ApplicationErrorDescriber())); } protected override IdentityServiceApplication CreateTestApplication() diff --git a/test/Microsoft.AspNetCore.Identity.Service.EntityFrameworkCore.Test/ApplicationStoreTest.cs b/test/Microsoft.AspNetCore.Identity.Service.EntityFrameworkCore.Test/ApplicationStoreTest.cs index 20be012c33..2a3d1d5dcb 100644 --- a/test/Microsoft.AspNetCore.Identity.Service.EntityFrameworkCore.Test/ApplicationStoreTest.cs +++ b/test/Microsoft.AspNetCore.Identity.Service.EntityFrameworkCore.Test/ApplicationStoreTest.cs @@ -2,10 +2,12 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; -using Microsoft.AspNetCore.Identity.EntityFrameworkCore; +using System.Threading.Tasks; using Microsoft.AspNetCore.Identity.EntityFrameworkCore.Test; using Microsoft.AspNetCore.Identity.Service.Specification.Tests; +using Microsoft.AspNetCore.Identity.Test; using Microsoft.AspNetCore.Testing; +using Microsoft.AspNetCore.Testing.xunit; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Xunit; @@ -15,6 +17,7 @@ namespace Microsoft.AspNetCore.Identity.Service.EntityFrameworkCore.Test public class ApplicationStoreTest : IdentityServiceSpecificationTestBase, IClassFixture { private readonly ScratchDatabaseFixture _fixture; + public static readonly ApplicationErrorDescriber ErrorDescriber = new ApplicationErrorDescriber(); public ApplicationStoreTest(ScratchDatabaseFixture fixture) { @@ -24,7 +27,7 @@ namespace Microsoft.AspNetCore.Identity.Service.EntityFrameworkCore.Test protected override void AddApplicationStore(IServiceCollection services, object context = null) { services.AddSingleton>( - new ApplicationStore, IdentityServiceApplicationClaim, IdentityServiceRedirectUri, IdentityServiceDbContext, string, string>((IdentityServiceDbContext)context)); + new ApplicationStore, IdentityServiceApplicationClaim, IdentityServiceRedirectUri, IdentityServiceDbContext, string, string>((IdentityServiceDbContext)context, new ApplicationErrorDescriber())); } public IdentityServiceDbContext CreateContext(bool delete = false) @@ -59,5 +62,89 @@ namespace Microsoft.AspNetCore.Identity.Service.EntityFrameworkCore.Test } protected override bool ShouldSkipDbTests() => TestPlatformHelper.IsMono || !TestPlatformHelper.IsWindows; + + [ConditionalFact] + [FrameworkSkipCondition(RuntimeFrameworks.Mono)] + [OSSkipCondition(OperatingSystems.Linux)] + [OSSkipCondition(OperatingSystems.MacOSX)] + public async Task ConcurrentUpdatesWillFail() + { + var application = CreateTestApplication(); + using (var db = CreateContext()) + { + var manager = CreateManager(db); + IdentityServiceResultAssert.IsSuccess(await manager.CreateAsync(application)); + } + using (var db = CreateContext()) + using (var db2 = CreateContext()) + { + var manager1 = CreateManager(db); + var manager2 = CreateManager(db2); + var application1 = await manager1.FindByIdAsync(application.Id); + var application2 = await manager2.FindByIdAsync(application.Id); + Assert.NotNull(application1); + Assert.NotNull(application2); + Assert.NotSame(application1, application2); + application1.Name = Guid.NewGuid().ToString(); + application2.Name = Guid.NewGuid().ToString(); + IdentityServiceResultAssert.IsSuccess(await manager1.UpdateAsync(application1)); + IdentityServiceResultAssert.IsFailure(await manager2.UpdateAsync(application2), ErrorDescriber.ConcurrencyFailure()); + } + } + + [ConditionalFact] + [FrameworkSkipCondition(RuntimeFrameworks.Mono)] + [OSSkipCondition(OperatingSystems.Linux)] + [OSSkipCondition(OperatingSystems.MacOSX)] + public async Task ConcurrentUpdatesWillFailWithDetachedApplication() + { + var application = CreateTestApplication(); + using (var db = CreateContext()) + { + var manager = CreateManager(db); + IdentityServiceResultAssert.IsSuccess(await manager.CreateAsync(application)); + } + using (var db1 = CreateContext()) + using (var db2 = CreateContext()) + { + var manager1 = CreateManager(db1); + var manager2 = CreateManager(db2); + var application2 = await manager2.FindByIdAsync(application.Id); + Assert.NotNull(application2); + Assert.NotSame(application, application2); + application.Name= Guid.NewGuid().ToString(); + application2.Name = Guid.NewGuid().ToString(); + IdentityServiceResultAssert.IsSuccess(await manager1.UpdateAsync(application)); + IdentityServiceResultAssert.IsFailure(await manager2.UpdateAsync(application2), ErrorDescriber.ConcurrencyFailure()); + } + } + + [ConditionalFact] + [FrameworkSkipCondition(RuntimeFrameworks.Mono)] + [OSSkipCondition(OperatingSystems.Linux)] + [OSSkipCondition(OperatingSystems.MacOSX)] + public async Task DeleteAModifiedApplicationWillFail() + { + var application = CreateTestApplication(); + using (var db = CreateContext()) + { + var manager = CreateManager(db); + IdentityServiceResultAssert.IsSuccess(await manager.CreateAsync(application)); + } + using (var db = CreateContext()) + using (var db2 = CreateContext()) + { + var manager1 = CreateManager(db); + var manager2 = CreateManager(db2); + var application1 = await manager1.FindByIdAsync(application.Id); + var application2 = await manager2.FindByIdAsync(application.Id); + Assert.NotNull(application1); + Assert.NotNull(application2); + Assert.NotSame(application1, application2); + application1.Name = Guid.NewGuid().ToString(); + IdentityServiceResultAssert.IsSuccess(await manager1.UpdateAsync(application1)); + IdentityServiceResultAssert.IsFailure(await manager2.DeleteAsync(application2), ErrorDescriber.ConcurrencyFailure()); + } + } } } diff --git a/test/Microsoft.AspNetCore.Identity.Service.InMemory.Test/InMemoryStore.cs b/test/Microsoft.AspNetCore.Identity.Service.InMemory.Test/InMemoryStore.cs index 7b08ee1579..5a5ca6004f 100644 --- a/test/Microsoft.AspNetCore.Identity.Service.InMemory.Test/InMemoryStore.cs +++ b/test/Microsoft.AspNetCore.Identity.Service.InMemory.Test/InMemoryStore.cs @@ -172,6 +172,12 @@ namespace Microsoft.AspNetCore.Identity.Service.InMemory.Test return Task.CompletedTask; } + public Task SetApplicationNameAsync(TApplication application, string name, CancellationToken cancellationToken) + { + application.Name = name; + return Task.CompletedTask; + } + public Task SetClientSecretHashAsync(TApplication application, string clientSecretHash, CancellationToken cancellationToken) { application.ClientSecretHash = clientSecretHash; diff --git a/test/Microsoft.AspNetCore.Identity.Service.Test/ApplicationManagerTest.cs b/test/Microsoft.AspNetCore.Identity.Service.Test/ApplicationManagerTest.cs new file mode 100644 index 0000000000..88eb1c23d5 --- /dev/null +++ b/test/Microsoft.AspNetCore.Identity.Service.Test/ApplicationManagerTest.cs @@ -0,0 +1,1461 @@ +// 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.Security.Claims; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Moq; +using Xunit; + +namespace Microsoft.AspNetCore.Identity.Service +{ + public class ApplicationManagerTest + { + public static readonly ApplicationErrorDescriber ErrorDescriber = new ApplicationErrorDescriber(); + + [Fact] + public async Task CreateCallsStore() + { + // Arrange + var application = new TestApplication { Name = "Application", ClientId = "ClientId" }; + + var store = new Mock>(); + store.Setup(s => s.CreateAsync(application, CancellationToken.None)) + .ReturnsAsync(IdentityServiceResult.Success) + .Verifiable(); + + var applicationManager = GetApplicationManager(store.Object, application); + + // Act + var result = await applicationManager.CreateAsync(application); + + // Assert + Assert.True(result.Succeeded); + store.VerifyAll(); + } + + [Fact] + public async Task CreateCallsValidators() + { + // Arrange + var application = new TestApplication { Name = "Application", ClientId = "ClientId" }; + + var validator = new Mock>(); + validator.Setup(v => v.ValidateAsync(It.IsAny>(), application)) + .ReturnsAsync(IdentityServiceResult.Success); + + var store = new Mock>(); + store.Setup(s => s.CreateAsync(application, CancellationToken.None)) + .ReturnsAsync(IdentityServiceResult.Success) + .Verifiable(); + + var applicationManager = GetApplicationManager(store.Object, application, validator.Object); + + // Act + var result = await applicationManager.CreateAsync(application); + + // Assert + Assert.True(result.Succeeded); + store.VerifyAll(); + validator.VerifyAll(); + } + + [Fact] + public async Task CreateValidatorsCanBlockCreate() + { + // Arrange + var application = new TestApplication { Name = "Application", ClientId = "ClientId" }; + + var validator = new Mock>(); + validator.Setup(v => v.ValidateAsync(It.IsAny>(), application)) + .ReturnsAsync(IdentityServiceResult.Failed(new IdentityServiceError())); + + var store = new Mock>(); + + var applicationManager = GetApplicationManager(store.Object, application, validator.Object); + + // Act + var result = await applicationManager.CreateAsync(application); + + // Assert + Assert.False(result.Succeeded); + store.VerifyAll(); + validator.VerifyAll(); + } + + [Fact] + public async Task UpdateCallsStore() + { + // Arrange + var application = new TestApplication { Name = "Application", ClientId = "ClientId" }; + + var store = new Mock>(); + store.Setup(s => s.UpdateAsync(application, CancellationToken.None)) + .ReturnsAsync(IdentityServiceResult.Success) + .Verifiable(); + + var applicationManager = GetApplicationManager(store.Object, application); + + // Act + var result = await applicationManager.UpdateAsync(application); + + // Assert + Assert.True(result.Succeeded); + store.VerifyAll(); + } + + [Fact] + public async Task UpdateCallsValidators() + { + // Arrange + var application = new TestApplication { Name = "Application", ClientId = "ClientId" }; + + var validator = new Mock>(); + validator.Setup(v => v.ValidateAsync(It.IsAny>(), application)) + .ReturnsAsync(IdentityServiceResult.Success); + + var store = new Mock>(); + store.Setup(s => s.UpdateAsync(application, CancellationToken.None)) + .ReturnsAsync(IdentityServiceResult.Success) + .Verifiable(); + + var applicationManager = GetApplicationManager(store.Object, application, validator.Object); + + // Act + var result = await applicationManager.UpdateAsync(application); + + // Assert + Assert.True(result.Succeeded); + store.VerifyAll(); + validator.VerifyAll(); + } + + [Fact] + public async Task UpdateValidatorsCanBlockCreate() + { + // Arrange + var application = new TestApplication { Name = "Application", ClientId = "ClientId" }; + + var validator = new Mock>(); + validator.Setup(v => v.ValidateAsync(It.IsAny>(), application)) + .ReturnsAsync(IdentityServiceResult.Failed(new IdentityServiceError())); + + var store = new Mock>(); + + var applicationManager = GetApplicationManager(store.Object, application, validator.Object); + + // Act + var result = await applicationManager.UpdateAsync(application); + + // Assert + Assert.False(result.Succeeded); + store.VerifyAll(); + validator.VerifyAll(); + } + + [Fact] + public async Task DeleteCallsStore() + { + // Arrange + var application = new TestApplication { Name = "Application", ClientId = "ClientId" }; + + var store = new Mock>(); + store.Setup(s => s.DeleteAsync(application, CancellationToken.None)) + .ReturnsAsync(IdentityServiceResult.Success) + .Verifiable(); + + var applicationManager = GetApplicationManager(store.Object, application); + + // Act + var result = await applicationManager.DeleteAsync(application); + + // Assert + Assert.True(result.Succeeded); + store.VerifyAll(); + } + + [Fact] + public async Task FindByIdCallsStore() + { + // Arrange + var application = new TestApplication { Name = "Application", ClientId = "ClientId" }; + + var store = new Mock>(); + store.Setup(s => s.FindByIdAsync(It.IsAny(), CancellationToken.None)) + .ReturnsAsync(application) + .Verifiable(); + + var applicationManager = GetApplicationManager(store.Object, application); + + // Act + var foundApplication = await applicationManager.FindByIdAsync("id"); + + // Assert + Assert.Equal(application, foundApplication); + store.VerifyAll(); + } + + [Fact] + public async Task GetApplicationIdCallsStore() + { + // Arrange + var application = new TestApplication { Name = "Application", ClientId = "ClientId" }; + + var store = new Mock>(); + store.Setup(s => s.GetApplicationIdAsync(application, CancellationToken.None)) + .ReturnsAsync("id") + .Verifiable(); + + var applicationManager = GetApplicationManager(store.Object, application); + + // Act + var id = await applicationManager.GetApplicationIdAsync(application); + + // Assert + Assert.Equal("id", id); + store.VerifyAll(); + } + + [Fact] + public async Task FindByClientIdCallsStore() + { + // Arrange + var application = new TestApplication { Name = "Application", ClientId = "ClientId" }; + + var store = new Mock>(); + store.Setup(s => s.FindByClientIdAsync(It.IsAny(), CancellationToken.None)) + .ReturnsAsync(application) + .Verifiable(); + + var applicationManager = GetApplicationManager(store.Object, application); + + // Act + var foundApplication = await applicationManager.FindByClientIdAsync("ClientId"); + + // Assert + Assert.Equal(application, foundApplication); + store.VerifyAll(); + } + + [Fact] + public async Task GetApplicationClientIdCallsStore() + { + // Arrange + var application = new TestApplication { Name = "Application", ClientId = "ClientId" }; + + var store = new Mock>(); + store.Setup(s => s.GetApplicationClientIdAsync(application, CancellationToken.None)) + .ReturnsAsync("clientId") + .Verifiable(); + + var applicationManager = GetApplicationManager(store.Object, application); + + // Act + var clientId = await applicationManager.GetApplicationClientIdAsync(application); + + // Assert + Assert.Equal("clientId", clientId); + store.VerifyAll(); + } + + [Fact] + public async Task FindByNameCallsStore() + { + // Arrange + var application = new TestApplication { Name = "Application", ClientId = "ClientId" }; + + var store = new Mock>(); + store.Setup(s => s.FindByNameAsync(It.IsAny(), CancellationToken.None)) + .ReturnsAsync(application) + .Verifiable(); + + var applicationManager = GetApplicationManager(store.Object, application); + + // Act + var foundApplication = await applicationManager.FindByNameAsync("Application"); + + // Assert + Assert.Equal(application, foundApplication); + store.VerifyAll(); + } + + [Fact] + public async Task GetApplicationNameCallsStore() + { + // Arrange + var application = new TestApplication { Name = "Application", ClientId = "ClientId" }; + + var store = new Mock>(); + store.Setup(s => s.GetApplicationNameAsync(application, CancellationToken.None)) + .ReturnsAsync("Application") + .Verifiable(); + + var applicationManager = GetApplicationManager(store.Object, application); + + // Act + var name = await applicationManager.GetApplicationNameAsync(application); + + // Assert + Assert.Equal("Application", name); + store.VerifyAll(); + } + + [Fact] + public async Task SetApplicationNameCallsStore() + { + // Arrange + var application = new TestApplication { Name = "Application", ClientId = "ClientId" }; + + var store = new Mock>(); + store.Setup(s => s.SetApplicationNameAsync(application, "Updated", CancellationToken.None)) + .Returns(Task.CompletedTask) + .Verifiable(); + + store.Setup(s => s.UpdateAsync(application, CancellationToken.None)) + .ReturnsAsync(IdentityServiceResult.Success) + .Verifiable(); + + var applicationManager = GetApplicationManager(store.Object, application); + + // Act + var result = await applicationManager.SetApplicationNameAsync(application, "Updated"); + + // Assert + Assert.True(result.Succeeded); + store.VerifyAll(); + } + + [Fact] + public async Task SetApplicationNameCallsValidators() + { + // Arrange + var application = new TestApplication { Name = "Application", ClientId = "ClientId" }; + + var store = new Mock>(); + store.Setup(s => s.SetApplicationNameAsync(application, "Updated", CancellationToken.None)) + .Returns(Task.CompletedTask) + .Verifiable(); + + store.Setup(s => s.UpdateAsync(application, CancellationToken.None)) + .ReturnsAsync(IdentityServiceResult.Success) + .Verifiable(); + + var validator = new Mock>(); + validator.Setup(v => v.ValidateAsync(It.IsAny>(), application)) + .ReturnsAsync(IdentityServiceResult.Success); + + var applicationManager = GetApplicationManager(store.Object, application, validator.Object); + + // Act + var result = await applicationManager.SetApplicationNameAsync(application, "Updated"); + + // Assert + Assert.True(result.Succeeded); + store.VerifyAll(); + } + + [Fact] + public async Task SetApplicationNameValidatorsCanBlockUpdate() + { + // Arrange + var application = new TestApplication { Name = "Application", ClientId = "ClientId" }; + + var store = new Mock>(); + store.Setup(s => s.SetApplicationNameAsync(application, "Updated", CancellationToken.None)) + .Returns(Task.CompletedTask) + .Verifiable(); + + var validator = new Mock>(); + validator.Setup(v => v.ValidateAsync(It.IsAny>(), application)) + .ReturnsAsync(IdentityServiceResult.Failed(new IdentityServiceError())); + + var applicationManager = GetApplicationManager(store.Object, application, validator.Object); + + // Act + var result = await applicationManager.SetApplicationNameAsync(application, "Updated"); + + // Assert + Assert.False(result.Succeeded); + store.VerifyAll(); + } + + [Fact] + public async Task HasClientSecretCallsStore() + { + // Arrange + var application = new TestApplication { Name = "Application", ClientId = "ClientId" }; + + var store = new Mock>() + .As>(); + store.Setup(s => s.HasClientSecretAsync(application, CancellationToken.None)) + .ReturnsAsync(true) + .Verifiable(); + + var applicationManager = GetApplicationManager(store.Object, application); + + // Act + var hasClientSecret = await applicationManager.HasClientSecretAsync(application); + + // Assert + Assert.True(hasClientSecret); + store.VerifyAll(); + } + + [Fact] + public async Task AddClientSecretCallsStore() + { + // Arrange + var application = new TestApplication { Name = "Application", ClientId = "ClientId" }; + + var store = new Mock>() + .As>(); + + store.Setup(s => s.GetClientSecretHashAsync(application, CancellationToken.None)) + .ReturnsAsync((string)null) + .Verifiable(); + + store.Setup(s => s.SetClientSecretHashAsync(application, "new-hash", CancellationToken.None)) + .Returns(Task.CompletedTask) + .Verifiable(); + + store.Setup(s => s.UpdateAsync(application, CancellationToken.None)) + .ReturnsAsync(IdentityServiceResult.Success) + .Verifiable(); + + var applicationManager = GetApplicationManager(store.Object, application); + + // Act + var result = await applicationManager.AddClientSecretAsync(application, "client-secret"); + + // Assert + Assert.True(result.Succeeded); + store.VerifyAll(); + } + + [Fact] + public async Task AddClientSecretFailsIfTheresAlreadyASecret() + { + // Arrange + var application = new TestApplication { Name = "Application", ClientId = "ClientId" }; + var expectedError = new List { ErrorDescriber.ApplicationAlreadyHasClientSecret() }; + + var store = new Mock>() + .As>(); + + store.Setup(s => s.GetClientSecretHashAsync(application, CancellationToken.None)) + .ReturnsAsync("hash") + .Verifiable(); + + var applicationManager = GetApplicationManager(store.Object, application); + + // Act + var result = await applicationManager.AddClientSecretAsync(application, "client-secret"); + + // Assert + Assert.False(result.Succeeded); + Assert.Equal(expectedError, result.Errors, ErrorsComparer.Instance); + + store.VerifyAll(); + } + + [Fact] + public async Task AddClientSecretFailsIfValidatorBlocksTheUpdate() + { + // Arrange + var application = new TestApplication { Name = "Application", ClientId = "ClientId" }; + + var store = new Mock>() + .As>(); + + store.Setup(s => s.GetClientSecretHashAsync(application, CancellationToken.None)) + .ReturnsAsync((string)null) + .Verifiable(); + + store.Setup(s => s.SetClientSecretHashAsync(application, "new-hash", CancellationToken.None)) + .Returns(Task.CompletedTask) + .Verifiable(); + + var validator = new Mock>(); + validator.Setup(v => v.ValidateAsync(It.IsAny>(), application)) + .ReturnsAsync(IdentityServiceResult.Failed(new IdentityServiceError())); + + var applicationManager = GetApplicationManager(store.Object, application, validator.Object); + + // Act + var result = await applicationManager.AddClientSecretAsync(application, "client-secret"); + + // Assert + Assert.False(result.Succeeded); + store.VerifyAll(); + } + + [Fact] + public async Task ChangeClientSecretCallsStore() + { + // Arrange + var application = new TestApplication { Name = "Application", ClientId = "ClientId" }; + + var store = new Mock>() + .As>(); + + store.Setup(s => s.SetClientSecretHashAsync(application, "new-hash", CancellationToken.None)) + .Returns(Task.CompletedTask) + .Verifiable(); + + store.Setup(s => s.UpdateAsync(application, CancellationToken.None)) + .ReturnsAsync(IdentityServiceResult.Success) + .Verifiable(); + + var applicationManager = GetApplicationManager(store.Object, application); + + // Act + var result = await applicationManager.ChangeClientSecretAsync(application, "client-secret"); + + // Assert + Assert.True(result.Succeeded); + store.VerifyAll(); + } + + [Fact] + public async Task ChangeClientSecretFailsIfValidatorBlocksTheUpdate() + { + // Arrange + var application = new TestApplication { Name = "Application", ClientId = "ClientId" }; + + var store = new Mock>() + .As>(); + + store.Setup(s => s.SetClientSecretHashAsync(application, "new-hash", CancellationToken.None)) + .Returns(Task.CompletedTask) + .Verifiable(); + + var validator = new Mock>(); + validator.Setup(v => v.ValidateAsync(It.IsAny>(), application)) + .ReturnsAsync(IdentityServiceResult.Failed(new IdentityServiceError())); + + var applicationManager = GetApplicationManager(store.Object, application, validator.Object); + + // Act + var result = await applicationManager.ChangeClientSecretAsync(application, "client-secret"); + + // Assert + Assert.False(result.Succeeded); + store.VerifyAll(); + } + + [Fact] + public async Task RemoveClientSecretCallsStore() + { + // Arrange + var application = new TestApplication { Name = "Application", ClientId = "ClientId" }; + + var store = new Mock>() + .As>(); + + store.Setup(s => s.SetClientSecretHashAsync(application, null, CancellationToken.None)) + .Returns(Task.CompletedTask) + .Verifiable(); + + store.Setup(s => s.UpdateAsync(application, CancellationToken.None)) + .ReturnsAsync(IdentityServiceResult.Success) + .Verifiable(); + + var applicationManager = GetApplicationManager(store.Object, application); + + // Act + var result = await applicationManager.RemoveClientSecretAsync(application); + + // Assert + Assert.True(result.Succeeded); + store.VerifyAll(); + } + + [Fact] + public async Task RemoveClientSecretFailsIfValidatorBlocksTheUpdate() + { + // Arrange + var application = new TestApplication { Name = "Application", ClientId = "ClientId" }; + + var store = new Mock>() + .As>(); + + store.Setup(s => s.SetClientSecretHashAsync(application, null, CancellationToken.None)) + .Returns(Task.CompletedTask) + .Verifiable(); + + var validator = new Mock>(); + validator.Setup(v => v.ValidateAsync(It.IsAny>(), application)) + .ReturnsAsync(IdentityServiceResult.Failed(new IdentityServiceError())); + + var applicationManager = GetApplicationManager(store.Object, application, validator.Object); + + // Act + var result = await applicationManager.RemoveClientSecretAsync(application); + + // Assert + Assert.False(result.Succeeded); + store.VerifyAll(); + } + + [Fact] + public async Task ValidateClientCredentialsCallsStore() + { + // Arrange + var application = new TestApplication { Name = "Application", ClientId = "ClientId" }; + + var store = new Mock>(); + store.Setup(s => s.FindByClientIdAsync("clientId", It.IsAny())) + .ReturnsAsync(application); + + var secretStore = store.As>(); + secretStore.Setup(s => s.HasClientSecretAsync(application, CancellationToken.None)) + .ReturnsAsync(true) + .Verifiable(); + + secretStore.Setup(s => s.GetClientSecretHashAsync(application, CancellationToken.None)) + .ReturnsAsync("hash") + .Verifiable(); + + var applicationManager = GetApplicationManager(secretStore.Object, application); + + // Act + var result = await applicationManager.ValidateClientCredentialsAsync("clientId", "client-secret"); + + // Assert + Assert.True(result); + secretStore.VerifyAll(); + } + + [Fact] + public async Task ValidateClientCredentialsUpdatesHashIfNeeded() + { + // Arrange + var application = new TestApplication { Name = "Application", ClientId = "ClientId" }; + + var store = new Mock>(); + store.Setup(s => s.FindByClientIdAsync("clientId", It.IsAny())) + .ReturnsAsync(application); + + store.Setup(s => s.UpdateAsync(application, It.IsAny())) + .ReturnsAsync(IdentityServiceResult.Success); + + var secretStore = store.As>(); + secretStore.Setup(s => s.HasClientSecretAsync(application, CancellationToken.None)) + .ReturnsAsync(true) + .Verifiable(); + + secretStore.Setup(s => s.GetClientSecretHashAsync(application, CancellationToken.None)) + .ReturnsAsync("hash") + .Verifiable(); + + secretStore.Setup(s => s.SetClientSecretHashAsync(application, "new-hash", CancellationToken.None)) + .Returns(Task.CompletedTask) + .Verifiable(); + + var applicationManager = GetApplicationManager(secretStore.Object, application, null, PasswordVerificationResult.SuccessRehashNeeded); + + // Act + var result = await applicationManager.ValidateClientCredentialsAsync("clientId", "client-secret"); + + // Assert + Assert.True(result); + secretStore.VerifyAll(); + } + + [Fact] + public async Task ValidateClientCredentialsReturnsFalseIfClientCredentialsValidationFails() + { + // Arrange + var application = new TestApplication { Name = "Application", ClientId = "ClientId" }; + + var store = new Mock>(); + store.Setup(s => s.FindByClientIdAsync("clientId", It.IsAny())) + .ReturnsAsync(application); + + var secretStore = store.As>(); + secretStore.Setup(s => s.HasClientSecretAsync(application, CancellationToken.None)) + .ReturnsAsync(true) + .Verifiable(); + + secretStore.Setup(s => s.GetClientSecretHashAsync(application, CancellationToken.None)) + .ReturnsAsync("hash") + .Verifiable(); + + var applicationManager = GetApplicationManager(secretStore.Object, application, null, PasswordVerificationResult.Failed); + + // Act + var result = await applicationManager.ValidateClientCredentialsAsync("clientId", "client-secret"); + + // Assert + Assert.False(result); + secretStore.VerifyAll(); + } + + [Fact] + public async Task FindRegisteredUrisCallsStore() + { + // Arrange + var application = new TestApplication { Name = "Application", ClientId = "ClientId" }; + + var store = new Mock>(); + + store.As>() + .Setup(s => s.FindRegisteredUrisAsync(application, It.IsAny())) + .ReturnsAsync(new[] { "https://www.example.com/sign-in" }); + + var applicationManager = GetApplicationManager(store.Object, application); + + // Act + var result = await applicationManager.FindRegisteredUrisAsync(application); + + // Assert + Assert.Equal(new[] { "https://www.example.com/sign-in" }, result); + store.VerifyAll(); + } + + [Fact] + public async Task RegisterRedirectUriCallsStore() + { + // Arrange + var application = new TestApplication { Name = "Application", ClientId = "ClientId" }; + + var store = new Mock>(); + store.Setup(s => s.UpdateAsync(application, It.IsAny())) + .ReturnsAsync(IdentityServiceResult.Success); + + var redirectUriStore = store.As>(); + redirectUriStore.Setup(s => s.RegisterRedirectUriAsync(application, It.IsAny(), It.IsAny())) + .ReturnsAsync(IdentityServiceResult.Success); + + var applicationManager = GetApplicationManager(redirectUriStore.Object, application); + + // Act + var result = await applicationManager.RegisterRedirectUriAsync(application, "https://www.example.com/sign-in"); + + // Assert + Assert.True(result.Succeeded); + redirectUriStore.VerifyAll(); + } + + [Fact] + public async Task RegisterRedirectUriValidatorsCanBlockUpdate() + { + // Arrange + var application = new TestApplication { Name = "Application", ClientId = "ClientId" }; + + var redirectUriStore = new Mock>(); + + var validator = new Mock>(); + validator.Setup(s => s.ValidateRedirectUriAsync(It.IsAny>(), application, It.IsAny())) + .ReturnsAsync(IdentityServiceResult.Failed(new IdentityServiceError())); + + var applicationManager = GetApplicationManager(redirectUriStore.Object, application, validator.Object); + + // Act + var result = await applicationManager.RegisterRedirectUriAsync(application, "https://www.example.com/sign-in"); + + // Assert + Assert.False(result.Succeeded); + redirectUriStore.VerifyAll(); + } + + [Fact] + public async Task UpdateRedirectUriCallsStore() + { + // Arrange + var application = new TestApplication { Name = "Application", ClientId = "ClientId" }; + + var store = new Mock>(); + store.Setup(s => s.UpdateAsync(application, It.IsAny())) + .ReturnsAsync(IdentityServiceResult.Success); + + var redirectUriStore = store.As>(); + redirectUriStore.Setup(s => s.FindRegisteredUrisAsync(application, It.IsAny())) + .ReturnsAsync(new[] { "https://www.example.com/sign-in" }); + + redirectUriStore.Setup(s => s.UpdateRedirectUriAsync(application, It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(IdentityServiceResult.Success); + + var applicationManager = GetApplicationManager(redirectUriStore.Object, application); + + // Act + var result = await applicationManager.UpdateRedirectUriAsync(application, "https://www.example.com/sign-in", "https://www.example.com/signin"); + + // Assert + Assert.True(result.Succeeded); + store.VerifyAll(); + } + + [Fact] + public async Task UpdateRedirectUriFailsIfItDoesNotFindTheUriToUpdate() + { + // Arrange + var application = new TestApplication { Name = "Application", ClientId = "ClientId" }; + var expectedError = new[] { ErrorDescriber.RedirectUriNotFound("https://www.example.com/sign-in") }; + + var store = new Mock>(); + var redirectUriStore = store.As>(); + redirectUriStore.Setup(s => s.FindRegisteredUrisAsync(application, It.IsAny())) + .ReturnsAsync(new string[] { }); + + var applicationManager = GetApplicationManager(redirectUriStore.Object, application); + + // Act + var result = await applicationManager.UpdateRedirectUriAsync(application, "https://www.example.com/sign-in", "https://www.example.com/signin"); + + // Assert + Assert.False(result.Succeeded); + Assert.Equal(expectedError, result.Errors, ErrorsComparer.Instance); + store.VerifyAll(); + } + + [Fact] + public async Task UpdateRedirectUriValidatorsCanBlockUpdate() + { + // Arrange + var application = new TestApplication { Name = "Application", ClientId = "ClientId" }; + + var redirectUriStore = new Mock>(); + redirectUriStore.Setup(s => s.FindRegisteredUrisAsync(application, It.IsAny())) + .ReturnsAsync(new[] { "https://www.example.com/sign-in" }); + + var validator = new Mock>(); + validator.Setup(s => s.ValidateRedirectUriAsync(It.IsAny>(), application, It.IsAny())) + .ReturnsAsync(IdentityServiceResult.Failed(new IdentityServiceError())); + + var applicationManager = GetApplicationManager(redirectUriStore.Object, application, validator.Object); + + // Act + var result = await applicationManager.UpdateRedirectUriAsync(application, "https://www.example.com/sign-in", "https://www.example.com/signin"); + + // Assert + Assert.False(result.Succeeded); + redirectUriStore.VerifyAll(); + } + + [Fact] + public async Task UnregisterRedirectUriCallsStore() + { + // Arrange + var application = new TestApplication { Name = "Application", ClientId = "ClientId" }; + + var store = new Mock>(); + store.Setup(s => s.UpdateAsync(application, It.IsAny())) + .ReturnsAsync(IdentityServiceResult.Success); + + var redirectUriStore = store.As>(); + redirectUriStore.Setup(s => s.FindRegisteredUrisAsync(application, It.IsAny())) + .ReturnsAsync(new[] { "https://www.example.com/sign-in" }); + + redirectUriStore.Setup(s => s.UnregisterRedirectUriAsync(application, It.IsAny(), It.IsAny())) + .ReturnsAsync(IdentityServiceResult.Success); + + var applicationManager = GetApplicationManager(redirectUriStore.Object, application); + + // Act + var result = await applicationManager.UnregisterRedirectUriAsync(application, "https://www.example.com/sign-in"); + + // Assert + Assert.True(result.Succeeded); + store.VerifyAll(); + } + + [Fact] + public async Task UnregisterRedirectUriFailsIfItDoesNotFindTheUriToUpdate() + { + // Arrange + var application = new TestApplication { Name = "Application", ClientId = "ClientId" }; + var expectedError = new[] { ErrorDescriber.RedirectUriNotFound("https://www.example.com/sign-in") }; + + var store = new Mock>(); + var redirectUriStore = store.As>(); + redirectUriStore.Setup(s => s.FindRegisteredUrisAsync(application, It.IsAny())) + .ReturnsAsync(new string[] { }); + + var applicationManager = GetApplicationManager(redirectUriStore.Object, application); + + // Act + var result = await applicationManager.UnregisterRedirectUriAsync(application, "https://www.example.com/sign-in"); + + // Assert + Assert.False(result.Succeeded); + Assert.Equal(expectedError, result.Errors, ErrorsComparer.Instance); + store.VerifyAll(); + } + + [Fact] + public async Task FindRegisteredLogoutUrisCallsStore() + { + // Arrange + var application = new TestApplication { Name = "Application", ClientId = "ClientId" }; + + var store = new Mock>(); + + store.As>() + .Setup(s => s.FindRegisteredLogoutUrisAsync(application, It.IsAny())) + .ReturnsAsync(new[] { "https://www.example.com/sign-in" }); + + var applicationManager = GetApplicationManager(store.Object, application); + + // Act + var result = await applicationManager.FindRegisteredLogoutUrisAsync(application); + + // Assert + Assert.Equal(new[] { "https://www.example.com/sign-in" }, result); + store.VerifyAll(); + } + + [Fact] + public async Task RegisterLogoutUriCallsStore() + { + // Arrange + var application = new TestApplication { Name = "Application", ClientId = "ClientId" }; + + var store = new Mock>(); + store.Setup(s => s.UpdateAsync(application, It.IsAny())) + .ReturnsAsync(IdentityServiceResult.Success); + + var logoutUriStore = store.As>(); + logoutUriStore.Setup(s => s.RegisterLogoutRedirectUriAsync(application, It.IsAny(), It.IsAny())) + .ReturnsAsync(IdentityServiceResult.Success); + + var applicationManager = GetApplicationManager(logoutUriStore.Object, application); + + // Act + var result = await applicationManager.RegisterLogoutUriAsync(application, "https://www.example.com/sign-in"); + + // Assert + Assert.True(result.Succeeded); + logoutUriStore.VerifyAll(); + } + + [Fact] + public async Task RegisterLogoutUriValidatorsCanBlockUpdate() + { + // Arrange + var application = new TestApplication { Name = "Application", ClientId = "ClientId" }; + + var logoutUriStore = new Mock>(); + + var validator = new Mock>(); + validator.Setup(s => s.ValidateLogoutUriAsync(It.IsAny>(), application, It.IsAny())) + .ReturnsAsync(IdentityServiceResult.Failed(new IdentityServiceError())); + + var applicationManager = GetApplicationManager(logoutUriStore.Object, application, validator.Object); + + // Act + var result = await applicationManager.RegisterLogoutUriAsync(application, "https://www.example.com/sign-in"); + + // Assert + Assert.False(result.Succeeded); + logoutUriStore.VerifyAll(); + } + + [Fact] + public async Task UpdateLogoutUriCallsStore() + { + // Arrange + var application = new TestApplication { Name = "Application", ClientId = "ClientId" }; + + var store = new Mock>(); + store.Setup(s => s.UpdateAsync(application, It.IsAny())) + .ReturnsAsync(IdentityServiceResult.Success); + + var logoutUriStore = store.As>(); + logoutUriStore.Setup(s => s.FindRegisteredLogoutUrisAsync(application, It.IsAny())) + .ReturnsAsync(new[] { "https://www.example.com/sign-in" }); + + logoutUriStore.Setup(s => s.UpdateLogoutRedirectUriAsync(application, It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(IdentityServiceResult.Success); + + var applicationManager = GetApplicationManager(logoutUriStore.Object, application); + + // Act + var result = await applicationManager.UpdateLogoutUriAsync(application, "https://www.example.com/sign-in", "https://www.example.com/signin"); + + // Assert + Assert.True(result.Succeeded); + store.VerifyAll(); + } + + [Fact] + public async Task UpdateLogoutUriFailsIfItDoesNotFindTheUriToUpdate() + { + // Arrange + var application = new TestApplication { Name = "Application", ClientId = "ClientId" }; + var expectedError = new[] { ErrorDescriber.LogoutUriNotFound("https://www.example.com/sign-in") }; + + var store = new Mock>(); + var logoutUriStore = store.As>(); + logoutUriStore.Setup(s => s.FindRegisteredLogoutUrisAsync(application, It.IsAny())) + .ReturnsAsync(new string[] { }); + + var applicationManager = GetApplicationManager(logoutUriStore.Object, application); + + // Act + var result = await applicationManager.UpdateLogoutUriAsync(application, "https://www.example.com/sign-in", "https://www.example.com/signin"); + + // Assert + Assert.False(result.Succeeded); + Assert.Equal(expectedError, result.Errors, ErrorsComparer.Instance); + store.VerifyAll(); + } + + [Fact] + public async Task UpdateLogoutUriValidatorsCanBlockUpdate() + { + // Arrange + var application = new TestApplication { Name = "Application", ClientId = "ClientId" }; + + var logoutUriStore = new Mock>(); + logoutUriStore.Setup(s => s.FindRegisteredLogoutUrisAsync(application, It.IsAny())) + .ReturnsAsync(new[] { "https://www.example.com/sign-in" }); + + var validator = new Mock>(); + validator.Setup(s => s.ValidateLogoutUriAsync(It.IsAny>(), application, It.IsAny())) + .ReturnsAsync(IdentityServiceResult.Failed(new IdentityServiceError())); + + var applicationManager = GetApplicationManager(logoutUriStore.Object, application, validator.Object); + + // Act + var result = await applicationManager.UpdateLogoutUriAsync(application, "https://www.example.com/sign-in", "https://www.example.com/signin"); + + // Assert + Assert.False(result.Succeeded); + logoutUriStore.VerifyAll(); + } + + [Fact] + public async Task UnregisterLogoutUriCallsStore() + { + // Arrange + var application = new TestApplication { Name = "Application", ClientId = "ClientId" }; + + var store = new Mock>(); + store.Setup(s => s.UpdateAsync(application, It.IsAny())) + .ReturnsAsync(IdentityServiceResult.Success); + + var logoutUriStore = store.As>(); + logoutUriStore.Setup(s => s.FindRegisteredLogoutUrisAsync(application, It.IsAny())) + .ReturnsAsync(new[] { "https://www.example.com/sign-in" }); + + logoutUriStore.Setup(s => s.UnregisterLogoutRedirectUriAsync(application, It.IsAny(), It.IsAny())) + .ReturnsAsync(IdentityServiceResult.Success); + + var applicationManager = GetApplicationManager(logoutUriStore.Object, application); + + // Act + var result = await applicationManager.UnregisterLogoutUriAsync(application, "https://www.example.com/sign-in"); + + // Assert + Assert.True(result.Succeeded); + store.VerifyAll(); + } + + [Fact] + public async Task UnregisterLogoutUriFailsIfItDoesNotFindTheUriToUpdate() + { + // Arrange + var application = new TestApplication { Name = "Application", ClientId = "ClientId" }; + var expectedError = new[] { ErrorDescriber.LogoutUriNotFound("https://www.example.com/sign-in") }; + + var store = new Mock>(); + var logoutUriStore = store.As>(); + logoutUriStore.Setup(s => s.FindRegisteredLogoutUrisAsync(application, It.IsAny())) + .ReturnsAsync(new string[] { }); + + var applicationManager = GetApplicationManager(logoutUriStore.Object, application); + + // Act + var result = await applicationManager.UnregisterLogoutUriAsync(application, "https://www.example.com/sign-in"); + + // Assert + Assert.False(result.Succeeded); + Assert.Equal(expectedError, result.Errors, ErrorsComparer.Instance); + store.VerifyAll(); + } + + [Fact] + public async Task FindScopesAsyncCallsStore() + { + // Arrange + var application = new TestApplication { Name = "Application", ClientId = "ClientId" }; + + var store = new Mock>(); + store.Setup(s => s.FindScopesAsync(application, It.IsAny())) + .ReturnsAsync(new[] { "openid" }); + + var applicationManager = GetApplicationManager(store.Object, application); + + // Act + var scopes = await applicationManager.FindScopesAsync(application); + + // Assert + Assert.Equal(new[] { "openid" }, scopes); + } + + [Fact] + public async Task AddScopeCallsStore() + { + // Arrange + var application = new TestApplication { Name = "Application", ClientId = "ClientId" }; + + var store = new Mock>(); + store.Setup(s => s.UpdateAsync(application, It.IsAny())) + .ReturnsAsync(IdentityServiceResult.Success); + + var scopeStore = store.As>(); + scopeStore.Setup(s => s.AddScopeAsync(application, It.IsAny(), It.IsAny())) + .ReturnsAsync(IdentityServiceResult.Success); + + var applicationManager = GetApplicationManager(scopeStore.Object, application); + + // Act + var result = await applicationManager.AddScopeAsync(application, "https://www.example.com/sign-in"); + + // Assert + Assert.True(result.Succeeded); + scopeStore.VerifyAll(); + } + + [Fact] + public async Task AddScopeValidatorsCanBlockUpdate() + { + // Arrange + var application = new TestApplication { Name = "Application", ClientId = "ClientId" }; + + var scopeStore = new Mock>(); + + var validator = new Mock>(); + validator.Setup(s => s.ValidateScopeAsync(It.IsAny>(), application, It.IsAny())) + .ReturnsAsync(IdentityServiceResult.Failed(new IdentityServiceError())); + + var applicationManager = GetApplicationManager(scopeStore.Object, application, validator.Object); + + // Act + var result = await applicationManager.AddScopeAsync(application, "openid"); + + // Assert + Assert.False(result.Succeeded); + scopeStore.VerifyAll(); + } + + [Fact] + public async Task UpdateScopeCallsStore() + { + // Arrange + var application = new TestApplication { Name = "Application", ClientId = "ClientId" }; + + var store = new Mock>(); + store.Setup(s => s.UpdateAsync(application, It.IsAny())) + .ReturnsAsync(IdentityServiceResult.Success); + + var scopeStore = store.As>(); + scopeStore.Setup(s => s.FindScopesAsync(application, It.IsAny())) + .ReturnsAsync(new[] { "openid" }); + + scopeStore.Setup(s => s.UpdateScopeAsync(application, It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(IdentityServiceResult.Success); + + var applicationManager = GetApplicationManager(scopeStore.Object, application); + + // Act + var result = await applicationManager.UpdateScopeAsync(application, "openid", "offline_access"); + + // Assert + Assert.True(result.Succeeded); + store.VerifyAll(); + } + + [Fact] + public async Task UpdateScopeFailsIfItDoesNotFindTheUriToUpdate() + { + // Arrange + var application = new TestApplication { Name = "Application", ClientId = "ClientId" }; + var expectedError = new[] { ErrorDescriber.ScopeNotFound("openid") }; + + var store = new Mock>(); + var scopeStore = store.As>(); + scopeStore.Setup(s => s.FindScopesAsync(application, It.IsAny())) + .ReturnsAsync(new string[] { }); + + var applicationManager = GetApplicationManager(scopeStore.Object, application); + + // Act + var result = await applicationManager.UpdateScopeAsync(application, "openid", "offline_access"); + + // Assert + Assert.False(result.Succeeded); + Assert.Equal(expectedError, result.Errors, ErrorsComparer.Instance); + store.VerifyAll(); + } + + [Fact] + public async Task UpdateScopeValidatorsCanBlockUpdate() + { + // Arrange + var application = new TestApplication { Name = "Application", ClientId = "ClientId" }; + + var scopeStore = new Mock>(); + scopeStore.Setup(s => s.FindScopesAsync(application, It.IsAny())) + .ReturnsAsync(new[] { "openid" }); + + var validator = new Mock>(); + validator.Setup(s => s.ValidateScopeAsync(It.IsAny>(), application, It.IsAny())) + .ReturnsAsync(IdentityServiceResult.Failed(new IdentityServiceError())); + + var applicationManager = GetApplicationManager(scopeStore.Object, application, validator.Object); + + // Act + var result = await applicationManager.UpdateScopeAsync(application, "openid", "offline_access"); + + // Assert + Assert.False(result.Succeeded); + scopeStore.VerifyAll(); + } + + [Fact] + public async Task RemoveScopeCallsStore() + { + // Arrange + var application = new TestApplication { Name = "Application", ClientId = "ClientId" }; + + var store = new Mock>(); + store.Setup(s => s.UpdateAsync(application, It.IsAny())) + .ReturnsAsync(IdentityServiceResult.Success); + + var scopeStore = store.As>(); + scopeStore.Setup(s => s.FindScopesAsync(application, It.IsAny())) + .ReturnsAsync(new[] { "openid" }); + + scopeStore.Setup(s => s.RemoveScopeAsync(application, It.IsAny(), It.IsAny())) + .ReturnsAsync(IdentityServiceResult.Success); + + var applicationManager = GetApplicationManager(scopeStore.Object, application); + + // Act + var result = await applicationManager.RemoveScopeAsync(application, "openid"); + + // Assert + Assert.True(result.Succeeded); + store.VerifyAll(); + } + + [Fact] + public async Task RemoveScopeFailsIfItDoesNotFindTheUriToUpdate() + { + // Arrange + var application = new TestApplication { Name = "Application", ClientId = "ClientId" }; + var expectedError = new[] { ErrorDescriber.ScopeNotFound("openid") }; + + var store = new Mock>(); + var scopeStore = store.As>(); + scopeStore.Setup(s => s.FindScopesAsync(application, It.IsAny())) + .ReturnsAsync(new string[] { }); + + var applicationManager = GetApplicationManager(scopeStore.Object, application); + + // Act + var result = await applicationManager.RemoveScopeAsync(application, "openid"); + + // Assert + Assert.False(result.Succeeded); + Assert.Equal(expectedError, result.Errors, ErrorsComparer.Instance); + store.VerifyAll(); + } + + [Fact] + public async Task FindClaimsAsyncCallsStore() + { + // Arrange + var application = new TestApplication { Name = "Application", ClientId = "ClientId" }; + + var store = new Mock>(); + var expectedClaims = new[] { new Claim("type", "value") }; + store.Setup(s => s.GetClaimsAsync(application, It.IsAny())) + .ReturnsAsync(expectedClaims); + + var applicationManager = GetApplicationManager(store.Object, application); + + // Act + var claims = await applicationManager.GetClaimsAsync(application); + + // Assert + Assert.Equal(expectedClaims, claims); + } + + [Fact] + public async Task AddClaimCallsStore() + { + // Arrange + var application = new TestApplication { Name = "Application", ClientId = "ClientId" }; + + var store = new Mock>(); + store.Setup(s => s.UpdateAsync(application, It.IsAny())) + .ReturnsAsync(IdentityServiceResult.Success); + + var claimsStore = store.As>(); + claimsStore.Setup(s => s.AddClaimsAsync(application, It.IsAny>(), It.IsAny())) + .Returns(Task.CompletedTask); + + var applicationManager = GetApplicationManager(claimsStore.Object, application); + + // Act + var result = await applicationManager.AddClaimAsync(application, new Claim("type", "value")); + + // Assert + Assert.True(result.Succeeded); + claimsStore.VerifyAll(); + } + + [Fact] + public async Task AddClaimValidatorsCanBlockUpdate() + { + // Arrange + var application = new TestApplication { Name = "Application", ClientId = "ClientId" }; + + var claimsStore = new Mock>(); + + var validator = new Mock>(); + validator.Setup(s => s.ValidateClaimAsync(It.IsAny>(), application, It.IsAny())) + .ReturnsAsync(IdentityServiceResult.Failed(new IdentityServiceError())); + + var applicationManager = GetApplicationManager(claimsStore.Object, application, validator.Object); + + // Act + var result = await applicationManager.AddClaimAsync(application, new Claim("type","value")); + + // Assert + Assert.False(result.Succeeded); + claimsStore.VerifyAll(); + } + + [Fact] + public async Task ReplaceClaimCallsStore() + { + // Arrange + var application = new TestApplication { Name = "Application", ClientId = "ClientId" }; + + var store = new Mock>(); + store.Setup(s => s.UpdateAsync(application, It.IsAny())) + .ReturnsAsync(IdentityServiceResult.Success); + + var claimsStore = store.As>(); + claimsStore.Setup(s => s.ReplaceClaimAsync(application, It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(Task.CompletedTask); + + var applicationManager = GetApplicationManager(claimsStore.Object, application); + + // Act + var result = await applicationManager.ReplaceClaimAsync(application, new Claim("type", "value"), new Claim("new-type", "new-value")); + + // Assert + Assert.True(result.Succeeded); + store.VerifyAll(); + } + + [Fact] + public async Task ReplaceClaimValidatorsCanBlockUpdate() + { + // Arrange + var application = new TestApplication { Name = "Application", ClientId = "ClientId" }; + + var claimsStore = new Mock>(); + + var validator = new Mock>(); + validator.Setup(s => s.ValidateClaimAsync(It.IsAny>(), application, It.IsAny())) + .ReturnsAsync(IdentityServiceResult.Failed(new IdentityServiceError())); + + var applicationManager = GetApplicationManager(claimsStore.Object, application, validator.Object); + + // Act + var result = await applicationManager.ReplaceClaimAsync(application, new Claim("type", "value"), new Claim("new-type", "new-value")); + + // Assert + Assert.False(result.Succeeded); + claimsStore.VerifyAll(); + } + + [Fact] + public async Task RemoveClaimCallsStore() + { + // Arrange + var application = new TestApplication { Name = "Application", ClientId = "ClientId" }; + + var store = new Mock>(); + store.Setup(s => s.UpdateAsync(application, It.IsAny())) + .ReturnsAsync(IdentityServiceResult.Success); + + var claimsStore = store.As>(); + + claimsStore.Setup(s => s.RemoveClaimsAsync(application, It.IsAny>(), It.IsAny())) + .Returns(Task.CompletedTask); + + var applicationManager = GetApplicationManager(claimsStore.Object, application); + + // Act + var result = await applicationManager.RemoveClaimAsync(application, new Claim("type", "value")); + + // Assert + Assert.True(result.Succeeded); + store.VerifyAll(); + } + + private ApplicationManager GetApplicationManager( + IApplicationStore store, + TestApplication application, + IApplicationValidator validator = null, + PasswordVerificationResult hashResult = PasswordVerificationResult.Success) + { + var hasher = new Mock>(); + hasher.Setup(s => s.HashPassword(application, "client-secret")) + .Returns("new-hash"); + + hasher.Setup(s => s.VerifyHashedPassword(application, It.IsAny(), It.IsAny())) + .Returns(hashResult); + + return new ApplicationManager( + Options.Create(new ApplicationOptions()), + store, + hasher.Object, + validator == null ? Enumerable.Empty>() : new List> { validator }, + 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 Name { get; internal set; } + public string ClientId { get; internal set; } + public List RedirectUris { get; internal set; } + public List LogoutUris { get; internal set; } + public List Scopes { get; internal set; } + } + } +} diff --git a/test/Microsoft.AspNetCore.Identity.Service.Test/ApplicationValidatorTest.cs b/test/Microsoft.AspNetCore.Identity.Service.Test/ApplicationValidatorTest.cs new file mode 100644 index 0000000000..a43a01c3f4 --- /dev/null +++ b/test/Microsoft.AspNetCore.Identity.Service.Test/ApplicationValidatorTest.cs @@ -0,0 +1,525 @@ +// 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; } + } + } +} diff --git a/test/Microsoft.AspNetCore.Identity.Service.Test/ClientApplicationValidatorTest.cs b/test/Microsoft.AspNetCore.Identity.Service.Test/ClientApplicationValidatorTest.cs index bcb623409e..7c5312111c 100644 --- a/test/Microsoft.AspNetCore.Identity.Service.Test/ClientApplicationValidatorTest.cs +++ b/test/Microsoft.AspNetCore.Identity.Service.Test/ClientApplicationValidatorTest.cs @@ -28,10 +28,12 @@ namespace Microsoft.AspNetCore.Identity.Service .ReturnsAsync(exists ? new IdentityServiceApplication() : null); var manager = new ApplicationManager( + Options.Create(new ApplicationOptions()), store.Object, Mock.Of>(), Array.Empty>(), - Mock.Of>>()); + Mock.Of>>(), + new ApplicationErrorDescriber()); var clientValidator = new ClientApplicationValidator( Options.Create(options), @@ -59,10 +61,12 @@ namespace Microsoft.AspNetCore.Identity.Service .ReturnsAsync(false); var manager = new ApplicationManager( + Options.Create(new ApplicationOptions()), store.Object, Mock.Of>(), Array.Empty>(), - Mock.Of>>()); + Mock.Of>>(), + new ApplicationErrorDescriber()); var clientValidator = new ClientApplicationValidator( Options.Create(options),