diff --git a/OpenIddict.sln b/OpenIddict.sln index 2e1abd3a..5f526033 100644 --- a/OpenIddict.sln +++ b/OpenIddict.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 14 -VisualStudioVersion = 14.0.25123.0 +VisualStudioVersion = 14.0.25420.1 MinimumVisualStudioVersion = 10.0.40219.1 Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "OpenIddict", "src\OpenIddict\OpenIddict.xproj", "{80A8D6CE-C29A-4602-9844-D51FEF9C33C8}" EndProject diff --git a/global.json b/global.json index 90b78172..b0323e42 100644 --- a/global.json +++ b/global.json @@ -1,3 +1,3 @@ { - "projects": [ "src", "external" ] + "projects": [ "src" ] } \ No newline at end of file diff --git a/samples/Mvc.Client/Startup.cs b/samples/Mvc.Client/Startup.cs index 597b30d6..48e3ccef 100644 --- a/samples/Mvc.Client/Startup.cs +++ b/samples/Mvc.Client/Startup.cs @@ -39,7 +39,7 @@ namespace Mvc.Client { SaveTokens = true, // Use the authorization code flow. - ResponseType = OpenIdConnectResponseTypes.Code, + ResponseType = OpenIdConnectResponseType.Code, // Note: setting the Authority allows the OIDC client middleware to automatically // retrieve the identity provider's configuration and spare you from setting diff --git a/samples/Mvc.Client/project.json b/samples/Mvc.Client/project.json index 97799596..48029529 100644 --- a/samples/Mvc.Client/project.json +++ b/samples/Mvc.Client/project.json @@ -14,30 +14,30 @@ }, "dependencies": { - "Microsoft.AspNetCore.Authentication.Cookies": "1.0.0-rc2-final", - "Microsoft.AspNetCore.Authentication.OpenIdConnect": "1.0.0-rc2-final", - "Microsoft.AspNetCore.Diagnostics": "1.0.0-rc2-final", - "Microsoft.AspNetCore.Hosting": "1.0.0-rc2-final", - "Microsoft.AspNetCore.Mvc": "1.0.0-rc2-final", - "Microsoft.AspNetCore.Server.IISIntegration": "1.0.0-rc2-final", - "Microsoft.AspNetCore.Server.Kestrel": "1.0.0-rc2-final", - "Microsoft.AspNetCore.StaticFiles": "1.0.0-rc2-final", - "Microsoft.Extensions.Configuration.CommandLine": "1.0.0-rc2-final", - "Microsoft.Extensions.Configuration.EnvironmentVariables": "1.0.0-rc2-final", - "Microsoft.Extensions.Logging.Console": "1.0.0-rc2-final", - "Microsoft.Extensions.Logging.Debug": "1.0.0-rc2-final" + "Microsoft.AspNetCore.Authentication.Cookies": "1.0.0", + "Microsoft.AspNetCore.Authentication.OpenIdConnect": "1.0.0", + "Microsoft.AspNetCore.Diagnostics": "1.0.0", + "Microsoft.AspNetCore.Hosting": "1.0.0", + "Microsoft.AspNetCore.Mvc": "1.0.0", + "Microsoft.AspNetCore.Server.IISIntegration": "1.0.0", + "Microsoft.AspNetCore.Server.Kestrel": "1.0.0", + "Microsoft.AspNetCore.StaticFiles": "1.0.0", + "Microsoft.Extensions.Configuration.CommandLine": "1.0.0", + "Microsoft.Extensions.Configuration.EnvironmentVariables": "1.0.0", + "Microsoft.Extensions.Logging.Console": "1.0.0", + "Microsoft.Extensions.Logging.Debug": "1.0.0" }, "frameworks": { "net451": { "dependencies": { - "Microsoft.NETCore.Platforms": "1.0.1-rc2-24027" + "Microsoft.NETCore.Platforms": "1.0.1" } }, "netcoreapp1.0": { "dependencies": { - "Microsoft.NETCore.App": { "type": "platform", "version": "1.0.0-rc2-3002702" } + "Microsoft.NETCore.App": { "type": "platform", "version": "1.0.0" } }, "imports": [ @@ -48,10 +48,7 @@ }, "tools": { - "Microsoft.AspNetCore.Server.IISIntegration.Tools": { - "version": "1.0.0-preview1-final", - "imports": "portable-net45+wp80+win8+wpa81+dnxcore50" - } + "Microsoft.AspNetCore.Server.IISIntegration.Tools": "1.0.0-preview2-final" }, "scripts": { diff --git a/samples/Mvc.Server/Startup.cs b/samples/Mvc.Server/Startup.cs index 7483dead..14334710 100644 --- a/samples/Mvc.Server/Startup.cs +++ b/samples/Mvc.Server/Startup.cs @@ -1,6 +1,5 @@ using System; using System.Linq; -using AspNet.Security.OAuth.GitHub; using CryptoHelper; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Identity.EntityFrameworkCore; @@ -63,9 +62,7 @@ namespace Mvc.Server { // .SetLogoutEndpointPath("/connect/logout"); // Note: if you don't explicitly register a signing key, one is automatically generated and - // persisted on the disk. If the key cannot be persisted, an in-memory key is used instead: - // when the application shuts down, the key is definitely lost and the access/identity tokens - // will be considered as invalid by client applications/resource servers when validating them. + // persisted on the disk. If the key cannot be persisted, an exception is thrown. // // On production, using a X.509 certificate stored in the machine store is recommended. // You can generate a self-signed certificate using Pluralsight's self-cert utility: @@ -121,11 +118,7 @@ namespace Mvc.Server { ConsumerSecret = "Il2eFzGIrYhz6BWjYhVXBPQSfZuS4xoHpSSyD9PI" }); - app.UseGitHubAuthentication(new GitHubAuthenticationOptions { - ClientId = "49e302895d8b09ea5656", - ClientSecret = "98f1bf028608901e9df91d64ee61536fe562064b", - Scope = { "user:email" } - }); + app.UseSession(); app.UseOpenIddict(); diff --git a/samples/Mvc.Server/project.json b/samples/Mvc.Server/project.json index 99237f62..39c85d93 100644 --- a/samples/Mvc.Server/project.json +++ b/samples/Mvc.Server/project.json @@ -19,22 +19,21 @@ }, "dependencies": { - "AspNet.Security.OAuth.GitHub": "1.0.0-alpha4-final", - "AspNet.Security.OAuth.Introspection": "1.0.0-alpha1-final", - "AspNet.Security.OAuth.Validation": "1.0.0-alpha1-final", - "Microsoft.AspNetCore.Authentication.Google": "1.0.0-rc2-final", - "Microsoft.AspNetCore.Authentication.Twitter": "1.0.0-rc2-final", - "Microsoft.AspNetCore.Diagnostics": "1.0.0-rc2-final", - "Microsoft.AspNetCore.Mvc": "1.0.0-rc2-final", - "Microsoft.AspNetCore.Server.IISIntegration": "1.0.0-rc2-final", - "Microsoft.AspNetCore.Server.Kestrel": "1.0.0-rc2-final", - "Microsoft.AspNetCore.StaticFiles": "1.0.0-rc2-final", - "Microsoft.EntityFrameworkCore.SqlServer": "1.0.0-rc2-final", - "Microsoft.Extensions.Configuration.CommandLine": "1.0.0-rc2-final", - "Microsoft.Extensions.Configuration.EnvironmentVariables": "1.0.0-rc2-final", - "Microsoft.Extensions.Configuration.Json": "1.0.0-rc2-final", - "Microsoft.Extensions.Logging.Console": "1.0.0-rc2-final", - "Microsoft.Extensions.Logging.Debug": "1.0.0-rc2-final", + "AspNet.Security.OAuth.Introspection": "1.0.0-alpha2-final", + "AspNet.Security.OAuth.Validation": "1.0.0-alpha2-final", + "Microsoft.AspNetCore.Authentication.Google": "1.0.0", + "Microsoft.AspNetCore.Authentication.Twitter": "1.0.0", + "Microsoft.AspNetCore.Diagnostics": "1.0.0", + "Microsoft.AspNetCore.Mvc": "1.0.0", + "Microsoft.AspNetCore.Server.IISIntegration": "1.0.0", + "Microsoft.AspNetCore.Server.Kestrel": "1.0.0", + "Microsoft.AspNetCore.StaticFiles": "1.0.0", + "Microsoft.EntityFrameworkCore.SqlServer": "1.0.0", + "Microsoft.Extensions.Configuration.CommandLine": "1.0.0", + "Microsoft.Extensions.Configuration.EnvironmentVariables": "1.0.0", + "Microsoft.Extensions.Configuration.Json": "1.0.0", + "Microsoft.Extensions.Logging.Console": "1.0.0", + "Microsoft.Extensions.Logging.Debug": "1.0.0", "OpenIddict": { "target": "project" }, "OpenIddict.Assets": { "target": "project" }, "OpenIddict.Mvc": { "target": "project" }, @@ -44,13 +43,13 @@ "frameworks": { "net451": { "dependencies": { - "Microsoft.NETCore.Platforms": "1.0.1-rc2-24027" + "Microsoft.NETCore.Platforms": "1.0.1" } }, "netcoreapp1.0": { "dependencies": { - "Microsoft.NETCore.App": { "type": "platform", "version": "1.0.0-rc2-3002702" } + "Microsoft.NETCore.App": { "type": "platform", "version": "1.0.0" } }, "imports": [ @@ -61,10 +60,7 @@ }, "tools": { - "Microsoft.AspNetCore.Server.IISIntegration.Tools": { - "version": "1.0.0-preview1-final", - "imports": "portable-net45+wp80+win8+wpa81+dnxcore50" - } + "Microsoft.AspNetCore.Server.IISIntegration.Tools": "1.0.0-preview2-final" }, "scripts": { diff --git a/src/OpenIddict.Assets/project.json b/src/OpenIddict.Assets/project.json index 4e028b05..7b305091 100644 --- a/src/OpenIddict.Assets/project.json +++ b/src/OpenIddict.Assets/project.json @@ -42,8 +42,8 @@ "dependencies": { "JetBrains.Annotations": { "type": "build", "version": "10.1.4" }, - "Microsoft.AspNetCore.StaticFiles": "1.0.0-rc2-final", - "Microsoft.Extensions.FileProviders.Embedded": "1.0.0-rc2-final", + "Microsoft.AspNetCore.StaticFiles": "1.0.0", + "Microsoft.Extensions.FileProviders.Embedded": "1.0.0", "OpenIddict.Core": { "target": "project" } }, diff --git a/src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Authentication.cs b/src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Authentication.cs index e51474a9..b500be9d 100644 --- a/src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Authentication.cs +++ b/src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Authentication.cs @@ -100,8 +100,8 @@ namespace OpenIddict.Infrastructure { if (!string.IsNullOrEmpty(email) && string.Equals(username, email, StringComparison.OrdinalIgnoreCase)) { services.Logger.LogError("The authorization request was rejected because the 'email' scope was not requested: " + - "to prevent data leakage, the 'email' scope must be granted when the username" + - "is identical to the email address associated with the user ({Username}).", username); + "to prevent data leakage, the 'email' scope must be granted when the username " + + "is identical to the email address associated with the user profile."); context.Reject( error: OpenIdConnectConstants.Errors.InvalidRequest, @@ -178,19 +178,25 @@ namespace OpenIddict.Infrastructure { // Note: user may be null if the user was removed after // the initial check made by ValidateAuthorizationRequest. - // In this case, throw an exception to abort the request. var user = await services.Users.GetUserAsync(principal); if (user == null) { - throw new InvalidOperationException("The prompt=none authorization request was aborted because the profile " + - "corresponding to the logged in user was not found in the database."); + services.Logger.LogError("The authorization request was aborted because the profile corresponding " + + "to the logged in user was not found in the database: {Identifier}.", + context.HttpContext.User.GetClaim(ClaimTypes.NameIdentifier)); + + context.Reject( + error: OpenIdConnectConstants.Errors.ServerError, + description: "An internal error has occurred."); + + return; } // Note: filtering the username is not needed at this stage as OpenIddictController.Accept - // and OpenIddictProvider.GrantResourceOwnerCredentials are expected to reject requests that - // don't include the "email" scope if the username corresponds to the registed email address. + // and OpenIddictProvider.HandleTokenRequest are expected to reject requests that don't + // include the "email" scope if the username corresponds to the registed email address. var identity = await services.Users.CreateIdentityAsync(user, context.Request.GetScopes()); if (identity == null) { - throw new InvalidOperationException("The authorization request failed because the user manager returned a null " + + throw new InvalidOperationException("The authorization request was aborted because the user manager returned a null " + $"identity for user '{await services.Users.GetUserNameAsync(user)}'."); } diff --git a/src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Exchange.cs b/src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Exchange.cs index 6272e40a..ecd4d02f 100644 --- a/src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Exchange.cs +++ b/src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Exchange.cs @@ -59,9 +59,9 @@ namespace OpenIddict.Infrastructure { // and https://tools.ietf.org/html/rfc6749#section-6 for more information. // Skip client authentication if the client identifier is missing. - // Note: ASOS will automatically ensure that the calling application - // cannot use an authorization code or a refresh token if it's not - // the intended audience, even if client authentication was skipped. + // Note: the OpenID Connect server middleware will automatically ensure that + // the calling application cannot use an authorization code or a refresh token + // if it's not the intended audience, even if client authentication was skipped. if (string.IsNullOrEmpty(context.ClientId)) { services.Logger.LogInformation("The token request validation process was skipped " + "because the client_id parameter was missing or empty."); @@ -126,236 +126,263 @@ namespace OpenIddict.Infrastructure { context.Validate(); } - public override async Task GrantClientCredentials([NotNull] GrantClientCredentialsContext context) { + public override async Task HandleTokenRequest([NotNull] HandleTokenRequestContext context) { var services = context.HttpContext.RequestServices.GetRequiredService>(); - // Retrieve the application details corresponding to the requested client_id. - // Note: this call shouldn't return a null instance, but a race condition may occur - // if the application was removed after the initial check made by ValidateTokenRequest. - var application = await services.Applications.FindByClientIdAsync(context.ClientId); - if (application == null) { - throw new InvalidOperationException("The token request was aborted because the client application corresponding " + - $"to the '{context.ClientId}' identifier was not found in the database."); - } + Debug.Assert(context.Request.IsAuthorizationCodeGrantType() || + context.Request.IsClientCredentialsGrantType() || + context.Request.IsPasswordGrantType() || + context.Request.IsRefreshTokenGrantType(), "The grant_type parameter should be a supported value."); - var identity = new ClaimsIdentity(context.Options.AuthenticationScheme); + // Note: the OpenID Connect server middleware automatically reuses the authentication ticket + // stored in the authorization code to create a new identity. To ensure the user was not removed + // after the authorization code was issued, a new check is made before validating the request. + if (context.Request.IsAuthorizationCodeGrantType()) { + Debug.Assert(context.Ticket.Principal != null, "The authentication ticket shouldn't be null."); - // Note: the name identifier is always included in both identity and - // access tokens, even if an explicit destination is not specified. - identity.AddClaim(ClaimTypes.NameIdentifier, context.ClientId); + var user = await services.Users.GetUserAsync(context.Ticket.Principal); + if (user == null) { + services.Logger.LogError("The token request was rejected because the user profile associated " + + "with the authorization code was not found in the database: '{Identifier}'.", + context.Ticket.Principal.GetClaim(ClaimTypes.NameIdentifier)); - identity.AddClaim(ClaimTypes.Name, await services.Applications.GetDisplayNameAsync(application), - OpenIdConnectConstants.Destinations.AccessToken, - OpenIdConnectConstants.Destinations.IdentityToken); + context.Reject( + error: OpenIdConnectConstants.Errors.InvalidGrant, + description: "The authorization code is no longer valid."); - // Create a new authentication ticket - // holding the application identity. - var ticket = new AuthenticationTicket( - new ClaimsPrincipal(identity), - new AuthenticationProperties(), - context.Options.AuthenticationScheme); + return; + } - ticket.SetResources(context.Request.GetResources()); - ticket.SetScopes(context.Request.GetScopes()); + context.Validate(context.Ticket); + } - context.Validate(ticket); - } + // Note: the OpenID Connect server middleware automatically reuses the authentication ticket + // stored in the refresh token to create a new identity. To ensure the user was not removed + // after the refresh token was issued, a new check is made before validating the request. + else if (context.Request.IsRefreshTokenGrantType()) { + Debug.Assert(context.Ticket.Principal != null, "The authentication ticket shouldn't be null."); - public override async Task GrantAuthorizationCode([NotNull] GrantAuthorizationCodeContext context) { - var services = context.HttpContext.RequestServices.GetRequiredService>(); + var user = await services.Users.GetUserAsync(context.Ticket.Principal); + if (user == null) { + services.Logger.LogError("The token request was rejected because the user profile associated " + + "with the refresh token was not found in the database: '{Identifier}'.", + context.Ticket.Principal.GetClaim(ClaimTypes.NameIdentifier)); - var user = await services.Users.GetUserAsync(context.Ticket.Principal); - if (user == null) { - services.Logger.LogError("The token request was rejected because the user profile associated " + - "with the authorization code was not found in the database: '{Identifier}'.", - context.Ticket.Principal.GetClaim(ClaimTypes.NameIdentifier)); + context.Reject( + error: OpenIdConnectConstants.Errors.InvalidGrant, + description: "The refresh token is no longer valid."); - context.Reject( - error: OpenIdConnectConstants.Errors.InvalidGrant, - description: "The authorization code is no longer valid."); + return; + } - return; - } + // Extract the token identifier from the refresh token. + var identifier = context.Ticket.GetTicketId(); + Debug.Assert(!string.IsNullOrEmpty(identifier), + "The refresh token should contain a ticket identifier."); - context.Validate(context.Ticket); - } + // Retrieve the token from the database and ensure it is still valid. + var token = await services.Tokens.FindByIdAsync(identifier); + if (token == null) { + services.Logger.LogError("The token request was rejected because the refresh token was revoked."); - public override async Task GrantRefreshToken([NotNull] GrantRefreshTokenContext context) { - var services = context.HttpContext.RequestServices.GetRequiredService>(); + context.Reject( + error: OpenIdConnectConstants.Errors.InvalidGrant, + description: "The refresh token is no longer valid."); - var user = await services.Users.GetUserAsync(context.Ticket.Principal); - if (user == null) { - services.Logger.LogError("The token request was rejected because the user profile associated " + - "with the refresh token was not found in the database: '{Identifier}'.", - context.Ticket.Principal.GetClaim(ClaimTypes.NameIdentifier)); + return; + } - context.Reject( - error: OpenIdConnectConstants.Errors.InvalidGrant, - description: "The refresh token is no longer valid."); + // When sliding expiration is enabled, immediately + // revoke the refresh token to prevent future reuse. + // See https://tools.ietf.org/html/rfc6749#section-6. + if (context.Options.UseSlidingExpiration) { + await services.Tokens.RevokeAsync(token); + } - return; + // Note: the "scopes" property stored in context.AuthenticationTicket is automatically updated by the + // OpenID Connect server middleware when the client application requests a restricted scopes collection. + var identity = await services.Users.CreateIdentityAsync(user, context.Ticket.GetScopes()); + if (identity == null) { + throw new InvalidOperationException("The token request was aborted because the user manager returned a null " + + $"identity for user '{await services.Users.GetUserNameAsync(user)}'."); + } + + // Create a new authentication ticket holding the user identity but + // reuse the authentication properties stored in the refresh token. + var ticket = new AuthenticationTicket( + new ClaimsPrincipal(identity), + context.Ticket.Properties, + context.Options.AuthenticationScheme); + + context.Validate(ticket); } - // Extract the token identifier from the refresh token. - var identifier = context.Ticket.GetTicketId(); - Debug.Assert(!string.IsNullOrEmpty(identifier), - "The refresh token should contain a ticket identifier."); + else if (context.Request.IsPasswordGrantType()) { + // Note: at this stage, the client credentials cannot be null as the OpenID Connect server middleware + // automatically rejects grant_type=password requests that don't specify a username/password couple. + Debug.Assert(!string.IsNullOrEmpty(context.Request.Username) && + !string.IsNullOrEmpty(context.Request.Password), "The user credentials shouldn't be null."); - // Retrieve the token from the database and ensure it is still valid. - var token = await services.Tokens.FindByIdAsync(identifier); - if (token == null) { - services.Logger.LogError("The token request was rejected because the refresh token was revoked."); + var user = await services.Users.FindByNameAsync(context.Request.Username); + if (user == null) { + services.Logger.LogError("The token request was rejected because no user profile corresponding to " + + "the specified username was found: '{Username}'.", context.Request.Username); - context.Reject( - error: OpenIdConnectConstants.Errors.InvalidGrant, - description: "The refresh token is no longer valid."); + context.Reject( + error: OpenIdConnectConstants.Errors.InvalidGrant, + description: "Invalid credentials."); - return; - } + return; + } - // When sliding expiration is enabled, immediately - // revoke the refresh token to prevent future reuse. - // See https://tools.ietf.org/html/rfc6749#section-6. - if (context.Options.UseSlidingExpiration) { - await services.Tokens.RevokeAsync(token); - } + // Ensure the user is allowed to sign in. + if (!await services.SignIn.CanSignInAsync(user)) { + services.Logger.LogError("The token request was rejected because the user '{Username}' " + + "was not allowed to sign in.", context.Request.Username); - // Note: the "scopes" property stored in context.AuthenticationTicket is automatically - // updated by ASOS when the client application requests a restricted scopes collection. - var identity = await services.Users.CreateIdentityAsync(user, context.Ticket.GetScopes()); - if (identity == null) { - throw new InvalidOperationException("The token request failed because the user manager returned a null " + - $"identity for user '{await services.Users.GetUserNameAsync(user)}'."); - } + context.Reject( + error: OpenIdConnectConstants.Errors.InvalidGrant, + description: "The user is not allowed to sign in."); - // Create a new authentication ticket holding the user identity but - // reuse the authentication properties stored in the refresh token. - var ticket = new AuthenticationTicket( - new ClaimsPrincipal(identity), - context.Ticket.Properties, - context.Options.AuthenticationScheme); + return; + } - context.Validate(ticket); - } + // Ensure the user is not already locked out. + if (services.Users.SupportsUserLockout && await services.Users.IsLockedOutAsync(user)) { + services.Logger.LogError("The token request was rejected because the account '{Username}' " + + "was locked out to prevent brute force attacks.", context.Request.Username); - public override async Task GrantResourceOwnerCredentials([NotNull] GrantResourceOwnerCredentialsContext context) { - var services = context.HttpContext.RequestServices.GetRequiredService>(); + context.Reject( + error: OpenIdConnectConstants.Errors.InvalidGrant, + description: "Account locked out."); - var user = await services.Users.FindByNameAsync(context.UserName); - if (user == null) { - services.Logger.LogError("The token request was rejected because no user profile corresponding to " + - "the specified username was found: '{Username}'.", context.UserName); + return; + } - context.Reject( - error: OpenIdConnectConstants.Errors.InvalidGrant, - description: "Invalid credentials."); + // Ensure the password is valid. + if (!await services.Users.CheckPasswordAsync(user, context.Request.Password)) { + services.Logger.LogError("The token request was rejected because the password didn't match " + + "the password associated with the account '{Username}'.", context.Request.Username); - return; - } + context.Reject( + error: OpenIdConnectConstants.Errors.InvalidGrant, + description: "Invalid credentials."); - // Ensure the user is allowed to sign in. - if (!await services.SignIn.CanSignInAsync(user)) { - services.Logger.LogError("The token request was rejected because the user '{Username}' " + - "was not allowed to sign in.", context.UserName); + if (services.Users.SupportsUserLockout) { + await services.Users.AccessFailedAsync(user); - context.Reject( - error: OpenIdConnectConstants.Errors.InvalidGrant, - description: "The user is not allowed to sign in."); + // Ensure the user is not locked out. + if (await services.Users.IsLockedOutAsync(user)) { + services.Logger.LogError("The token request was rejected because the account '{Username}' " + + "was locked out to prevent brute force attacks.", context.Request.Username); - return; - } + context.Reject( + error: OpenIdConnectConstants.Errors.InvalidGrant, + description: "Account locked out."); + } + } - // Ensure the user is not already locked out. - if (services.Users.SupportsUserLockout && await services.Users.IsLockedOutAsync(user)) { - services.Logger.LogError("The token request was rejected because the account '{Username}' " + - "was locked out to prevent brute force attacks.", context.UserName); + return; + } - context.Reject( - error: OpenIdConnectConstants.Errors.InvalidGrant, - description: "Account locked out."); + if (services.Users.SupportsUserLockout) { + await services.Users.ResetAccessFailedCountAsync(user); + } - return; - } + // Reject the token request if two-factor authentication has been enabled by the user. + if (services.Users.SupportsUserTwoFactor && await services.Users.GetTwoFactorEnabledAsync(user)) { + services.Logger.LogError("The token request was rejected because two-factor authentication " + + "was required for the account '{Username}.", context.Request.Username); - // Ensure the password is valid. - if (!await services.Users.CheckPasswordAsync(user, context.Password)) { - services.Logger.LogError("The token request was rejected because the password didn't match " + - "the password associated with the account '{Username}'.", context.UserName); + context.Reject( + error: OpenIdConnectConstants.Errors.InvalidGrant, + description: "Two-factor authentication is required for this account."); - context.Reject( - error: OpenIdConnectConstants.Errors.InvalidGrant, - description: "Invalid credentials."); + return; + } - if (services.Users.SupportsUserLockout) { - await services.Users.AccessFailedAsync(user); + // Return an error if the username corresponds to the registered + // email address and if the "email" scope has not been requested. + if (services.Users.SupportsUserEmail && context.Request.HasScope(OpenIdConnectConstants.Scopes.Profile) && + !context.Request.HasScope(OpenIdConnectConstants.Scopes.Email)) { + // Retrieve the username and the email address associated with the user. + var username = await services.Users.GetUserNameAsync(user); + var email = await services.Users.GetEmailAsync(user); - // Ensure the user is not locked out. - if (await services.Users.IsLockedOutAsync(user)) { - services.Logger.LogError("The token request was rejected because the account '{Username}' " + - "was locked out to prevent brute force attacks.", context.UserName); + if (!string.IsNullOrEmpty(email) && string.Equals(username, email, StringComparison.OrdinalIgnoreCase)) { + services.Logger.LogError("The token request was rejected because the 'email' scope was not requested: " + + "to prevent data leakage, the 'email' scope must be granted when the username " + + "is identical to the email address associated with the user profile."); context.Reject( - error: OpenIdConnectConstants.Errors.InvalidGrant, - description: "Account locked out."); + error: OpenIdConnectConstants.Errors.InvalidRequest, + description: "The 'email' scope is required."); + + return; } } - return; - } - - if (services.Users.SupportsUserLockout) { - await services.Users.ResetAccessFailedCountAsync(user); - } + var identity = await services.Users.CreateIdentityAsync(user, context.Request.GetScopes()); + if (identity == null) { + throw new InvalidOperationException("The token request was aborted because the user manager returned a null " + + $"identity for user '{await services.Users.GetUserNameAsync(user)}'."); + } - // Reject the token request if two-factor authentication has been enabled by the user. - if (services.Users.SupportsUserTwoFactor && await services.Users.GetTwoFactorEnabledAsync(user)) { - services.Logger.LogError("The token request was rejected because two-factor authentication " + - "was required for the account '{Username}.", context.UserName); + // Create a new authentication ticket holding the user identity. + var ticket = new AuthenticationTicket( + new ClaimsPrincipal(identity), + new AuthenticationProperties(), + context.Options.AuthenticationScheme); - context.Reject( - error: OpenIdConnectConstants.Errors.InvalidGrant, - description: "Two-factor authentication is required for this account."); + ticket.SetResources(context.Request.GetResources()); + ticket.SetScopes(context.Request.GetScopes()); - return; + context.Validate(ticket); } - // Return an error if the username corresponds to the registered - // email address and if the "email" scope has not been requested. - if (services.Users.SupportsUserEmail && context.Request.HasScope(OpenIdConnectConstants.Scopes.Profile) && - !context.Request.HasScope(OpenIdConnectConstants.Scopes.Email)) { - // Retrieve the username and the email address associated with the user. - var username = await services.Users.GetUserNameAsync(user); - var email = await services.Users.GetEmailAsync(user); + else if (context.Request.IsClientCredentialsGrantType()) { + // Note: at this stage, the client credentials cannot be null or invalid, as client authentication is required + // to use the client credentials grant and is automatically enforced by the OpenID Connect server middleware. + Debug.Assert(!string.IsNullOrEmpty(context.Request.ClientId) && + !string.IsNullOrEmpty(context.Request.ClientSecret), "The client credentials shouldn't be null."); - if (!string.IsNullOrEmpty(email) && string.Equals(username, email, StringComparison.OrdinalIgnoreCase)) { - services.Logger.LogError("The token request was rejected because the 'email' scope was not requested: " + - "to prevent data leakage, the 'email' scope must be granted when the username" + - "is identical to the email address associated with the user ({Username}).", username); + // Retrieve the application details corresponding to the requested client_id. + // Note: this call shouldn't return a null instance, but a race condition may occur + // if the application was removed after the initial check made by ValidateTokenRequest. + var application = await services.Applications.FindByClientIdAsync(context.Request.ClientId); + if (application == null) { + services.Logger.LogError("The token request was aborted because the client application " + + "was not found in the database: '{ClientId}'.", context.Request.ClientId); context.Reject( - error: OpenIdConnectConstants.Errors.InvalidRequest, - description: "The 'email' scope is required."); + error: OpenIdConnectConstants.Errors.InvalidClient, + description: "Application not found in the database: ensure that your client_id is correct."); return; } - } - var identity = await services.Users.CreateIdentityAsync(user, context.Request.GetScopes()); - if (identity == null) { - throw new InvalidOperationException("The token request failed because the user manager returned a null " + - $"identity for user '{await services.Users.GetUserNameAsync(user)}'."); - } + var identity = new ClaimsIdentity(context.Options.AuthenticationScheme); + + // Note: the name identifier is always included in both identity and + // access tokens, even if an explicit destination is not specified. + identity.AddClaim(ClaimTypes.NameIdentifier, context.Request.ClientId); - // Create a new authentication ticket holding the user identity. - var ticket = new AuthenticationTicket( - new ClaimsPrincipal(identity), - new AuthenticationProperties(), - context.Options.AuthenticationScheme); + identity.AddClaim(ClaimTypes.Name, await services.Applications.GetDisplayNameAsync(application), + OpenIdConnectConstants.Destinations.AccessToken, + OpenIdConnectConstants.Destinations.IdentityToken); - ticket.SetResources(context.Request.GetResources()); - ticket.SetScopes(context.Request.GetScopes()); + // Create a new authentication ticket + // holding the application identity. + var ticket = new AuthenticationTicket( + new ClaimsPrincipal(identity), + new AuthenticationProperties(), + context.Options.AuthenticationScheme); - context.Validate(ticket); + ticket.SetResources(context.Request.GetResources()); + ticket.SetScopes(context.Request.GetScopes()); + + context.Validate(ticket); + } } } } \ No newline at end of file diff --git a/src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Revocation.cs b/src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Revocation.cs index 6bc00d09..87d5a7f5 100644 --- a/src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Revocation.cs +++ b/src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Revocation.cs @@ -32,9 +32,9 @@ namespace OpenIddict.Infrastructure { } // Skip client authentication if the client identifier is missing. - // Note: ASOS will automatically ensure that the calling application - // cannot revoke a refresh token if it's not the intended audience, - // even if client authentication was skipped. + // Note: the OpenID Connect server middleware will automatically ensure that + // the calling application cannot revoke a refresh token if it's not + // the intended audience, even if client authentication was skipped. if (string.IsNullOrEmpty(context.ClientId)) { services.Logger.LogInformation("The revocation request validation process was skipped " + "because the client_id parameter was missing or empty."); diff --git a/src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Serialization.cs b/src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Serialization.cs index 5b844603..67dd6a0c 100644 --- a/src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Serialization.cs +++ b/src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Serialization.cs @@ -20,7 +20,7 @@ namespace OpenIddict.Infrastructure { public override async Task SerializeRefreshToken([NotNull] SerializeRefreshTokenContext context) { var services = context.HttpContext.RequestServices.GetRequiredService>(); - Debug.Assert(context.Request.RequestType == OpenIdConnectRequestType.TokenRequest, + Debug.Assert(context.Request.RequestType == OpenIdConnectRequestType.Token, "The request should be a token request."); Debug.Assert(!context.Request.IsClientCredentialsGrantType(), diff --git a/src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Userinfo.cs b/src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Userinfo.cs index 57d991b3..a2c9b1ca 100644 --- a/src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Userinfo.cs +++ b/src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Userinfo.cs @@ -4,8 +4,6 @@ * the license and the contributors participating to this project. */ -using System; -using System.Diagnostics; using System.Security.Claims; using System.Threading.Tasks; using AspNet.Security.OpenIdConnect.Extensions; @@ -18,18 +16,13 @@ using Newtonsoft.Json.Linq; namespace OpenIddict.Infrastructure { public partial class OpenIddictProvider : OpenIdConnectServerProvider where TUser : class where TApplication : class where TAuthorization : class where TScope : class where TToken : class { - public override async Task ValidateUserinfoRequest([NotNull] ValidateUserinfoRequestContext context) { + public override async Task HandleUserinfoRequest([NotNull] HandleUserinfoRequestContext context) { var services = context.HttpContext.RequestServices.GetRequiredService>(); - // Note: the principal returned by AuthenticateAsync cannot be null as the OpenID Connect server - // middleware always ensures the ticket is valid before invoking the ValidateUserinfoRequest event. - var principal = await context.HttpContext.Authentication.AuthenticateAsync(context.Options.AuthenticationScheme); - Debug.Assert(principal != null, "The principal extracted from the access token shouldn't be null."); - - // Ensure the user was not removed from the database. - var user = await services.Users.GetUserAsync(principal); + // Note: user may be null if the user was removed after the access token was issued. + var user = await services.Users.GetUserAsync(context.Ticket.Principal); if (user == null) { - services.Logger.LogError("The userinfo request was rejected because the user profile " + + services.Logger.LogError("The userinfo request was aborted because the user profile " + "corresponding to the access token was not found in the database."); context.Reject( @@ -39,29 +32,14 @@ namespace OpenIddict.Infrastructure { return; } - context.Validate(); - } - - public override async Task HandleUserinfoRequest([NotNull] HandleUserinfoRequestContext context) { - var services = context.HttpContext.RequestServices.GetRequiredService>(); - - // Note: user may be null if the user was removed after - // the initial check made by ValidateUserinfoRequest. - // In this case, throw an exception to abort the request. - var user = await services.Users.GetUserAsync(context.Ticket.Principal); - if (user == null) { - throw new InvalidOperationException("The userinfo request was aborted because the user profile " + - "corresponding to the access token was not found in the database."); - } - // Note: "sub" is a mandatory claim. // See http://openid.net/specs/openid-connect-core-1_0.html#UserInfoResponse context.Subject = await services.Users.GetUserIdAsync(user); // Only add the "preferred_username" claim if the "profile" scope was present in the access token. // Note: filtering the username is not needed at this stage as OpenIddictController.Accept - // and OpenIddictProvider.GrantResourceOwnerCredentials are expected to reject requests that - // don't include the "email" scope if the username corresponds to the registered email address. + // and OpenIddictProvider.HandleTokenRequest are expected to reject requests that don't + // include the "email" scope if the username corresponds to the registed email address. if (context.Ticket.HasScope(OpenIdConnectConstants.Scopes.Profile)) { context.PreferredUsername = await services.Users.GetUserNameAsync(user); @@ -84,8 +62,7 @@ namespace OpenIddict.Infrastructure { }; // Only add the phone number details if the "phone" scope was present in the access token. - if (services.Users.SupportsUserPhoneNumber && - context.Ticket.HasScope(OpenIdConnectConstants.Scopes.Phone)) { + if (services.Users.SupportsUserPhoneNumber && context.Ticket.HasScope(OpenIdConnectConstants.Scopes.Phone)) { context.PhoneNumber = await services.Users.GetPhoneNumberAsync(user); // Only add the "phone_number_verified" diff --git a/src/OpenIddict.Core/OpenIddictBuilder.cs b/src/OpenIddict.Core/OpenIddictBuilder.cs index ed5286c7..c33517a1 100644 --- a/src/OpenIddict.Core/OpenIddictBuilder.cs +++ b/src/OpenIddict.Core/OpenIddictBuilder.cs @@ -6,6 +6,7 @@ using System; using System.ComponentModel; +using System.IdentityModel.Tokens.Jwt; using System.IO; using System.Linq; using System.Reflection; @@ -431,7 +432,7 @@ namespace Microsoft.AspNetCore.Builder { /// /// The . public virtual OpenIddictBuilder UseJsonWebTokens() { - return Configure(options => options.UseJwtTokens()); + return Configure(options => options.AccessTokenHandler = new JwtSecurityTokenHandler()); } } } \ No newline at end of file diff --git a/src/OpenIddict.Core/project.json b/src/OpenIddict.Core/project.json index 847538ad..a63a4d88 100644 --- a/src/OpenIddict.Core/project.json +++ b/src/OpenIddict.Core/project.json @@ -33,11 +33,11 @@ }, "dependencies": { - "AspNet.Security.OpenIdConnect.Server": "1.0.0-beta5-final", - "CryptoHelper": "1.0.0-rc2-final", + "AspNet.Security.OpenIdConnect.Server": "1.0.0-beta6-final", + "CryptoHelper": "2.0.0", "JetBrains.Annotations": { "type": "build", "version": "10.1.4" }, - "Microsoft.AspNetCore.Identity": "1.0.0-rc2-final", - "Microsoft.Extensions.Caching.Memory": "1.0.0-rc2-final" + "Microsoft.AspNetCore.Identity": "1.0.0", + "Microsoft.Extensions.Caching.Memory": "1.0.0" }, "frameworks": { diff --git a/src/OpenIddict.EntityFramework/project.json b/src/OpenIddict.EntityFramework/project.json index c4cab766..ee2c09be 100644 --- a/src/OpenIddict.EntityFramework/project.json +++ b/src/OpenIddict.EntityFramework/project.json @@ -34,7 +34,7 @@ "dependencies": { "JetBrains.Annotations": { "type": "build", "version": "10.1.4" }, - "Microsoft.AspNetCore.Identity.EntityFrameworkCore": "1.0.0-rc2-final", + "Microsoft.AspNetCore.Identity.EntityFrameworkCore": "1.0.0", "OpenIddict.Core": { "target": "project" } }, diff --git a/src/OpenIddict.Mvc/OpenIddictController.cs b/src/OpenIddict.Mvc/OpenIddictController.cs index b9082324..80426ff0 100644 --- a/src/OpenIddict.Mvc/OpenIddictController.cs +++ b/src/OpenIddict.Mvc/OpenIddictController.cs @@ -54,7 +54,7 @@ namespace OpenIddict.Mvc { }); } - // Note: AspNet.Security.OpenIdConnect.Server automatically ensures an application + // Note: the OpenID Connect server middleware automatically ensures an application // corresponds to the client_id specified in the authorization request using // IOpenIdConnectServerProvider.ValidateAuthorizationRequest (see OpenIddictProvider.cs). var application = await applications.FindByClientIdAsync(request.ClientId); @@ -117,7 +117,7 @@ namespace OpenIddict.Mvc { ticket.SetResources(request.GetResources()); ticket.SetScopes(request.GetScopes()); - // Returning a SignInResult will ask ASOS to serialize the specified identity to build appropriate tokens. + // Returning a SignInResult will ask the OpenID Connect server middleware to issue the appropriate tokens. // Note: you should always make sure the identities you return contain ClaimTypes.NameIdentifier claim. // In this sample, the identity always contains the name identifier returned by the external provider. return SignIn(ticket.Principal, ticket.Properties, ticket.AuthenticationScheme); @@ -138,9 +138,9 @@ namespace OpenIddict.Mvc { })); } - // Notify ASOS that the authorization grant has been denied by the resource owner. - // Note: OpenIdConnectServerHandler will automatically take care of redirecting - // the user agent to the client application using the appropriate response_mode. + // Notify the OpenID Connect server middleware that the authorization grant + // has been denied by the resource owner to redirect user agent to + // the client application using the appropriate response_mode. return Task.FromResult(Forbid(options.Value.AuthenticationScheme)); } @@ -171,8 +171,8 @@ namespace OpenIddict.Mvc { // after a successful authentication flow (e.g Google or Facebook). await signin.SignOutAsync(); - // Returning a SignOutResult will ask ASOS to redirect the user agent - // to the post_logout_redirect_uri specified by the client application. + // Returning a SignOutResult will ask the OpenID Connect server middleware to redirect + // the user agent to the post_logout_redirect_uri specified by the client application. return SignOut(options.Value.AuthenticationScheme); } } diff --git a/src/OpenIddict.Mvc/OpenIddictExtensions.cs b/src/OpenIddict.Mvc/OpenIddictExtensions.cs index e885fa24..02f41db2 100644 --- a/src/OpenIddict.Mvc/OpenIddictExtensions.cs +++ b/src/OpenIddict.Mvc/OpenIddictExtensions.cs @@ -7,14 +7,17 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Linq; using System.Reflection; using AspNet.Security.OpenIdConnect.Server; using JetBrains.Annotations; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.ApplicationModels; using Microsoft.AspNetCore.Mvc.ApplicationParts; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyModel; using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.Options; using OpenIddict; @@ -81,8 +84,10 @@ namespace Microsoft.AspNetCore.Builder { // Note: ConfigureApplicationPartManager() must be // called before AddControllersAsServices(). .ConfigureApplicationPartManager(manager => { + var parts = manager.ApplicationParts.ToArray(); + manager.ApplicationParts.Clear(); - manager.ApplicationParts.Add(new OpenIddictPart(builder)); + manager.ApplicationParts.Add(new OpenIddictPart(builder, parts)); }) .AddControllersAsServices() @@ -169,30 +174,64 @@ namespace Microsoft.AspNetCore.Builder { private class OpenIddictConvention : IControllerModelConvention { public void Apply(ControllerModel controller) { // Ensure the convention is only applied to the intended controller. - Debug.Assert(controller.ControllerType != null); - Debug.Assert(controller.ControllerType.IsGenericType); - Debug.Assert(controller.ControllerType.GetGenericTypeDefinition() == typeof(OpenIddictController<,,,>)); - - // Note: manually updating the controller name is required - // to remove the ending markers added to the generic type name. - controller.ControllerName = "OpenIddict"; + if (controller.ControllerType != null && + controller.ControllerType.IsGenericType && + controller.ControllerType.GetGenericTypeDefinition() == typeof(OpenIddictController<,,,>)) { + // Note: manually updating the controller name is required + // to remove the ending markers added to the generic type name. + controller.ControllerName = "OpenIddict"; + } } } - private class OpenIddictPart : ApplicationPart, IApplicationPartTypeProvider { - public OpenIddictPart(OpenIddictBuilder builder) { - Types = new[] { + private class OpenIddictPart : ApplicationPart, IApplicationPartTypeProvider, ICompilationReferencesProvider { + public OpenIddictPart(OpenIddictBuilder builder, IEnumerable parts) { + var types = new List { typeof(OpenIddictController<,,,>).MakeGenericType( /* TUser: */ builder.UserType, /* TApplication: */ builder.ApplicationType, /* TAuthorization: */ builder.AuthorizationType, /* TToken: */ builder.TokenType).GetTypeInfo() }; + + var assemblies = new List(); + + foreach (var part in parts.OfType()) { + assemblies.Add(part.Assembly); + + foreach (var type in part.Types) { + if (typeof(ControllerBase).GetTypeInfo().IsAssignableFrom(type)) { + continue; + } + + types.Add(type); + } + } + + Assemblies = assemblies; + Types = types; } public override string Name { get; } = "OpenIddict.Mvc"; + public IEnumerable Assemblies { get; } + public IEnumerable Types { get; } + + public IEnumerable GetReferencePaths() { + foreach (var assembly in Assemblies) { + var context = DependencyContext.Load(assembly); + if (context == null) { + continue; + } + + foreach (var library in context.CompileLibraries) { + foreach (var path in library.ResolveReferencePaths()) { + yield return path; + } + } + } + } } } } \ No newline at end of file diff --git a/src/OpenIddict.Mvc/project.json b/src/OpenIddict.Mvc/project.json index 8a7840d2..7f946e5d 100644 --- a/src/OpenIddict.Mvc/project.json +++ b/src/OpenIddict.Mvc/project.json @@ -30,6 +30,7 @@ "warningsAsErrors": true, "nowarn": [ "CS1591" ], "xmlDoc": true, + "preserveCompilationContext": true, "embed": { "include": [ "Views/**" ] @@ -37,20 +38,20 @@ }, "dependencies": { - "AspNet.Hosting.Extensions": "1.0.0-alpha2-final", + "AspNet.Hosting.Extensions": "1.0.0-alpha3-final", "JetBrains.Annotations": { "type": "build", "version": "10.1.4" }, - "Microsoft.AspNetCore.Mvc": "1.0.0-rc2-final", - "Microsoft.Extensions.FileProviders.Embedded": "1.0.0-rc2-final", - "Microsoft.Extensions.FileProviders.Composite": "1.0.0-rc2-final", + "Microsoft.AspNetCore.Mvc": "1.0.0", + "Microsoft.Extensions.FileProviders.Embedded": "1.0.0", + "Microsoft.Extensions.FileProviders.Composite": "1.0.0", "OpenIddict.Core": { "target": "project" } }, "frameworks": { "net451": { }, - "netstandard1.5": { + "netstandard1.6": { "imports": [ - "dotnet5.6", + "dotnet5.7", "portable-net451+win8" ] } diff --git a/src/OpenIddict.Security/project.json b/src/OpenIddict.Security/project.json index 303290a4..48cf98f1 100644 --- a/src/OpenIddict.Security/project.json +++ b/src/OpenIddict.Security/project.json @@ -34,17 +34,17 @@ "dependencies": { "JetBrains.Annotations": { "type": "build", "version": "10.1.4" }, - "Microsoft.AspNetCore.Cors": "1.0.0-rc2-final", - "NWebsec.AspNetCore.Middleware": "1.0.0-gamma-5", + "Microsoft.AspNetCore.Cors": "1.0.0", + "NWebsec.AspNetCore.Middleware": "1.0.0-gamma1-15", "OpenIddict.Core": { "target": "project" } }, "frameworks": { "net451": { }, - "netstandard1.5": { + "netstandard1.4": { "imports": [ - "dotnet5.6", + "dotnet5.5", "portable-net451+win8" ] }