diff --git a/samples/Mvc.Client/Startup.cs b/samples/Mvc.Client/Startup.cs index 036b083d..8ad3992d 100644 --- a/samples/Mvc.Client/Startup.cs +++ b/samples/Mvc.Client/Startup.cs @@ -31,8 +31,8 @@ namespace Mvc.Client { app.UseOpenIdConnectAuthentication(new OpenIdConnectOptions { // Note: these settings must match the application details // inserted in the database at the server level. - ClientId = "myClient", - ClientSecret = "secret_secret_secret", + ClientId = "mvc", + ClientSecret = "901564A5-E7FE-42CB-B10D-61EF6A8F3654", PostLogoutRedirectUri = "http://localhost:53507/", RequireHttpsMetadata = false, diff --git a/samples/Mvc.Server/Startup.cs b/samples/Mvc.Server/Startup.cs index 15ff9374..d4b79807 100644 --- a/samples/Mvc.Server/Startup.cs +++ b/samples/Mvc.Server/Startup.cs @@ -1,5 +1,6 @@ -using System.Linq; -using CryptoHelper; +using System; +using System.Threading; +using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Identity.EntityFrameworkCore; using Microsoft.EntityFrameworkCore; @@ -150,50 +151,47 @@ namespace Mvc.Server { app.UseMvcWithDefaultRoute(); - using (var context = new ApplicationDbContext( - app.ApplicationServices.GetRequiredService>())) { - context.Database.EnsureCreated(); - - var applications = context.Set(); - - // Add Mvc.Client to the known applications. - if (!applications.Any()) { - // Note: when using the introspection middleware, your resource server - // MUST be registered as an OAuth2 client and have valid credentials. - // - // context.Applications.Add(new OpenIddictApplication { - // Id = "resource_server", - // DisplayName = "Main resource server", - // Secret = Crypto.HashPassword("secret_secret_secret"), - // Type = OpenIddictConstants.ClientTypes.Confidential - // }); - - applications.Add(new OpenIddictApplication { - ClientId = "myClient", - ClientSecret = Crypto.HashPassword("secret_secret_secret"), - DisplayName = "My client application", + // Seed the database with the sample applications. + // Note: in a real world application, this step should be part of a setup script. + InitializeAsync(app.ApplicationServices, CancellationToken.None).GetAwaiter().GetResult(); + } + + private async Task InitializeAsync(IServiceProvider services, CancellationToken cancellationToken) { + // Create a new service scope to ensure the database context is correctly disposed when this methods returns. + using (var scope = services.GetRequiredService().CreateScope()) { + var context = scope.ServiceProvider.GetRequiredService(); + await context.Database.EnsureCreatedAsync(); + + var manager = scope.ServiceProvider.GetRequiredService>(); + + if (await manager.FindByClientIdAsync("mvc", cancellationToken) == null) { + var application = new OpenIddictApplication { + ClientId = "mvc", + DisplayName = "MVC client application", LogoutRedirectUri = "http://localhost:53507/", - RedirectUri = "http://localhost:53507/signin-oidc", - Type = OpenIddictConstants.ClientTypes.Confidential - }); - - // To test this sample with Postman, use the following settings: - // - // * Authorization URL: http://localhost:54540/connect/authorize - // * Access token URL: http://localhost:54540/connect/token - // * Client ID: postman - // * Client secret: [blank] (not used with public clients) - // * Scope: openid email profile roles - // * Grant type: authorization code - // * Request access token locally: yes - applications.Add(new OpenIddictApplication { + RedirectUri = "http://localhost:53507/signin-oidc" + }; + + await manager.CreateAsync(application, "901564A5-E7FE-42CB-B10D-61EF6A8F3654", cancellationToken); + } + + // To test this sample with Postman, use the following settings: + // + // * Authorization URL: http://localhost:54540/connect/authorize + // * Access token URL: http://localhost:54540/connect/token + // * Client ID: postman + // * Client secret: [blank] (not used with public clients) + // * Scope: openid email profile roles + // * Grant type: authorization code + // * Request access token locally: yes + if (await manager.FindByClientIdAsync("postman", cancellationToken) == null) { + var application = new OpenIddictApplication { ClientId = "postman", DisplayName = "Postman", - RedirectUri = "https://www.getpostman.com/oauth2/callback", - Type = OpenIddictConstants.ClientTypes.Public - }); + RedirectUri = "https://www.getpostman.com/oauth2/callback" + }; - context.SaveChanges(); + await manager.CreateAsync(application, cancellationToken); } } } diff --git a/src/OpenIddict.Core/Managers/OpenIddictApplicationManager.cs b/src/OpenIddict.Core/Managers/OpenIddictApplicationManager.cs index 93c37c2f..8fd14b0b 100644 --- a/src/OpenIddict.Core/Managers/OpenIddictApplicationManager.cs +++ b/src/OpenIddict.Core/Managers/OpenIddictApplicationManager.cs @@ -44,12 +44,67 @@ namespace OpenIddict.Core { /// A that can be used to monitor the asynchronous operation, /// whose result returns the unique identifier associated with the application. /// - public virtual Task CreateAsync(TApplication application, CancellationToken cancellationToken) { + public virtual async Task CreateAsync([NotNull] TApplication application, CancellationToken cancellationToken) { if (application == null) { throw new ArgumentNullException(nameof(application)); } - return Store.CreateAsync(application, cancellationToken); + if (!string.IsNullOrEmpty(await Store.GetHashedSecretAsync(application, cancellationToken))) { + throw new ArgumentException("The client secret hash cannot be directly set on the application entity. " + + "To create a confidential application, use the CreateAsync() overload accepting a secret parameter."); + } + + // If no client type was specified, assume it's a public application. + if (string.IsNullOrEmpty(await Store.GetClientTypeAsync(application, cancellationToken))) { + await Store.SetClientTypeAsync(application, OpenIddictConstants.ClientTypes.Public, cancellationToken); + } + + await ValidateAsync(application, cancellationToken); + return await Store.CreateAsync(application, cancellationToken); + } + + /// + /// Creates a new confidential application, using the specified client secret. + /// + /// The application to create. + /// The client secret associated with the application. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the unique identifier associated with the application. + /// + public virtual async Task CreateAsync( + [NotNull] TApplication application, + [NotNull] string secret, CancellationToken cancellationToken) { + if (application == null) { + throw new ArgumentNullException(nameof(application)); + } + + if (!string.IsNullOrEmpty(await Store.GetHashedSecretAsync(application, cancellationToken))) { + throw new ArgumentException("The client secret hash cannot be directly set on the application entity."); + } + + await Store.SetClientTypeAsync(application, OpenIddictConstants.ClientTypes.Confidential, cancellationToken); + await Store.SetHashedSecretAsync(application, Crypto.HashPassword(secret), cancellationToken); + await ValidateAsync(application, cancellationToken); + + return await Store.CreateAsync(application, cancellationToken); + } + + /// + /// Removes an existing application. + /// + /// The application to delete. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public virtual Task DeleteAsync([NotNull] TApplication application, CancellationToken cancellationToken) { + if (application == null) { + throw new ArgumentNullException(nameof(application)); + } + + return Store.DeleteAsync(application, cancellationToken); } /// @@ -100,7 +155,7 @@ namespace OpenIddict.Core { /// A that can be used to monitor the asynchronous operation, /// whose result returns the client type of the application (by default, "public"). /// - public virtual async Task GetClientTypeAsync(TApplication application, CancellationToken cancellationToken) { + public virtual async Task GetClientTypeAsync([NotNull] TApplication application, CancellationToken cancellationToken) { if (application == null) { throw new ArgumentNullException(nameof(application)); } @@ -126,7 +181,7 @@ namespace OpenIddict.Core { /// A that can be used to monitor the asynchronous operation, /// whose result returns the display name associated with the application. /// - public virtual Task GetDisplayNameAsync(TApplication application, CancellationToken cancellationToken) { + public virtual Task GetDisplayNameAsync([NotNull] TApplication application, CancellationToken cancellationToken) { if (application == null) { throw new ArgumentNullException(nameof(application)); } @@ -143,13 +198,48 @@ namespace OpenIddict.Core { /// A that can be used to monitor the asynchronous operation, /// whose result returns the tokens associated with the application. /// - public virtual Task> GetTokensAsync(TApplication application, CancellationToken cancellationToken) { + public virtual Task> GetTokensAsync([NotNull] TApplication application, CancellationToken cancellationToken) { if (application == null) { throw new ArgumentNullException(nameof(application)); } return Store.GetTokensAsync(application, cancellationToken); } + + /// + /// Determines whether the specified application has a redirect_uri. + /// + /// The application. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns a boolean indicating whether a redirect_uri is registered. + /// + public virtual async Task HasRedirectUriAsync([NotNull] TApplication application, CancellationToken cancellationToken) { + if (application == null) { + throw new ArgumentNullException(nameof(application)); + } + + return !string.IsNullOrEmpty(await Store.GetRedirectUriAsync(application, cancellationToken)); + } + + /// + /// Determines whether the specified application has a client secret. + /// + /// The application. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns a boolean indicating whether a client secret is registered. + /// + public virtual async Task HasClientSecretAsync([NotNull] TApplication application, CancellationToken cancellationToken) { + if (application == null) { + throw new ArgumentNullException(nameof(application)); + } + + return !string.IsNullOrEmpty(await Store.GetHashedSecretAsync(application, cancellationToken)); + } + /// /// Determines whether an application is a confidential client. /// @@ -175,7 +265,7 @@ namespace OpenIddict.Core { /// The application. /// The that can be used to abort the operation. /// true if the application is a public client, false otherwise. - public async Task IsPublicAsync(TApplication application, CancellationToken cancellationToken) { + public async Task IsPublicAsync([NotNull] TApplication application, CancellationToken cancellationToken) { if (application == null) { throw new ArgumentNullException(nameof(application)); } @@ -189,6 +279,105 @@ namespace OpenIddict.Core { return string.Equals(type, OpenIddictConstants.ClientTypes.Public, StringComparison.OrdinalIgnoreCase); } + /// + /// Updates the client secret associated with an application. + /// + /// The application. + /// The client secret associated with the application. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public virtual async Task SetClientSecretAsync([NotNull] TApplication application, [NotNull] string secret, CancellationToken cancellationToken) { + if (application == null) { + throw new ArgumentNullException(nameof(application)); + } + + if (string.IsNullOrEmpty(secret)) { + throw new ArgumentException("The client secret cannot be null or empty.", nameof(secret)); + } + + await Store.SetHashedSecretAsync(application, Crypto.HashPassword(secret), cancellationToken); + await UpdateAsync(application, cancellationToken); + } + + /// + /// Updates an existing application. + /// + /// The application to update. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public virtual async Task UpdateAsync([NotNull] TApplication application, CancellationToken cancellationToken) { + if (application == null) { + throw new ArgumentNullException(nameof(application)); + } + + await ValidateAsync(application, cancellationToken); + await Store.UpdateAsync(application, cancellationToken); + } + + /// + /// Validates the application to ensure it's in a consistent state. + /// + /// The application. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation. + /// + protected virtual async Task ValidateAsync([NotNull] TApplication application, CancellationToken cancellationToken) { + if (application == null) { + throw new ArgumentNullException(nameof(application)); + } + + if (string.IsNullOrEmpty(await Store.GetClientIdAsync(application, cancellationToken))) { + throw new ArgumentException("The client identifier cannot be null or empty.", nameof(application)); + } + + var type = await Store.GetClientTypeAsync(application, cancellationToken); + if (string.IsNullOrEmpty(type)) { + throw new ArgumentException("The client type cannot be null or empty.", nameof(application)); + } + + // Ensure the application type is supported by the manager. + if (!string.Equals(type, OpenIddictConstants.ClientTypes.Confidential, StringComparison.OrdinalIgnoreCase) && + !string.Equals(type, OpenIddictConstants.ClientTypes.Public, StringComparison.OrdinalIgnoreCase)) { + throw new ArgumentException("Only 'confidential' or 'public' applications are " + + "supported by the default application manager.", nameof(application)); + } + + var hash = await Store.GetHashedSecretAsync(application, cancellationToken); + + // Ensure a client secret was specified if the client is a confidential application. + if (string.IsNullOrEmpty(hash) && + string.Equals(type, OpenIddictConstants.ClientTypes.Confidential, StringComparison.OrdinalIgnoreCase)) { + throw new ArgumentException("The client secret cannot be null or empty for a confidential application.", nameof(application)); + } + + // Ensure no client secret was specified if the client is a public application. + else if (!string.IsNullOrEmpty(hash) && + string.Equals(type, OpenIddictConstants.ClientTypes.Public, StringComparison.OrdinalIgnoreCase)) { + throw new ArgumentException("A client secret cannot be associated with a public application.", nameof(application)); + } + + // When a redirect_uri is specified, ensure it is valid and spec-compliant. + // See https://tools.ietf.org/html/rfc6749#section-3.1 for more information. + var address = await Store.GetRedirectUriAsync(application, cancellationToken); + if (!string.IsNullOrEmpty(address)) { + Uri uri; + // Ensure the redirect_uri is a valid and absolute URL. + if (!Uri.TryCreate(address, UriKind.Absolute, out uri)) { + throw new ArgumentException("The redirect_uri must be an absolute URL."); + } + + // Ensure the redirect_uri doesn't contain a fragment. + if (!string.IsNullOrEmpty(uri.Fragment)) { + throw new ArgumentException("The redirect_uri cannot contain a fragment."); + } + } + } + /// /// Validates the redirect_uri associated with an application. /// @@ -199,7 +388,7 @@ namespace OpenIddict.Core { /// A that can be used to monitor the asynchronous operation, /// whose result returns a boolean indicating whether the redirect_uri was valid. /// - public virtual async Task ValidateRedirectUriAsync(TApplication application, string address, CancellationToken cancellationToken) { + public virtual async Task ValidateRedirectUriAsync([NotNull] TApplication application, string address, CancellationToken cancellationToken) { if (application == null) { throw new ArgumentNullException(nameof(application)); } @@ -225,7 +414,7 @@ namespace OpenIddict.Core { /// A that can be used to monitor the asynchronous operation, /// whose result returns a boolean indicating whether the client secret was valid. /// - public virtual async Task ValidateSecretAsync(TApplication application, string secret, CancellationToken cancellationToken) { + public virtual async Task ValidateClientSecretAsync([NotNull] TApplication application, string secret, CancellationToken cancellationToken) { if (application == null) { throw new ArgumentNullException(nameof(application)); } diff --git a/src/OpenIddict.Core/Stores/IOpenIddictApplicationStore.cs b/src/OpenIddict.Core/Stores/IOpenIddictApplicationStore.cs index 873531ce..09931dd6 100644 --- a/src/OpenIddict.Core/Stores/IOpenIddictApplicationStore.cs +++ b/src/OpenIddict.Core/Stores/IOpenIddictApplicationStore.cs @@ -26,6 +26,16 @@ namespace OpenIddict.Core { /// Task CreateAsync([NotNull] TApplication application, CancellationToken cancellationToken); + /// + /// Removes an existing application. + /// + /// The application to delete. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation. + /// + Task DeleteAsync([NotNull] TApplication application, CancellationToken cancellationToken); + /// /// Retrieves an application using its unique identifier. /// @@ -59,6 +69,17 @@ namespace OpenIddict.Core { /// Task FindByLogoutRedirectUri(string url, CancellationToken cancellationToken); + /// + /// Retrieves the client identifier associated with an application. + /// + /// The application. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the client identifier associated with the application. + /// + Task GetClientIdAsync([NotNull] TApplication application, CancellationToken cancellationToken); + /// /// Retrieves the client type associated with an application. /// @@ -113,5 +134,37 @@ namespace OpenIddict.Core { /// whose result returns the tokens associated with the application. /// Task> GetTokensAsync([NotNull] TApplication application, CancellationToken cancellationToken); + + /// + /// Sets the client type associated with an application. + /// + /// The application. + /// The client type associated with the application. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation. + /// + Task SetClientTypeAsync([NotNull] TApplication application, [NotNull] string type, CancellationToken cancellationToken); + + /// + /// Sets the hashed secret associated with an application. + /// + /// The application. + /// The hashed client secret associated with the application. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation. + /// + Task SetHashedSecretAsync([NotNull] TApplication application, [NotNull] string hash, CancellationToken cancellationToken); + + /// + /// Updates an existing application. + /// + /// The application to update. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation. + /// + Task UpdateAsync([NotNull] TApplication application, CancellationToken cancellationToken); } } \ No newline at end of file diff --git a/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictApplicationStore.cs b/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictApplicationStore.cs index 8e981e28..80301312 100644 --- a/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictApplicationStore.cs +++ b/src/OpenIddict.EntityFrameworkCore/Stores/OpenIddictApplicationStore.cs @@ -70,6 +70,28 @@ namespace OpenIddict.EntityFrameworkCore { return converter.ConvertToInvariantString(application.Id); } + /// + /// Removes an existing application. + /// + /// The application to delete. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public virtual async Task DeleteAsync([NotNull] TApplication application, CancellationToken cancellationToken) { + if (application == null) { + throw new ArgumentNullException(nameof(application)); + } + + Context.Remove(application); + + try { + await Context.SaveChangesAsync(cancellationToken); + } + + catch (DbUpdateConcurrencyException) { } + } + /// /// Retrieves an application using its unique identifier. /// @@ -119,6 +141,23 @@ namespace OpenIddict.EntityFrameworkCore { return Applications.SingleOrDefaultAsync(application => application.LogoutRedirectUri == url, cancellationToken); } + /// + /// Retrieves the client identifier associated with an application. + /// + /// The application. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation, + /// whose result returns the client identifier associated with the application. + /// + public virtual Task GetClientIdAsync([NotNull] TApplication application, CancellationToken cancellationToken) { + if (application == null) { + throw new ArgumentNullException(nameof(application)); + } + + return Task.FromResult(application.ClientId); + } + /// /// Retrieves the client type associated with an application. /// @@ -220,5 +259,74 @@ namespace OpenIddict.EntityFrameworkCore { return tokens; } + + /// + /// Sets the client type associated with an application. + /// + /// The application. + /// The client type associated with the application. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public virtual Task SetClientTypeAsync([NotNull] TApplication application, [NotNull] string type, CancellationToken cancellationToken) { + if (application == null) { + throw new ArgumentNullException(nameof(application)); + } + + if (string.IsNullOrEmpty(type)) { + throw new ArgumentException("The client type cannot be null or empty.", nameof(type)); + } + + application.Type = type; + + return Task.FromResult(0); + } + + /// + /// Sets the hashed secret associated with an application. + /// + /// The application. + /// The hashed client secret associated with the application. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public virtual Task SetHashedSecretAsync([NotNull] TApplication application, [NotNull] string hash, CancellationToken cancellationToken) { + if (application == null) { + throw new ArgumentNullException(nameof(application)); + } + + if (string.IsNullOrEmpty(hash)) { + throw new ArgumentException("The client secret hash cannot be null or empty.", nameof(hash)); + } + + application.ClientSecret = hash; + + return Task.FromResult(0); + } + + /// + /// Updates an existing application. + /// + /// The application to update. + /// The that can be used to abort the operation. + /// + /// A that can be used to monitor the asynchronous operation. + /// + public virtual async Task UpdateAsync([NotNull] TApplication application, CancellationToken cancellationToken) { + if (application == null) { + throw new ArgumentNullException(nameof(application)); + } + + Context.Attach(application); + Context.Update(application); + + try { + await Context.SaveChangesAsync(cancellationToken); + } + + catch (DbUpdateConcurrencyException) { } + } } } \ No newline at end of file diff --git a/src/OpenIddict.Models/OpenIddictApplication.cs b/src/OpenIddict.Models/OpenIddictApplication.cs index 5eee5694..ee2b18a2 100644 --- a/src/OpenIddict.Models/OpenIddictApplication.cs +++ b/src/OpenIddict.Models/OpenIddictApplication.cs @@ -73,6 +73,6 @@ namespace OpenIddict.Models { /// Gets or sets the application type /// associated with the current application. /// - public virtual string Type { get; set; } = "public"; + public virtual string Type { get; set; } } } \ No newline at end of file diff --git a/src/OpenIddict/OpenIddictProvider.Authentication.cs b/src/OpenIddict/OpenIddictProvider.Authentication.cs index 6c04ca07..d9e4c6c0 100644 --- a/src/OpenIddict/OpenIddictProvider.Authentication.cs +++ b/src/OpenIddict/OpenIddictProvider.Authentication.cs @@ -246,6 +246,19 @@ namespace OpenIddict { return; } + // Ensure a redirect_uri was associated with the application. + if (!await applications.HasRedirectUriAsync(application, context.HttpContext.RequestAborted)) { + logger.LogError("The authorization request was rejected because no redirect_uri " + + "was registered with the application '{ClientId}'.", context.ClientId); + + context.Reject( + error: OpenIdConnectConstants.Errors.UnauthorizedClient, + description: "The client application is not allowed to use interactive flows."); + + return; + } + + // Ensure the redirect_uri is valid. if (!await applications.ValidateRedirectUriAsync(application, context.RedirectUri, context.HttpContext.RequestAborted)) { logger.LogError("The authorization request was rejected because the redirect_uri " + "was invalid: '{RedirectUri}'.", context.RedirectUri); diff --git a/src/OpenIddict/OpenIddictProvider.Exchange.cs b/src/OpenIddict/OpenIddictProvider.Exchange.cs index f09d11cf..599ac573 100644 --- a/src/OpenIddict/OpenIddictProvider.Exchange.cs +++ b/src/OpenIddict/OpenIddictProvider.Exchange.cs @@ -167,7 +167,7 @@ namespace OpenIddict { return; } - if (!await applications.ValidateSecretAsync(application, context.ClientSecret, context.HttpContext.RequestAborted)) { + if (!await applications.ValidateClientSecretAsync(application, context.ClientSecret, context.HttpContext.RequestAborted)) { logger.LogError("The token request was rejected because the confidential application " + "'{ClientId}' didn't specify valid client credentials.", context.ClientId); diff --git a/src/OpenIddict/OpenIddictProvider.Introspection.cs b/src/OpenIddict/OpenIddictProvider.Introspection.cs index ca5c31bb..bc4f8c2a 100644 --- a/src/OpenIddict/OpenIddictProvider.Introspection.cs +++ b/src/OpenIddict/OpenIddictProvider.Introspection.cs @@ -75,7 +75,7 @@ namespace OpenIddict { } // Validate the client credentials. - if (!await applications.ValidateSecretAsync(application, context.ClientSecret, context.HttpContext.RequestAborted)) { + if (!await applications.ValidateClientSecretAsync(application, context.ClientSecret, context.HttpContext.RequestAborted)) { logger.LogError("The introspection request was rejected because the confidential application " + "'{ClientId}' didn't specify valid client credentials.", context.ClientId); diff --git a/src/OpenIddict/OpenIddictProvider.Revocation.cs b/src/OpenIddict/OpenIddictProvider.Revocation.cs index ba0adf04..2ef8bdf3 100644 --- a/src/OpenIddict/OpenIddictProvider.Revocation.cs +++ b/src/OpenIddict/OpenIddictProvider.Revocation.cs @@ -104,7 +104,7 @@ namespace OpenIddict { return; } - if (!await applications.ValidateSecretAsync(application, context.ClientSecret, context.HttpContext.RequestAborted)) { + if (!await applications.ValidateClientSecretAsync(application, context.ClientSecret, context.HttpContext.RequestAborted)) { context.Reject( error: OpenIdConnectConstants.Errors.InvalidClient, description: "Invalid credentials: ensure that you specified a correct client_secret."); diff --git a/test/OpenIddict.Tests/OpenIddictProviderTests.Authentication.cs b/test/OpenIddict.Tests/OpenIddictProviderTests.Authentication.cs index 8397a096..b162a218 100644 --- a/test/OpenIddict.Tests/OpenIddictProviderTests.Authentication.cs +++ b/test/OpenIddict.Tests/OpenIddictProviderTests.Authentication.cs @@ -306,6 +306,40 @@ namespace OpenIddict.Tests { Mock.Get(manager).Verify(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny()), Times.Once()); } + [Fact] + public async Task ValidateAuthorizationRequest_RequestIsRejectedWhenClientHasNoRedirectUri() { + // Arrange + var application = new OpenIddictApplication(); + + var manager = CreateApplicationManager(instance => { + instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) + .ReturnsAsync(application); + + instance.Setup(mock => mock.HasRedirectUriAsync(application, It.IsAny())) + .ReturnsAsync(false); + }); + + var server = CreateAuthorizationServer(builder => { + builder.Services.AddSingleton(manager); + }); + + var client = new OpenIdConnectClient(server.CreateClient()); + + // Act + var response = await client.PostAsync(AuthorizationEndpoint, new OpenIdConnectRequest { + ClientId = "Fabrikam", + RedirectUri = "http://www.fabrikam.com/path", + ResponseType = OpenIdConnectConstants.ResponseTypes.Code + }); + + // Assert + Assert.Equal(OpenIdConnectConstants.Errors.UnauthorizedClient, response.Error); + Assert.Equal("The client application is not allowed to use interactive flows.", response.ErrorDescription); + + Mock.Get(manager).Verify(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny()), Times.Once()); + Mock.Get(manager).Verify(mock => mock.HasRedirectUriAsync(application, It.IsAny()), Times.Once()); + } + [Fact] public async Task ValidateAuthorizationRequest_RequestIsRejectedWhenRedirectUriIsInvalid() { // Arrange @@ -315,6 +349,9 @@ namespace OpenIddict.Tests { instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) .ReturnsAsync(application); + instance.Setup(mock => mock.HasRedirectUriAsync(application, It.IsAny())) + .ReturnsAsync(true); + instance.Setup(mock => mock.ValidateRedirectUriAsync(application, "http://www.fabrikam.com/path", It.IsAny())) .ReturnsAsync(false); }); @@ -337,6 +374,7 @@ namespace OpenIddict.Tests { Assert.Equal("Invalid redirect_uri.", response.ErrorDescription); Mock.Get(manager).Verify(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny()), Times.Once()); + Mock.Get(manager).Verify(mock => mock.HasRedirectUriAsync(application, It.IsAny()), Times.Once()); Mock.Get(manager).Verify(mock => mock.ValidateRedirectUriAsync(application, "http://www.fabrikam.com/path", It.IsAny()), Times.Once()); } @@ -353,6 +391,9 @@ namespace OpenIddict.Tests { instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) .ReturnsAsync(application); + instance.Setup(mock => mock.HasRedirectUriAsync(application, It.IsAny())) + .ReturnsAsync(true); + instance.Setup(mock => mock.ValidateRedirectUriAsync(application, "http://www.fabrikam.com/path", It.IsAny())) .ReturnsAsync(true); @@ -381,6 +422,7 @@ namespace OpenIddict.Tests { "an access token from the authorization endpoint.", response.ErrorDescription); Mock.Get(manager).Verify(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny()), Times.Once()); + Mock.Get(manager).Verify(mock => mock.HasRedirectUriAsync(application, It.IsAny()), Times.Once()); Mock.Get(manager).Verify(mock => mock.ValidateRedirectUriAsync(application, "http://www.fabrikam.com/path", It.IsAny()), Times.Once()); Mock.Get(manager).Verify(mock => mock.GetClientTypeAsync(application, It.IsAny()), Times.Once()); } @@ -397,6 +439,9 @@ namespace OpenIddict.Tests { instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) .ReturnsAsync(application); + instance.Setup(mock => mock.HasRedirectUriAsync(application, It.IsAny())) + .ReturnsAsync(true); + instance.Setup(mock => mock.ValidateRedirectUriAsync(application, "http://www.fabrikam.com/path", It.IsAny())) .ReturnsAsync(true); @@ -447,6 +492,9 @@ namespace OpenIddict.Tests { instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) .ReturnsAsync(application); + instance.Setup(mock => mock.HasRedirectUriAsync(application, It.IsAny())) + .ReturnsAsync(true); + instance.Setup(mock => mock.ValidateRedirectUriAsync(application, "http://www.fabrikam.com/path", It.IsAny())) .ReturnsAsync(true); @@ -507,6 +555,9 @@ namespace OpenIddict.Tests { instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) .ReturnsAsync(application); + instance.Setup(mock => mock.HasRedirectUriAsync(application, It.IsAny())) + .ReturnsAsync(true); + instance.Setup(mock => mock.ValidateRedirectUriAsync(application, "http://www.fabrikam.com/path", It.IsAny())) .ReturnsAsync(true); @@ -544,6 +595,9 @@ namespace OpenIddict.Tests { instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) .ReturnsAsync(application); + instance.Setup(mock => mock.HasRedirectUriAsync(application, It.IsAny())) + .ReturnsAsync(true); + instance.Setup(mock => mock.ValidateRedirectUriAsync(application, "http://www.fabrikam.com/path", It.IsAny())) .ReturnsAsync(true); diff --git a/test/OpenIddict.Tests/OpenIddictProviderTests.Exchange.cs b/test/OpenIddict.Tests/OpenIddictProviderTests.Exchange.cs index f2165f44..29c021cb 100644 --- a/test/OpenIddict.Tests/OpenIddictProviderTests.Exchange.cs +++ b/test/OpenIddict.Tests/OpenIddictProviderTests.Exchange.cs @@ -272,7 +272,7 @@ namespace OpenIddict.Tests { instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) .ReturnsAsync(OpenIddictConstants.ClientTypes.Confidential); - instance.Setup(mock => mock.ValidateSecretAsync(application, "7Fjfp0ZBr1KtDRbnfVdmIw", It.IsAny())) + instance.Setup(mock => mock.ValidateClientSecretAsync(application, "7Fjfp0ZBr1KtDRbnfVdmIw", It.IsAny())) .ReturnsAsync(false); }); @@ -297,7 +297,7 @@ namespace OpenIddict.Tests { Mock.Get(manager).Verify(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny()), Times.Once()); Mock.Get(manager).Verify(mock => mock.GetClientTypeAsync(application, It.IsAny()), Times.Once()); - Mock.Get(manager).Verify(mock => mock.ValidateSecretAsync(application, "7Fjfp0ZBr1KtDRbnfVdmIw", It.IsAny()), Times.Once()); + Mock.Get(manager).Verify(mock => mock.ValidateClientSecretAsync(application, "7Fjfp0ZBr1KtDRbnfVdmIw", It.IsAny()), Times.Once()); } [Fact] @@ -661,7 +661,7 @@ namespace OpenIddict.Tests { instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) .ReturnsAsync(OpenIddictConstants.ClientTypes.Confidential); - instance.Setup(mock => mock.ValidateSecretAsync(application, "7Fjfp0ZBr1KtDRbnfVdmIw", It.IsAny())) + instance.Setup(mock => mock.ValidateClientSecretAsync(application, "7Fjfp0ZBr1KtDRbnfVdmIw", It.IsAny())) .ReturnsAsync(true); })); diff --git a/test/OpenIddict.Tests/OpenIddictProviderTests.Introspection.cs b/test/OpenIddict.Tests/OpenIddictProviderTests.Introspection.cs index b3bb02df..918d0743 100644 --- a/test/OpenIddict.Tests/OpenIddictProviderTests.Introspection.cs +++ b/test/OpenIddict.Tests/OpenIddictProviderTests.Introspection.cs @@ -130,7 +130,7 @@ namespace OpenIddict.Tests { instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) .ReturnsAsync(OpenIddictConstants.ClientTypes.Confidential); - instance.Setup(mock => mock.ValidateSecretAsync(application, "7Fjfp0ZBr1KtDRbnfVdmIw", It.IsAny())) + instance.Setup(mock => mock.ValidateClientSecretAsync(application, "7Fjfp0ZBr1KtDRbnfVdmIw", It.IsAny())) .ReturnsAsync(false); }); @@ -153,7 +153,7 @@ namespace OpenIddict.Tests { Mock.Get(manager).Verify(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny()), Times.Once()); Mock.Get(manager).Verify(mock => mock.GetClientTypeAsync(application, It.IsAny()), Times.Once()); - Mock.Get(manager).Verify(mock => mock.ValidateSecretAsync(application, "7Fjfp0ZBr1KtDRbnfVdmIw", It.IsAny()), Times.Once()); + Mock.Get(manager).Verify(mock => mock.ValidateClientSecretAsync(application, "7Fjfp0ZBr1KtDRbnfVdmIw", It.IsAny()), Times.Once()); } [Fact] @@ -186,7 +186,7 @@ namespace OpenIddict.Tests { instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) .ReturnsAsync(OpenIddictConstants.ClientTypes.Confidential); - instance.Setup(mock => mock.ValidateSecretAsync(application, "7Fjfp0ZBr1KtDRbnfVdmIw", It.IsAny())) + instance.Setup(mock => mock.ValidateClientSecretAsync(application, "7Fjfp0ZBr1KtDRbnfVdmIw", It.IsAny())) .ReturnsAsync(true); })); @@ -236,7 +236,7 @@ namespace OpenIddict.Tests { instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) .ReturnsAsync(OpenIddictConstants.ClientTypes.Confidential); - instance.Setup(mock => mock.ValidateSecretAsync(application, "7Fjfp0ZBr1KtDRbnfVdmIw", It.IsAny())) + instance.Setup(mock => mock.ValidateClientSecretAsync(application, "7Fjfp0ZBr1KtDRbnfVdmIw", It.IsAny())) .ReturnsAsync(true); })); @@ -288,7 +288,7 @@ namespace OpenIddict.Tests { instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) .ReturnsAsync(OpenIddictConstants.ClientTypes.Confidential); - instance.Setup(mock => mock.ValidateSecretAsync(application, "7Fjfp0ZBr1KtDRbnfVdmIw", It.IsAny())) + instance.Setup(mock => mock.ValidateClientSecretAsync(application, "7Fjfp0ZBr1KtDRbnfVdmIw", It.IsAny())) .ReturnsAsync(true); })); @@ -345,7 +345,7 @@ namespace OpenIddict.Tests { instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) .ReturnsAsync(OpenIddictConstants.ClientTypes.Confidential); - instance.Setup(mock => mock.ValidateSecretAsync(application, "7Fjfp0ZBr1KtDRbnfVdmIw", It.IsAny())) + instance.Setup(mock => mock.ValidateClientSecretAsync(application, "7Fjfp0ZBr1KtDRbnfVdmIw", It.IsAny())) .ReturnsAsync(true); })); @@ -404,7 +404,7 @@ namespace OpenIddict.Tests { instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) .ReturnsAsync(OpenIddictConstants.ClientTypes.Confidential); - instance.Setup(mock => mock.ValidateSecretAsync(application, "7Fjfp0ZBr1KtDRbnfVdmIw", It.IsAny())) + instance.Setup(mock => mock.ValidateClientSecretAsync(application, "7Fjfp0ZBr1KtDRbnfVdmIw", It.IsAny())) .ReturnsAsync(true); })); diff --git a/test/OpenIddict.Tests/OpenIddictProviderTests.Revocation.cs b/test/OpenIddict.Tests/OpenIddictProviderTests.Revocation.cs index e317dd91..c6a90aaa 100644 --- a/test/OpenIddict.Tests/OpenIddictProviderTests.Revocation.cs +++ b/test/OpenIddict.Tests/OpenIddictProviderTests.Revocation.cs @@ -169,7 +169,7 @@ namespace OpenIddict.Tests { instance.Setup(mock => mock.GetClientTypeAsync(application, It.IsAny())) .ReturnsAsync(OpenIddictConstants.ClientTypes.Confidential); - instance.Setup(mock => mock.ValidateSecretAsync(application, "7Fjfp0ZBr1KtDRbnfVdmIw", It.IsAny())) + instance.Setup(mock => mock.ValidateClientSecretAsync(application, "7Fjfp0ZBr1KtDRbnfVdmIw", It.IsAny())) .ReturnsAsync(false); }); @@ -193,7 +193,7 @@ namespace OpenIddict.Tests { Mock.Get(manager).Verify(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny()), Times.Once()); Mock.Get(manager).Verify(mock => mock.GetClientTypeAsync(application, It.IsAny()), Times.Once()); - Mock.Get(manager).Verify(mock => mock.ValidateSecretAsync(application, "7Fjfp0ZBr1KtDRbnfVdmIw", It.IsAny()), Times.Once()); + Mock.Get(manager).Verify(mock => mock.ValidateClientSecretAsync(application, "7Fjfp0ZBr1KtDRbnfVdmIw", It.IsAny()), Times.Once()); } [Fact] diff --git a/test/OpenIddict.Tests/OpenIddictProviderTests.Serialization.cs b/test/OpenIddict.Tests/OpenIddictProviderTests.Serialization.cs index dcad715e..aca01474 100644 --- a/test/OpenIddict.Tests/OpenIddictProviderTests.Serialization.cs +++ b/test/OpenIddict.Tests/OpenIddictProviderTests.Serialization.cs @@ -25,6 +25,9 @@ namespace OpenIddict.Tests { instance.Setup(mock => mock.FindByClientIdAsync("Fabrikam", It.IsAny())) .ReturnsAsync(application); + instance.Setup(mock => mock.HasRedirectUriAsync(application, It.IsAny())) + .ReturnsAsync(true); + instance.Setup(mock => mock.ValidateRedirectUriAsync(application, "http://www.fabrikam.com/path", It.IsAny())) .ReturnsAsync(true);