diff --git a/OpenIddict.sln b/OpenIddict.sln index 694dafa7..2e1abd3a 100644 --- a/OpenIddict.sln +++ b/OpenIddict.sln @@ -15,9 +15,7 @@ Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Mvc.Server", "samples\Mvc.S EndProject Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "OpenIddict.Assets", "src\OpenIddict.Assets\OpenIddict.Assets.xproj", "{86293E11-DD31-4D54-BCAD-8788B5C9972F}" EndProject -Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "OpenIddict.Models", "src\OpenIddict.Models\OpenIddict.Models.xproj", "{79AE02C3-2AB4-4495-BEDD-685A714EA51C}" -EndProject -Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "OpenIddict.EF", "src\OpenIddict.EF\OpenIddict.EF.xproj", "{D2450929-ED0E-420D-B475-327924F9701C}" +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "OpenIddict.EntityFramework", "src\OpenIddict.EntityFramework\OpenIddict.EntityFramework.xproj", "{D2450929-ED0E-420D-B475-327924F9701C}" EndProject Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "OpenIddict.Core", "src\OpenIddict.Core\OpenIddict.Core.xproj", "{E60CF8CA-6313-4359-BE43-AFCBB927EA30}" EndProject @@ -47,10 +45,6 @@ Global {86293E11-DD31-4D54-BCAD-8788B5C9972F}.Debug|Any CPU.Build.0 = Debug|Any CPU {86293E11-DD31-4D54-BCAD-8788B5C9972F}.Release|Any CPU.ActiveCfg = Release|Any CPU {86293E11-DD31-4D54-BCAD-8788B5C9972F}.Release|Any CPU.Build.0 = Release|Any CPU - {79AE02C3-2AB4-4495-BEDD-685A714EA51C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {79AE02C3-2AB4-4495-BEDD-685A714EA51C}.Debug|Any CPU.Build.0 = Debug|Any CPU - {79AE02C3-2AB4-4495-BEDD-685A714EA51C}.Release|Any CPU.ActiveCfg = Release|Any CPU - {79AE02C3-2AB4-4495-BEDD-685A714EA51C}.Release|Any CPU.Build.0 = Release|Any CPU {D2450929-ED0E-420D-B475-327924F9701C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {D2450929-ED0E-420D-B475-327924F9701C}.Debug|Any CPU.Build.0 = Debug|Any CPU {D2450929-ED0E-420D-B475-327924F9701C}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -76,7 +70,6 @@ Global {96B22EB9-771A-4DCA-B828-E6EA2774CF1B} = {F47D1283-0EE9-4728-8026-58405C29B786} {7CBEAFD2-E3D0-4424-9B78-E87AB52327A6} = {F47D1283-0EE9-4728-8026-58405C29B786} {86293E11-DD31-4D54-BCAD-8788B5C9972F} = {D544447C-D701-46BB-9A5B-C76C612A596B} - {79AE02C3-2AB4-4495-BEDD-685A714EA51C} = {D544447C-D701-46BB-9A5B-C76C612A596B} {D2450929-ED0E-420D-B475-327924F9701C} = {D544447C-D701-46BB-9A5B-C76C612A596B} {E60CF8CA-6313-4359-BE43-AFCBB927EA30} = {D544447C-D701-46BB-9A5B-C76C612A596B} {7AE46E2F-E93B-4FF9-B941-6CD7A3E1BF84} = {D544447C-D701-46BB-9A5B-C76C612A596B} diff --git a/samples/Mvc.Client/project.json b/samples/Mvc.Client/project.json index 4d6cdaca..97799596 100644 --- a/samples/Mvc.Client/project.json +++ b/samples/Mvc.Client/project.json @@ -1,6 +1,5 @@ { "buildOptions": { - "debugType": "portable", "emitEntryPoint": true, "warningsAsErrors": true, "preserveCompilationContext": true, diff --git a/samples/Mvc.Server/Models/ApplicationUser.cs b/samples/Mvc.Server/Models/ApplicationUser.cs index 7a885488..0a873641 100644 --- a/samples/Mvc.Server/Models/ApplicationUser.cs +++ b/samples/Mvc.Server/Models/ApplicationUser.cs @@ -1,6 +1,6 @@ -using Microsoft.AspNetCore.Identity.EntityFrameworkCore; +using OpenIddict; namespace Mvc.Server.Models { // Add profile data for application users by adding properties to the ApplicationUser class - public class ApplicationUser : IdentityUser { } + public class ApplicationUser : OpenIddictUser { } } diff --git a/samples/Mvc.Server/Startup.cs b/samples/Mvc.Server/Startup.cs index 7c3f37cd..e99a1b5c 100644 --- a/samples/Mvc.Server/Startup.cs +++ b/samples/Mvc.Server/Startup.cs @@ -1,9 +1,7 @@ using System.Linq; -using System.Reflection; using AspNet.Security.OAuth.GitHub; using CryptoHelper; using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity.EntityFrameworkCore; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; @@ -13,7 +11,6 @@ using Mvc.Server.Models; using Mvc.Server.Services; using NWebsec.AspNetCore.Middleware; using OpenIddict; -using OpenIddict.Models; namespace Mvc.Server { public class Startup { @@ -30,10 +27,60 @@ namespace Mvc.Server { .AddDbContext(options => options.UseSqlServer(configuration["Data:DefaultConnection:ConnectionString"])); + // Register the Identity services. services.AddIdentity() .AddEntityFrameworkStores() - .AddDefaultTokenProviders() - .AddOpenIddict(); + .AddDefaultTokenProviders(); + + // Register the OpenIddict services, including the default Entity Framework stores. + services.AddOpenIddict() + // Register the HTML/CSS assets and MVC modules to handle the interactive flows. + // Note: these modules are not necessary when using your own authorization controller + // or when using non-interactive flows-only like the resource owner password credentials grant. + .AddAssets() + .AddMvc() + + // Register the NWebsec module. Note: you can replace the default Content Security Policy (CSP) + // by calling UseNWebsec with a custom delegate instead of using the parameterless extension. + // This can be useful to allow your HTML views to reference remote scripts/images/styles. + .AddNWebsec(options => options.DefaultSources(directive => directive.Self()) + .ImageSources(directive => directive.Self() + .CustomSources("*")) + .ScriptSources(directive => directive.Self() + .UnsafeInline() + .CustomSources("https://my.custom.url/")) + .StyleSources(directive => directive.Self() + .UnsafeInline())) + + // During development, you can disable the HTTPS requirement. + .DisableHttpsRequirement(); + + // When using your own authorization controller instead of using the + // MVC module, you need to configure the authorization/logout paths: + // services.AddOpenIddict() + // .SetAuthorizationEndpointPath("/connect/authorize") + // .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. + // + // 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: + // https://s3.amazonaws.com/pluralsight-free/keith-brown/samples/SelfCert.zip + // + // services.AddOpenIddict() + // .AddSigningCertificate("7D2A741FE34CC2C7369237A5F2078988E17A6A75"); + // + // Alternatively, you can also store the certificate as an embedded .pfx resource + // directly in this assembly or in a file published alongside this project: + // + // services.AddOpenIddict() + // .AddSigningCertificate( + // assembly: typeof(Startup).GetTypeInfo().Assembly, + // resource: "Mvc.Server.Certificate.pfx", + // password: "OpenIddict"); services.AddTransient(); services.AddTransient(); @@ -79,46 +126,12 @@ namespace Mvc.Server { Scope = { "user:email" } }); - // Note: OpenIddict must be added after - // ASP.NET Identity and the external providers. - app.UseOpenIddict(builder => { - builder.Options.AllowInsecureHttp = true; - - // 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. - // - // 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: - // https://s3.amazonaws.com/pluralsight-free/keith-brown/samples/SelfCert.zip - // - // builder.UseSigningCertificate("7D2A741FE34CC2C7369237A5F2078988E17A6A75"); - // - // Alternatively, you can also store the certificate as an embedded .pfx resource - // directly in this assembly or in a file published alongside this project: - // - // builder.UseSigningCertificate( - // assembly: typeof(Startup).GetTypeInfo().Assembly, - // resource: "Nancy.Server.Certificate.pfx", - // password: "Owin.Security.OpenIdConnect.Server"); - - // You can customize the default Content Security Policy (CSP) by calling UseNWebsec explicitly. - // This can be useful to allow your HTML views to reference remote scripts/images/styles. - builder.UseNWebsec(directives => { - directives.DefaultSources(directive => directive.Self()) - .ImageSources(directive => directive.Self().CustomSources("*")) - .ScriptSources(directive => directive - .Self() - .UnsafeInline() - .CustomSources("https://my.custom.url")) - .StyleSources(directive => directive.Self().UnsafeInline()); - }); - }); + app.UseOpenIddict(); app.UseMvcWithDefaultRoute(); - using (var context = app.ApplicationServices.GetRequiredService()) { + using (var context = new ApplicationDbContext( + app.ApplicationServices.GetRequiredService>())) { context.Database.EnsureCreated(); // Add Mvc.Client to the known applications. @@ -126,21 +139,20 @@ namespace Mvc.Server { // Note: when using the introspection middleware, your resource server // MUST be registered as an OAuth2 client and have valid credentials. // - // context.Applications.Add(new Application { + // context.Applications.Add(new OpenIddictApplication { // Id = "resource_server", // DisplayName = "Main resource server", - // Secret = "875sqd4s5d748z78z7ds1ff8zz8814ff88ed8ea4z4zzd" + // Secret = "875sqd4s5d748z78z7ds1ff8zz8814ff88ed8ea4z4zzd", + // Type = OpenIddictConstants.ClientTypes.Confidential // }); - var hasher = new PasswordHasher(); - - context.Applications.Add(new Application { + context.Applications.Add(new OpenIddictApplication { Id = "myClient", DisplayName = "My client application", RedirectUri = "http://localhost:53507/signin-oidc", LogoutRedirectUri = "http://localhost:53507/", Secret = Crypto.HashPassword("secret_secret_secret"), - Type = OpenIddictConstants.ApplicationTypes.Confidential + Type = OpenIddictConstants.ClientTypes.Confidential }); // To test this sample with Postman, use the following settings: @@ -152,11 +164,11 @@ namespace Mvc.Server { // * Scope: openid email profile roles // * Grant type: authorization code // * Request access token locally: yes - context.Applications.Add(new Application { + context.Applications.Add(new OpenIddictApplication { Id = "postman", DisplayName = "Postman", RedirectUri = "https://www.getpostman.com/oauth2/callback", - Type = OpenIddictConstants.ApplicationTypes.Public + Type = OpenIddictConstants.ClientTypes.Public }); context.SaveChanges(); diff --git a/samples/Mvc.Server/project.json b/samples/Mvc.Server/project.json index d18c7e34..99237f62 100644 --- a/samples/Mvc.Server/project.json +++ b/samples/Mvc.Server/project.json @@ -1,6 +1,5 @@ { "buildOptions": { - "debugType": "portable", "emitEntryPoint": true, "warningsAsErrors": true, "preserveCompilationContext": true, @@ -36,7 +35,10 @@ "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", - "OpenIddict": { "target": "project" } + "OpenIddict": { "target": "project" }, + "OpenIddict.Assets": { "target": "project" }, + "OpenIddict.Mvc": { "target": "project" }, + "OpenIddict.Security": { "target": "project" } }, "frameworks": { diff --git a/src/OpenIddict.Assets/OpenIddictExtensions.cs b/src/OpenIddict.Assets/OpenIddictExtensions.cs index a16341e0..6ef7bc10 100644 --- a/src/OpenIddict.Assets/OpenIddictExtensions.cs +++ b/src/OpenIddict.Assets/OpenIddictExtensions.cs @@ -11,7 +11,12 @@ using Microsoft.Extensions.FileProviders; namespace Microsoft.AspNetCore.Builder { public static class OpenIddictExtensions { - public static OpenIddictBuilder UseAssets([NotNull] this OpenIddictBuilder builder) { + /// + /// Registers the assets module, including the default HTML/CSS files used by the MVC module. + /// + /// The services builder used by OpenIddict to register new services. + /// The . + public static OpenIddictBuilder AddAssets([NotNull] this OpenIddictBuilder builder) { if (builder == null) { throw new ArgumentNullException(nameof(builder)); } diff --git a/src/OpenIddict.Core/IOpenIddictStore.cs b/src/OpenIddict.Core/IOpenIddictStore.cs deleted file mode 100644 index b8a2016d..00000000 --- a/src/OpenIddict.Core/IOpenIddictStore.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System.Threading; -using System.Threading.Tasks; - -namespace OpenIddict { - public interface IOpenIddictStore where TApplication : class { - Task FindApplicationByIdAsync(string identifier, CancellationToken cancellationToken); - Task FindApplicationByLogoutRedirectUri(string url, CancellationToken cancellationToken); - Task GetApplicationTypeAsync(TApplication application, CancellationToken cancellationToken); - Task GetDisplayNameAsync(TApplication application, CancellationToken cancellationToken); - Task GetRedirectUriAsync(TApplication application, CancellationToken cancellationToken); - Task GetHashedSecretAsync(TApplication application, CancellationToken cancellationToken); - } -} \ No newline at end of file diff --git a/src/OpenIddict.Core/Infrastructure/OpenIddictHelpers.cs b/src/OpenIddict.Core/Infrastructure/OpenIddictHelpers.cs new file mode 100644 index 00000000..2bc61ca8 --- /dev/null +++ b/src/OpenIddict.Core/Infrastructure/OpenIddictHelpers.cs @@ -0,0 +1,37 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +using System; +using System.Linq; +using System.Threading.Tasks; +using JetBrains.Annotations; +using Microsoft.AspNetCore.Identity; + +namespace OpenIddict.Infrastructure { + public static class OpenIddictHelpers { + public static async Task FindClaimAsync( + [NotNull] this UserManager manager, + [NotNull] TUser user, [NotNull] string type) where TUser : class { + if (manager == null) { + throw new ArgumentNullException(nameof(manager)); + } + + if (user == null) { + throw new ArgumentNullException(nameof(user)); + } + + if (string.IsNullOrEmpty(type)) { + throw new ArgumentNullException(nameof(type)); + } + + // Note: GetClaimsAsync will automatically throw an exception + // if the underlying store doesn't support custom claims. + return (from claim in await manager.GetClaimsAsync(user) + where string.Equals(claim.Type, type, StringComparison.OrdinalIgnoreCase) + select claim.Value).FirstOrDefault(); + } + } +} diff --git a/src/OpenIddict.Core/OpenIddictProvider.Authentication.cs b/src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Authentication.cs similarity index 90% rename from src/OpenIddict.Core/OpenIddictProvider.Authentication.cs rename to src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Authentication.cs index 18e9101f..e7fb0d4d 100644 --- a/src/OpenIddict.Core/OpenIddictProvider.Authentication.cs +++ b/src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Authentication.cs @@ -6,7 +6,6 @@ using System; using System.Diagnostics; -using System.IdentityModel.Tokens.Jwt; using System.Linq; using System.Security.Claims; using System.Threading.Tasks; @@ -17,10 +16,11 @@ using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Http.Authentication; using Microsoft.Extensions.DependencyInjection; -namespace OpenIddict { - public partial class OpenIddictProvider : OpenIdConnectServerProvider where TUser : class where TApplication : class { +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 ValidateAuthorizationRequest([NotNull] ValidateAuthorizationRequestContext context) { - var services = context.HttpContext.RequestServices.GetRequiredService>(); + var services = context.HttpContext.RequestServices.GetRequiredService>(); // Note: redirect_uri is not required for pure OAuth2 requests // but this provider uses a stricter policy making it mandatory, @@ -35,7 +35,7 @@ namespace OpenIddict { } // Retrieve the application details corresponding to the requested client_id. - var application = await services.Applications.FindApplicationByIdAsync(context.ClientId); + var application = await services.Applications.FindByIdAsync(context.ClientId); if (application == null) { context.Reject( error: OpenIdConnectConstants.Errors.InvalidClient, @@ -54,9 +54,11 @@ namespace OpenIddict { // To prevent downgrade attacks, ensure that authorization requests using the hybrid/implicit // flow are rejected if the client identifier corresponds to a confidential application. - // Note: when using the authorization code grant, ValidateClientAuthentication is responsible of + // Note: when using the authorization code grant, ValidateTokenRequest is responsible of // rejecting the token request if the client_id corresponds to an unauthenticated confidential client. - if (await services.Applications.IsConfidentialApplicationAsync(application) && !context.Request.IsAuthorizationCodeFlow()) { + var type = await services.Applications.GetClientTypeAsync(application); + if (!string.Equals(type, OpenIddictConstants.ClientTypes.Public, StringComparison.OrdinalIgnoreCase) && + !context.Request.IsAuthorizationCodeFlow()) { context.Reject( error: OpenIdConnectConstants.Errors.InvalidRequest, description: "Confidential clients can only use response_type=code."); @@ -144,13 +146,13 @@ namespace OpenIddict { } public override async Task HandleAuthorizationRequest([NotNull] HandleAuthorizationRequestContext context) { + var services = context.HttpContext.RequestServices.GetRequiredService>(); + // Only handle prompt=none requests at this stage. if (!string.Equals(context.Request.Prompt, "none", StringComparison.Ordinal)) { return; } - var services = context.HttpContext.RequestServices.GetRequiredService>(); - // Note: principal is guaranteed to be non-null since ValidateAuthorizationRequest // rejects prompt=none requests missing or having an invalid id_token_hint. var principal = await context.HttpContext.Authentication.AuthenticateAsync(context.Options.AuthenticationScheme); @@ -168,7 +170,7 @@ namespace OpenIddict { // 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. - var identity = await services.Applications.CreateIdentityAsync(user, context.Request.GetScopes()); + var identity = await services.Tokens.CreateIdentityAsync(user, context.Request.GetScopes()); Debug.Assert(identity != null); // Create a new authentication ticket holding the user identity. diff --git a/src/OpenIddict.Core/OpenIddictProvider.Exchange.cs b/src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Exchange.cs similarity index 79% rename from src/OpenIddict.Core/OpenIddictProvider.Exchange.cs rename to src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Exchange.cs index 8db5512e..6700b31d 100644 --- a/src/OpenIddict.Core/OpenIddictProvider.Exchange.cs +++ b/src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Exchange.cs @@ -12,15 +12,14 @@ using AspNet.Security.OpenIdConnect.Extensions; using AspNet.Security.OpenIdConnect.Server; using JetBrains.Annotations; using Microsoft.AspNetCore.Authentication; -using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http.Authentication; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Options; -namespace OpenIddict { - public partial class OpenIddictProvider : OpenIdConnectServerProvider where TUser : class where TApplication : class { +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 ValidateTokenRequest([NotNull] ValidateTokenRequestContext context) { - var services = context.HttpContext.RequestServices.GetRequiredService>(); + var services = context.HttpContext.RequestServices.GetRequiredService>(); // Note: OpenIdConnectServerHandler supports authorization code, refresh token, // client credentials, resource owner password credentials and custom grants @@ -53,7 +52,7 @@ namespace OpenIddict { } // Retrieve the application details corresponding to the requested client_id. - var application = await services.Applications.FindApplicationByIdAsync(context.ClientId); + var application = await services.Applications.FindByIdAsync(context.ClientId); if (application == null) { context.Reject( error: OpenIdConnectConstants.Errors.InvalidClient, @@ -63,7 +62,9 @@ namespace OpenIddict { } // Reject tokens requests containing a client_secret if the client application is not confidential. - if (await services.Applications.IsPublicApplicationAsync(application) && !string.IsNullOrEmpty(context.ClientSecret)) { + var type = await services.Applications.GetClientTypeAsync(application); + if (!string.Equals(type, OpenIddictConstants.ClientTypes.Confidential, StringComparison.OrdinalIgnoreCase) && + !string.IsNullOrEmpty(context.ClientSecret)) { context.Reject( error: OpenIdConnectConstants.Errors.InvalidRequest, description: "Public clients are not allowed to send a client_secret."); @@ -71,9 +72,8 @@ namespace OpenIddict { return; } - // Confidential applications MUST authenticate - // to protect them from impersonation attacks. - else if (await services.Applications.IsConfidentialApplicationAsync(application)) { + // Confidential applications MUST authenticate to protect them from impersonation attacks. + else if (!string.Equals(type, OpenIddictConstants.ClientTypes.Public)) { if (string.IsNullOrEmpty(context.ClientSecret)) { context.Reject( error: OpenIdConnectConstants.Errors.InvalidClient, @@ -95,10 +95,10 @@ namespace OpenIddict { } public override async Task GrantClientCredentials([NotNull] GrantClientCredentialsContext context) { - var services = context.HttpContext.RequestServices.GetRequiredService>(); + var services = context.HttpContext.RequestServices.GetRequiredService>(); // Retrieve the application details corresponding to the requested client_id. - var application = await services.Applications.FindApplicationByIdAsync(context.ClientId); + var application = await services.Applications.FindByIdAsync(context.ClientId); Debug.Assert(application != null); var identity = new ClaimsIdentity(context.Options.AuthenticationScheme); @@ -125,8 +125,7 @@ namespace OpenIddict { } public override async Task GrantRefreshToken([NotNull] GrantRefreshTokenContext context) { - var services = context.HttpContext.RequestServices.GetRequiredService>(); - var options = context.HttpContext.RequestServices.GetRequiredService>(); + var services = context.HttpContext.RequestServices.GetRequiredService>(); var principal = context.Ticket?.Principal; Debug.Assert(principal != null); @@ -140,23 +139,37 @@ namespace OpenIddict { return; } - // If the user manager supports security stamps, - // ensure that the refresh token is still valid. - if (services.Users.SupportsUserSecurityStamp) { - var identifier = principal.GetClaim(options.Value.ClaimsIdentity.SecurityStampClaimType); - if (!string.IsNullOrEmpty(identifier) && - !string.Equals(identifier, await services.Users.GetSecurityStampAsync(user), StringComparison.Ordinal)) { - context.Reject( - error: OpenIdConnectConstants.Errors.InvalidGrant, - description: "The refresh token is no longer valid."); + // Try to extract the token identifier from the authentication ticket. + // If the identifier cannot be found, the revocation check is skipped. + var identifier = context.Ticket.GetTicketId(); + if (string.IsNullOrEmpty(identifier)) { + context.Reject( + error: OpenIdConnectConstants.Errors.InvalidGrant, + description: "The refresh token is no longer valid."); - return; - } + return; + } + + // Retrieve the token from the database and ensure it is still valid. + var token = await services.Tokens.FindByIdAsync(identifier); + if (token == null) { + context.Reject( + error: OpenIdConnectConstants.Errors.InvalidGrant, + description: "The refresh token is no longer valid."); + + 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); } // 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.Applications.CreateIdentityAsync(user, context.Ticket.GetScopes()); + var identity = await services.Tokens.CreateIdentityAsync(user, context.Ticket.GetScopes()); Debug.Assert(identity != null); // Create a new authentication ticket holding the user identity but @@ -170,7 +183,7 @@ namespace OpenIddict { } public override async Task GrantResourceOwnerCredentials([NotNull] GrantResourceOwnerCredentialsContext context) { - var services = context.HttpContext.RequestServices.GetRequiredService>(); + var services = context.HttpContext.RequestServices.GetRequiredService>(); var user = await services.Users.FindByNameAsync(context.UserName); if (user == null) { @@ -198,7 +211,7 @@ namespace OpenIddict { return; } - + // Ensure the password is valid. if (!await services.Users.CheckPasswordAsync(user, context.Password)) { context.Reject( @@ -249,7 +262,7 @@ namespace OpenIddict { } } - var identity = await services.Applications.CreateIdentityAsync(user, context.Request.GetScopes()); + var identity = await services.Tokens.CreateIdentityAsync(user, context.Request.GetScopes()); Debug.Assert(identity != null); // Create a new authentication ticket holding the user identity. diff --git a/src/OpenIddict.Core/OpenIddictProvider.Introspection.cs b/src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Introspection.cs similarity index 71% rename from src/OpenIddict.Core/OpenIddictProvider.Introspection.cs rename to src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Introspection.cs index 4f4f5dd3..49a6a3d7 100644 --- a/src/OpenIddict.Core/OpenIddictProvider.Introspection.cs +++ b/src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Introspection.cs @@ -10,14 +10,13 @@ using System.Threading.Tasks; using AspNet.Security.OpenIdConnect.Extensions; using AspNet.Security.OpenIdConnect.Server; using JetBrains.Annotations; -using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Options; -namespace OpenIddict { - public partial class OpenIddictProvider : OpenIdConnectServerProvider where TUser : class where TApplication : class { +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 ValidateIntrospectionRequest([NotNull] ValidateIntrospectionRequestContext context) { - var services = context.HttpContext.RequestServices.GetRequiredService>(); + var services = context.HttpContext.RequestServices.GetRequiredService>(); // Note: ASOS supports both GET and POST introspection requests but OpenIddict only accepts POST requests. if (!string.Equals(context.HttpContext.Request.Method, "POST", StringComparison.OrdinalIgnoreCase)) { @@ -41,7 +40,7 @@ namespace OpenIddict { } // Retrieve the application details corresponding to the requested client_id. - var application = await services.Applications.FindApplicationByIdAsync(context.ClientId); + var application = await services.Applications.FindByIdAsync(context.ClientId); if (application == null) { context.Reject( error: OpenIdConnectConstants.Errors.InvalidClient, @@ -51,7 +50,8 @@ namespace OpenIddict { } // Reject non-confidential applications. - if (await services.Applications.IsPublicApplicationAsync(application)) { + var type = await services.Applications.GetClientTypeAsync(application); + if (!string.Equals(type, OpenIddictConstants.ClientTypes.Confidential, StringComparison.OrdinalIgnoreCase)) { context.Reject( error: OpenIdConnectConstants.Errors.InvalidClient, description: "Public applications are not allowed to use the introspection endpoint."); @@ -72,14 +72,7 @@ namespace OpenIddict { } public override async Task HandleIntrospectionRequest([NotNull] HandleIntrospectionRequestContext context) { - var services = context.HttpContext.RequestServices.GetRequiredService>(); - var options = context.HttpContext.RequestServices.GetRequiredService>(); - - // If the user manager doesn't support security - // stamps, skip the additional validation logic. - if (!services.Users.SupportsUserSecurityStamp) { - return; - } + var services = context.HttpContext.RequestServices.GetRequiredService>(); var principal = context.Ticket?.Principal; Debug.Assert(principal != null); @@ -91,12 +84,16 @@ namespace OpenIddict { return; } - var identifier = principal.GetClaim(options.Value.ClaimsIdentity.SecurityStampClaimType); - if (!string.IsNullOrEmpty(identifier) && - !string.Equals(identifier, await services.Users.GetSecurityStampAsync(user), StringComparison.Ordinal)) { - context.Active = false; + // When the received ticket is a refresh token, ensure it is still valid. + if (context.Ticket.IsRefreshToken()) { + // Retrieve the token from the database using the unique identifier stored in the refresh token: + // if the corresponding entry cannot be found, return Active = false to indicate that is is no longer valid. + var token = await services.Tokens.FindByIdAsync(context.Ticket.GetTicketId()); + if (token == null) { + context.Active = false; - return; + return; + } } } } diff --git a/src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Revocation.cs b/src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Revocation.cs new file mode 100644 index 00000000..212dac84 --- /dev/null +++ b/src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Revocation.cs @@ -0,0 +1,120 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +using System; +using System.Threading.Tasks; +using AspNet.Security.OpenIdConnect.Extensions; +using AspNet.Security.OpenIdConnect.Server; +using JetBrains.Annotations; +using Microsoft.Extensions.DependencyInjection; + +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 ValidateRevocationRequest([NotNull] ValidateRevocationRequestContext context) { + var services = context.HttpContext.RequestServices.GetRequiredService>(); + + // When token_type_hint is specified, reject the request + // if token_type_hint is not equal to "refresh_token". + if (!string.IsNullOrEmpty(context.Request.GetTokenTypeHint()) && + !string.Equals(context.Request.GetTokenTypeHint(), OpenIdConnectConstants.TokenTypeHints.RefreshToken)) { + context.Reject( + error: OpenIdConnectConstants.Errors.UnsupportedTokenType, + description: "Only refresh tokens can be revoked. When specifying a token_type_hint " + + "parameter, its value must be equal to 'refresh_token'."); + + return; + } + + // 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. + if (string.IsNullOrEmpty(context.ClientId)) { + context.Skip(); + + return; + } + + // Retrieve the application details corresponding to the requested client_id. + var application = await services.Applications.FindByIdAsync(context.ClientId); + if (application == null) { + context.Reject( + error: OpenIdConnectConstants.Errors.InvalidClient, + description: "Application not found in the database: ensure that your client_id is correct."); + + return; + } + + // Reject tokens requests containing a client_secret if the client application is not confidential. + var type = await services.Applications.GetClientTypeAsync(application); + if (!string.Equals(type, OpenIddictConstants.ClientTypes.Confidential, StringComparison.OrdinalIgnoreCase) && + !string.IsNullOrEmpty(context.ClientSecret)) { + context.Reject( + error: OpenIdConnectConstants.Errors.InvalidRequest, + description: "Public clients are not allowed to send a client_secret."); + + return; + } + + // Confidential applications MUST authenticate to protect them from impersonation attacks. + else if (!string.Equals(type, OpenIddictConstants.ClientTypes.Public)) { + if (string.IsNullOrEmpty(context.ClientSecret)) { + context.Reject( + error: OpenIdConnectConstants.Errors.InvalidClient, + description: "Missing credentials: ensure that you specified a client_secret."); + + return; + } + + if (!await services.Applications.ValidateSecretAsync(application, context.ClientSecret)) { + context.Reject( + error: OpenIdConnectConstants.Errors.InvalidClient, + description: "Invalid credentials: ensure that you specified a correct client_secret."); + + return; + } + } + + context.Validate(); + } + + public override async Task HandleRevocationRequest([NotNull] HandleRevocationRequestContext context) { + var services = context.HttpContext.RequestServices.GetRequiredService>(); + + // If the received token is not a refresh token, set Revoked + // to false to indicate that the token cannot be revoked. + if (!context.Ticket.IsRefreshToken()) { + context.Revoked = false; + + return; + } + + // Extract the token identifier from the authentication ticket. + // If the identifier cannot be extracted, abort the revocation. + var identifier = context.Ticket.GetTicketId(); + if (string.IsNullOrEmpty(identifier)) { + context.Revoked = true; + + return; + } + + // Retrieve the token from the database. If the token cannot be found, + // assume it is invalid and consider the revocation as successful. + var token = await services.Tokens.FindByIdAsync(identifier); + if (token == null) { + context.Revoked = true; + + return; + } + + // Revoke the refresh token. + await services.Tokens.RevokeAsync(token); + + context.Revoked = true; + } + } +} \ No newline at end of file diff --git a/src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Serialization.cs b/src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Serialization.cs new file mode 100644 index 00000000..d462e9a8 --- /dev/null +++ b/src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Serialization.cs @@ -0,0 +1,30 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +using System.Diagnostics; +using System.Threading.Tasks; +using AspNet.Security.OpenIdConnect.Extensions; +using AspNet.Security.OpenIdConnect.Server; +using JetBrains.Annotations; +using Microsoft.Extensions.DependencyInjection; + +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 SerializeRefreshToken([NotNull] SerializeRefreshTokenContext context) { + var services = context.HttpContext.RequestServices.GetRequiredService>(); + + // Note: the identifier cannot be null as it's always + // generated by the OpenID Connect server middleware + // before invoking the SerializeRefreshToken event. + Debug.Assert(!string.IsNullOrEmpty(context.Ticket.GetTicketId()), + "The unique identifier associated with the refresh token was missing or empty."); + + // Only persist the refresh token identifier in the database. + await services.Tokens.CreateAsync(context.Ticket.GetTicketId(), OpenIdConnectConstants.TokenTypeHints.RefreshToken); + } + } +} \ No newline at end of file diff --git a/src/OpenIddict.Core/OpenIddictProvider.Session.cs b/src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Session.cs similarity index 85% rename from src/OpenIddict.Core/OpenIddictProvider.Session.cs rename to src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Session.cs index 9251cf5f..68ef06e4 100644 --- a/src/OpenIddict.Core/OpenIddictProvider.Session.cs +++ b/src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Session.cs @@ -12,10 +12,11 @@ using AspNet.Security.OpenIdConnect.Server; using JetBrains.Annotations; using Microsoft.Extensions.DependencyInjection; -namespace OpenIddict { - public partial class OpenIddictProvider : OpenIdConnectServerProvider where TUser : class where TApplication : class { +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 ValidateLogoutRequest([NotNull] ValidateLogoutRequestContext context) { - var services = context.HttpContext.RequestServices.GetRequiredService>(); + var services = context.HttpContext.RequestServices.GetRequiredService>(); // Skip validation if the optional post_logout_redirect_uri // parameter was missing from the logout request. @@ -25,7 +26,7 @@ namespace OpenIddict { return; } - var application = await services.Applications.FindApplicationByLogoutRedirectUri(context.PostLogoutRedirectUri); + var application = await services.Applications.FindByLogoutRedirectUri(context.PostLogoutRedirectUri); if (application == null) { context.Reject( error: OpenIdConnectConstants.Errors.InvalidClient, @@ -38,7 +39,7 @@ namespace OpenIddict { } public override async Task HandleLogoutRequest([NotNull] HandleLogoutRequestContext context) { - var services = context.HttpContext.RequestServices.GetRequiredService>(); + var services = context.HttpContext.RequestServices.GetRequiredService>(); // Only validate the id_token_hint if the user is still logged in. // If the authentication cookie doesn't exist or is no longer valid, diff --git a/src/OpenIddict.Core/OpenIddictProvider.Userinfo.cs b/src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Userinfo.cs similarity index 91% rename from src/OpenIddict.Core/OpenIddictProvider.Userinfo.cs rename to src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Userinfo.cs index 3862727e..b2a9701b 100644 --- a/src/OpenIddict.Core/OpenIddictProvider.Userinfo.cs +++ b/src/OpenIddict.Core/Infrastructure/OpenIddictProvider.Userinfo.cs @@ -13,10 +13,11 @@ using JetBrains.Annotations; using Microsoft.Extensions.DependencyInjection; using Newtonsoft.Json.Linq; -namespace OpenIddict { - public partial class OpenIddictProvider : OpenIdConnectServerProvider where TUser : class where TApplication : class { +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 HandleUserinfoRequest([NotNull] HandleUserinfoRequestContext context) { - var services = context.HttpContext.RequestServices.GetRequiredService>(); + var services = context.HttpContext.RequestServices.GetRequiredService>(); var principal = context.Ticket?.Principal; Debug.Assert(principal != null); diff --git a/src/OpenIddict.Core/OpenIddictProvider.cs b/src/OpenIddict.Core/Infrastructure/OpenIddictProvider.cs similarity index 79% rename from src/OpenIddict.Core/OpenIddictProvider.cs rename to src/OpenIddict.Core/Infrastructure/OpenIddictProvider.cs index 48baf79d..1f943898 100644 --- a/src/OpenIddict.Core/OpenIddictProvider.cs +++ b/src/OpenIddict.Core/Infrastructure/OpenIddictProvider.cs @@ -8,8 +8,9 @@ using System.Threading.Tasks; using AspNet.Security.OpenIdConnect.Server; using JetBrains.Annotations; -namespace OpenIddict { - public partial class OpenIddictProvider : OpenIdConnectServerProvider where TUser : class where TApplication : class { +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 Task MatchEndpoint([NotNull] MatchEndpointContext context) { // Note: by default, OpenIdConnectServerHandler only handles authorization requests made to AuthorizationEndpointPath. // This context handler uses a more relaxed policy that allows extracting authorization requests received at diff --git a/src/OpenIddict.Core/OpenIddictServices.cs b/src/OpenIddict.Core/Infrastructure/OpenIddictServices.cs similarity index 51% rename from src/OpenIddict.Core/OpenIddictServices.cs rename to src/OpenIddict.Core/Infrastructure/OpenIddictServices.cs index fd37c908..9a5b1077 100644 --- a/src/OpenIddict.Core/OpenIddictServices.cs +++ b/src/OpenIddict.Core/Infrastructure/OpenIddictServices.cs @@ -12,42 +12,38 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -namespace OpenIddict { +namespace OpenIddict.Infrastructure { /// /// Exposes the common services used by OpenIddict. /// - public class OpenIddictServices where TUser : class where TApplication : class { + public class OpenIddictServices + where TUser : class where TApplication : class where TAuthorization : class + where TScope : class where TToken : class { public OpenIddictServices([NotNull] IServiceProvider services) { Services = services; } /// - /// Gets the . + /// Gets the . /// - public virtual OpenIddictManager Applications { - get { return Services.GetRequiredService>(); } - } + public virtual OpenIddictApplicationManager Applications => + Services.GetRequiredService>(); /// /// Gets the optional . /// - public virtual HttpContext Context { - get { return Services.GetService()?.HttpContext; } - } + public virtual HttpContext Context => Services.GetService()?.HttpContext; /// /// Gets the . /// - public virtual ILogger Logger { - get { return Services.GetRequiredService>>(); } - } + public virtual ILogger Logger => + Services.GetRequiredService>>(); /// /// Gets the . /// - public virtual OpenIddictOptions Options { - get { return Services.GetRequiredService>().Value; } - } + public virtual OpenIddictOptions Options => Services.GetRequiredService>().Value; /// /// Gets the used to resolve services. @@ -57,22 +53,17 @@ namespace OpenIddict { /// /// Gets the . /// - public virtual SignInManager SignIn { - get { return Services.GetRequiredService>(); } - } + public virtual SignInManager SignIn => Services.GetRequiredService>(); /// - /// Gets the . + /// Gets the . /// - public virtual IOpenIddictStore Store { - get { return Services.GetRequiredService>(); } - } + public virtual OpenIddictTokenManager Tokens => + Services.GetRequiredService>(); /// /// Gets the . /// - public virtual UserManager Users { - get { return Services.GetRequiredService>(); } - } + public virtual UserManager Users => Services.GetRequiredService>(); } } \ No newline at end of file diff --git a/src/OpenIddict.Core/Managers/OpenIddictApplicationManager.cs b/src/OpenIddict.Core/Managers/OpenIddictApplicationManager.cs new file mode 100644 index 00000000..7f309c1f --- /dev/null +++ b/src/OpenIddict.Core/Managers/OpenIddictApplicationManager.cs @@ -0,0 +1,173 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +using System; +using System.Threading; +using System.Threading.Tasks; +using CryptoHelper; +using JetBrains.Annotations; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace OpenIddict { + /// + /// Provides methods allowing to manage the applications stored in the store. + /// + /// The type of the Application entity. + public class OpenIddictApplicationManager where TApplication : class { + public OpenIddictApplicationManager( + [NotNull] IServiceProvider services, + [NotNull] IOpenIddictApplicationStore store, + [NotNull] ILogger> logger) { + Context = services?.GetRequiredService()?.HttpContext; + Store = store; + Logger = logger; + } + + /// + /// Gets the HTTP context associated with the current manager. + /// + protected HttpContext Context { get; } + + /// + /// Gets the cancellation token used to abort async operations. + /// + protected CancellationToken CancellationToken => Context?.RequestAborted ?? CancellationToken.None; + + /// + /// Gets the logger associated with the current manager. + /// + protected ILogger Logger { get; } + + /// + /// Gets the store associated with the current manager. + /// + protected IOpenIddictApplicationStore Store { get; } + + /// + /// Creates a new application. + /// + /// The application to create. + /// A that can be used to monitor the asynchronous operation. + public virtual Task CreateAsync(TApplication application) { + if (application == null) { + throw new ArgumentNullException(nameof(application)); + } + + return Store.CreateAsync(application, CancellationToken); + } + + /// + /// Retrieves an application using its unique identifier. + /// + /// The unique identifier associated with the application. + /// A that can be used to monitor the asynchronous operation. + public virtual Task FindByIdAsync(string identifier) { + return Store.FindByIdAsync(identifier, CancellationToken); + } + + /// + /// Retrieves an application using its post_logout_redirect_uri. + /// + /// The post_logout_redirect_uri associated with the application. + /// A that can be used to monitor the asynchronous operation. + public virtual Task FindByLogoutRedirectUri(string url) { + return Store.FindByLogoutRedirectUri(url, CancellationToken); + } + + /// + /// Retrieves the client type associated with an application. + /// + /// The application. + /// A that can be used to monitor the asynchronous operation. + public virtual async Task GetClientTypeAsync(TApplication application) { + if (application == null) { + throw new ArgumentNullException(nameof(application)); + } + + var type = await Store.GetClientTypeAsync(application, CancellationToken); + + // Ensure the application type returned by the store is supported by the manager. + if (!string.Equals(type, OpenIddictConstants.ClientTypes.Confidential, StringComparison.OrdinalIgnoreCase) && + !string.Equals(type, OpenIddictConstants.ClientTypes.Public, StringComparison.OrdinalIgnoreCase)) { + throw new InvalidOperationException("Only 'confidential' or 'public' applications are " + + "supported by the default application manager."); + } + + return type; + } + + /// + /// Retrieves the display name associated with an application. + /// + /// The application. + /// A that can be used to monitor the asynchronous operation. + public virtual Task GetDisplayNameAsync(TApplication application) { + if (application == null) { + throw new ArgumentNullException(nameof(application)); + } + + return Store.GetDisplayNameAsync(application, CancellationToken); + } + + /// + /// Validates the redirect_uri associated with an application. + /// + /// The application. + /// The address that should be compared to the redirect_uri stored in the database. + /// A that can be used to monitor the asynchronous operation. + public virtual async Task ValidateRedirectUriAsync(TApplication application, string address) { + if (application == null) { + throw new ArgumentNullException(nameof(application)); + } + + if (!string.Equals(address, await Store.GetRedirectUriAsync(application, CancellationToken), StringComparison.Ordinal)) { + Logger.LogWarning("Client validation failed because {RedirectUri} was not a valid redirect_uri " + + "for {Client}", address, await GetDisplayNameAsync(application)); + + return false; + } + + return true; + } + + /// + /// Validates the client_secret associated with an application. + /// + /// The application. + /// The secret that should be compared to the client_secret stored in the database. + /// A that can be used to monitor the asynchronous operation. + public virtual async Task ValidateSecretAsync(TApplication application, string secret) { + if (application == null) { + throw new ArgumentNullException(nameof(application)); + } + + var type = await GetClientTypeAsync(application); + if (type != OpenIddictConstants.ClientTypes.Confidential) { + Logger.LogWarning("Client authentication cannot be enforced for non-confidential applications."); + + return false; + } + + var hash = await Store.GetHashedSecretAsync(application, CancellationToken); + if (string.IsNullOrEmpty(hash)) { + Logger.LogError("Client authentication failed for {Client} because " + + "no client secret was associated with the application."); + + return false; + } + + if (!Crypto.VerifyHashedPassword(hash, secret)) { + Logger.LogWarning("Client authentication failed for {Client}.", await GetDisplayNameAsync(application)); + + return false; + } + + return true; + } + } +} \ No newline at end of file diff --git a/src/OpenIddict.Core/Managers/OpenIddictAuthorizationManager.cs b/src/OpenIddict.Core/Managers/OpenIddictAuthorizationManager.cs new file mode 100644 index 00000000..5ad6c81f --- /dev/null +++ b/src/OpenIddict.Core/Managers/OpenIddictAuthorizationManager.cs @@ -0,0 +1,49 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +using System; +using System.Threading; +using JetBrains.Annotations; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace OpenIddict { + /// + /// Provides methods allowing to manage the authorizations stored in the store. + /// + /// The type of the Authorization entity. + public class OpenIddictAuthorizationManager where TAuthorization : class { + public OpenIddictAuthorizationManager( + [NotNull] IServiceProvider services, + [NotNull] IOpenIddictAuthorizationStore store, + [NotNull] ILogger> logger) { + Context = services?.GetRequiredService()?.HttpContext; + Logger = logger; + Store = store; + } + + /// + /// Gets the HTTP context associated with the current manager. + /// + protected HttpContext Context { get; } + + /// + /// Gets the cancellation token used to abort async operations. + /// + protected CancellationToken CancellationToken => Context?.RequestAborted ?? CancellationToken.None; + + /// + /// Gets the logger associated with the current manager. + /// + protected ILogger Logger { get; } + + /// + /// Gets the store associated with the current manager. + /// + protected IOpenIddictAuthorizationStore Store { get; } + } +} \ No newline at end of file diff --git a/src/OpenIddict.Core/Managers/OpenIddictScopeManager.cs b/src/OpenIddict.Core/Managers/OpenIddictScopeManager.cs new file mode 100644 index 00000000..ec744bea --- /dev/null +++ b/src/OpenIddict.Core/Managers/OpenIddictScopeManager.cs @@ -0,0 +1,49 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +using System; +using System.Threading; +using JetBrains.Annotations; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace OpenIddict { + /// + /// Provides methods allowing to manage the scopes stored in the store. + /// + /// The type of the Scope entity. + public class OpenIddictScopeManager where TScope : class { + public OpenIddictScopeManager( + [NotNull] IServiceProvider services, + [NotNull] IOpenIddictAuthorizationStore store, + [NotNull] ILogger> logger) { + Context = services?.GetRequiredService()?.HttpContext; + Logger = logger; + Store = store; + } + + /// + /// Gets the HTTP context associated with the current manager. + /// + protected HttpContext Context { get; } + + /// + /// Gets the cancellation token used to abort async operations. + /// + protected CancellationToken CancellationToken => Context?.RequestAborted ?? CancellationToken.None; + + /// + /// Gets the logger associated with the current manager. + /// + protected ILogger Logger { get; } + + /// + /// Gets the store associated with the current manager. + /// + protected IOpenIddictAuthorizationStore Store { get; } + } +} \ No newline at end of file diff --git a/src/OpenIddict.Core/Managers/OpenIddictTokenManager.cs b/src/OpenIddict.Core/Managers/OpenIddictTokenManager.cs new file mode 100644 index 00000000..943a7d05 --- /dev/null +++ b/src/OpenIddict.Core/Managers/OpenIddictTokenManager.cs @@ -0,0 +1,189 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using System.Threading; +using System.Threading.Tasks; +using AspNet.Security.OpenIdConnect.Extensions; +using AspNet.Security.OpenIdConnect.Server; +using JetBrains.Annotations; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace OpenIddict { + /// + /// Provides methods allowing to manage the tokens stored in the store. + /// + /// The type of the Token entity. + /// The type of the User entity. + public class OpenIddictTokenManager where TToken : class where TUser : class { + public OpenIddictTokenManager( + [NotNull] IServiceProvider services, + [NotNull] IOpenIddictTokenStore store, + [NotNull] UserManager users, + [NotNull] IOptions options, + [NotNull] ILogger> logger) { + Context = services?.GetRequiredService()?.HttpContext; + Logger = logger; + Options = options.Value; + Store = store; + Users = users; + } + + /// + /// Gets the cancellation token used to abort async operations. + /// + protected CancellationToken CancellationToken => Context?.RequestAborted ?? CancellationToken.None; + + /// + /// Gets the HTTP context associated with the current manager. + /// + protected HttpContext Context { get; } + + /// + /// Gets the logger associated with the current manager. + /// + protected ILogger Logger { get; } + + /// + /// Gets the identity options. + /// + protected IdentityOptions Options { get; } + + /// + /// Gets the store associated with the current manager. + /// + protected IOpenIddictTokenStore Store { get; } + + /// + /// Gets the user manager. + /// + protected UserManager Users { get; } + + /// + /// Creates a new used to create new tokens. + /// + /// The user corresponding to the identity. + /// The scopes granted by the resource owner. + /// A that can be used to monitor the asynchronous operation. + public virtual async Task CreateIdentityAsync(TUser user, IEnumerable scopes) { + if (user == null) { + throw new ArgumentNullException(nameof(user)); + } + + if (scopes == null) { + throw new ArgumentNullException(nameof(scopes)); + } + + var identity = new ClaimsIdentity( + OpenIdConnectServerDefaults.AuthenticationScheme, + Options.ClaimsIdentity.UserNameClaimType, + Options.ClaimsIdentity.RoleClaimType); + + // 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, await Users.GetUserIdAsync(user)); + + // Resolve the email address associated with the user if the underlying store supports it. + var email = Users.SupportsUserEmail ? await Users.GetEmailAsync(user) : null; + + // Only add the name claim if the "profile" scope was granted. + if (scopes.Contains(OpenIdConnectConstants.Scopes.Profile)) { + var username = await Users.GetUserNameAsync(user); + + // Throw an exception if the username corresponds to the registered + // email address and if the "email" scope has not been requested. + if (!scopes.Contains(OpenIdConnectConstants.Scopes.Email) && + !string.IsNullOrEmpty(email) && + string.Equals(username, email, StringComparison.OrdinalIgnoreCase)) { + throw new InvalidOperationException("The 'email' scope is required."); + } + + identity.AddClaim(ClaimTypes.Name, username, + OpenIdConnectConstants.Destinations.AccessToken, + OpenIdConnectConstants.Destinations.IdentityToken); + } + + // Only add the email address if the "email" scope was granted. + if (!string.IsNullOrEmpty(email) && scopes.Contains(OpenIdConnectConstants.Scopes.Email)) { + identity.AddClaim(ClaimTypes.Email, email, + OpenIdConnectConstants.Destinations.AccessToken, + OpenIdConnectConstants.Destinations.IdentityToken); + } + + if (Users.SupportsUserRole && scopes.Contains(OpenIddictConstants.Scopes.Roles)) { + foreach (var role in await Users.GetRolesAsync(user)) { + identity.AddClaim(identity.RoleClaimType, role, + OpenIdConnectConstants.Destinations.AccessToken, + OpenIdConnectConstants.Destinations.IdentityToken); + } + } + + if (Users.SupportsUserSecurityStamp) { + var stamp = await Users.GetSecurityStampAsync(user); + + if (!string.IsNullOrEmpty(stamp)) { + identity.AddClaim(Options.ClaimsIdentity.SecurityStampClaimType, stamp, + OpenIdConnectConstants.Destinations.AccessToken, + OpenIdConnectConstants.Destinations.IdentityToken); + } + } + + return identity; + } + + /// + /// Creates a new token, defined by a unique identifier and a token type. + /// + /// The unique identifier associated with the token to create. + /// The token type. + /// A that can be used to monitor the asynchronous operation. + public virtual Task CreateAsync(string identifier, string type) { + if (string.IsNullOrEmpty(identifier)) { + throw new ArgumentException("The identifier cannot be null or empty", nameof(identifier)); + } + + if (string.IsNullOrEmpty(type)) { + throw new ArgumentException("The token type cannot be null or empty", nameof(type)); + } + + return Store.CreateAsync(identifier, type, CancellationToken); + } + + /// + /// Retrieves an token using its unique identifier. + /// + /// The unique identifier associated with the token. + /// A that can be used to monitor the asynchronous operation. + public virtual Task FindByIdAsync(string identifier) { + if (string.IsNullOrEmpty(identifier)) { + throw new ArgumentException("The identifier cannot be null or empty", nameof(identifier)); + } + + return Store.FindByIdAsync(identifier, CancellationToken); + } + + /// + /// Revokes a token. + /// + /// The token to revoke. + /// A that can be used to monitor the asynchronous operation. + public virtual Task RevokeAsync(TToken token) { + if (token == null) { + throw new ArgumentNullException(nameof(token)); + } + + return Store.RevokeAsync(token, CancellationToken); + } + } +} \ No newline at end of file diff --git a/src/OpenIddict.Core/OpenIddictBuilder.cs b/src/OpenIddict.Core/OpenIddictBuilder.cs index a7b7fd0b..e07d1651 100644 --- a/src/OpenIddict.Core/OpenIddictBuilder.cs +++ b/src/OpenIddict.Core/OpenIddictBuilder.cs @@ -1,19 +1,431 @@ -using System.Collections.Generic; +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +using System; +using System.ComponentModel; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Security.Cryptography.X509Certificates; +using JetBrains.Annotations; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; using OpenIddict; namespace Microsoft.AspNetCore.Builder { /// - /// Holds various properties allowing to configure OpenIddict. + /// Exposes the necessary methods required to configure OpenIddict. /// public class OpenIddictBuilder { /// - /// Gets the list of the OpenIddict modules. + /// Initializes a new instance of . + /// + /// The services collection. + public OpenIddictBuilder(IServiceCollection services) { + Services = services; + } + + /// + /// Gets or sets the type corresponding to the Application entity. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public Type ApplicationType { get; set; } + + /// + /// Gets or sets the type corresponding to the Authorization entity. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public Type AuthorizationType { get; set; } + + /// + /// Gets or sets the type corresponding to the Scope entity. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public Type ScopeType { get; set; } + + /// + /// Gets or sets the type corresponding to the Token entity. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public Type TokenType { get; set; } + + /// + /// Gets or sets the type corresponding to the User entity. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public Type UserType { get; set; } + + /// + /// Gets the services collection. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public IServiceCollection Services { get; } + + /// + /// Amends the default OpenIddict configuration. + /// + /// The delegate used to configure the OpenIddict options. + /// This extension can be safely called multiple times. + /// The . + public virtual OpenIddictBuilder Configure([NotNull] Action configuration) { + if (configuration == null) { + throw new ArgumentNullException(nameof(configuration)); + } + + Services.Configure(configuration); + + return this; + } + + /// + /// Adds a custom application manager. + /// + /// The type of the custom manager. + /// The . + public virtual OpenIddictBuilder AddApplicationManager() { + var contract = typeof(OpenIddictApplicationManager<>).MakeGenericType(ApplicationType); + if (!contract.IsAssignableFrom(typeof(TManager))) { + throw new InvalidOperationException("Custom managers must be derived from OpenIddictApplicationManager."); + } + + Services.AddScoped(contract, typeof(TManager)); + + return this; + } + + /// + /// Adds a custom application store. + /// + /// The type of the custom store. + /// The . + public virtual OpenIddictBuilder AddApplicationStore() { + var contract = typeof(IOpenIddictApplicationStore<>).MakeGenericType(ApplicationType); + if (!contract.IsAssignableFrom(typeof(TStore))) { + throw new InvalidOperationException("Custom stores must implement IOpenIddictApplicationStore."); + } + + Services.AddScoped(contract, typeof(TStore)); + + return this; + } + + /// + /// Adds a custom authorization manager. + /// + /// The type of the custom manager. + /// The . + public virtual OpenIddictBuilder AddAuthorizationManager() { + var contract = typeof(OpenIddictAuthorizationManager<>).MakeGenericType(AuthorizationType); + if (!contract.IsAssignableFrom(typeof(TManager))) { + throw new InvalidOperationException("Custom managers must be derived from OpenIddictAuthorizationManager."); + } + + Services.AddScoped(contract, typeof(TManager)); + + return this; + } + + /// + /// Adds a custom authorization store. + /// + /// The type of the custom store. + /// The . + public virtual OpenIddictBuilder AddAuthorizationStore() { + var contract = typeof(IOpenIddictAuthorizationStore<>).MakeGenericType(AuthorizationType); + if (!contract.IsAssignableFrom(typeof(TStore))) { + throw new InvalidOperationException("Custom stores must implement IOpenIddictAuthorizationStore."); + } + + Services.AddScoped(contract, typeof(TStore)); + + return this; + } + + /// + /// Adds a custom scope manager. + /// + /// The type of the custom manager. + /// The . + public virtual OpenIddictBuilder AddScopeManager() { + var contract = typeof(OpenIddictScopeManager<>).MakeGenericType(ScopeType); + if (!contract.IsAssignableFrom(typeof(TManager))) { + throw new InvalidOperationException("Custom managers must be derived from OpenIddictScopeManager."); + } + + Services.AddScoped(contract, typeof(TManager)); + + return this; + } + + /// + /// Adds a custom scope store. + /// + /// The type of the custom store. + /// The . + public virtual OpenIddictBuilder AddScopeStore() { + var contract = typeof(IOpenIddictScopeStore<>).MakeGenericType(ScopeType); + if (!contract.IsAssignableFrom(typeof(TStore))) { + throw new InvalidOperationException("Custom stores must implement IOpenIddictScopeStore."); + } + + Services.AddScoped(contract, typeof(TStore)); + + return this; + } + + /// + /// Adds a custom token manager. + /// + /// The type of the custom manager. + /// The . + public virtual OpenIddictBuilder AddTokenManager() { + var contract = typeof(OpenIddictTokenManager<,>).MakeGenericType(TokenType, UserType); + if (!contract.IsAssignableFrom(typeof(TManager))) { + throw new InvalidOperationException("Custom managers must be derived from OpenIddictTokenManager."); + } + + Services.AddScoped(contract, typeof(TManager)); + + return this; + } + + /// + /// Adds a custom token store. + /// + /// The type of the custom store. + /// The . + public virtual OpenIddictBuilder AddTokenStore() { + var contract = typeof(IOpenIddictTokenStore<>).MakeGenericType(TokenType); + if (!contract.IsAssignableFrom(typeof(TStore))) { + throw new InvalidOperationException("Custom stores must implement IOpenIddictTokenStore."); + } + + Services.AddScoped(contract, typeof(TStore)); + + return this; + } + + /// + /// Registers a new OpenIddict module. If a module with the same name already + /// exists, the new instance is ignored and this extension has no effect. + /// + /// The name of the OpenIddict module. + /// The relative position of the OpenIddict module in the ASP.NET Core pipeline. + /// The delegate used to register the module in the ASP.NET Core pipeline. + /// The. + public virtual OpenIddictBuilder AddModule( + [NotNull] string name, int position, + [NotNull] Action registration) { + if (string.IsNullOrEmpty(name)) { + throw new ArgumentNullException(nameof(name)); + } + + if (registration == null) { + throw new ArgumentNullException(nameof(registration)); + } + + return Configure(options => { + if (options.Modules.Any(module => module.Name == name)) { + return; + } + + options.Modules.Add(new OpenIddictModule(name, position, registration)); + }); + } + + /// + /// Registers a used to sign the tokens issued by OpenIddict. + /// + /// The certificate used to sign the security tokens issued by the server. + /// The . + public virtual OpenIddictBuilder AddSigningCertificate([NotNull] X509Certificate2 certificate) { + if (certificate == null) { + throw new ArgumentNullException(nameof(certificate)); + } + + if (!certificate.HasPrivateKey) { + throw new InvalidOperationException("The certificate doesn't contain the required private key."); + } + + return Configure(options => options.SigningCredentials.AddCertificate(certificate)); + } + + /// + /// Registers a retrieved from + /// an embedded resource to sign the tokens issued by OpenIddict. + /// + /// The assembly containing the certificate. + /// The name of the embedded resource. + /// The password used to open the certificate. + /// The . + public virtual OpenIddictBuilder AddSigningCertificate( + [NotNull] Assembly assembly, [NotNull] string resource, [NotNull] string password) { + if (assembly == null) { + throw new ArgumentNullException(nameof(assembly)); + } + + if (string.IsNullOrEmpty(resource)) { + throw new ArgumentNullException(nameof(resource)); + } + + if (string.IsNullOrEmpty(password)) { + throw new ArgumentNullException(nameof(password)); + } + + return Configure(options => options.SigningCredentials.AddCertificate(assembly, resource, password)); + } + + /// + /// Registers a extracted + /// from a stream to sign the tokens issued by OpenIddict. + /// + /// The stream containing the certificate. + /// The password used to open the certificate. + /// The . + public virtual OpenIddictBuilder AddSigningCertificate([NotNull] Stream stream, [NotNull] string password) { + if (stream == null) { + throw new ArgumentNullException(nameof(stream)); + } + + if (string.IsNullOrEmpty(password)) { + throw new ArgumentNullException(nameof(password)); + } + + return Configure(options => options.SigningCredentials.AddCertificate(stream, password)); + } + + /// + /// Registers a extracted + /// from a stream to sign the tokens issued by OpenIddict. + /// + /// The stream containing the certificate. + /// The password used to open the certificate. + /// + /// An enumeration of flags indicating how and where + /// to store the private key of the certificate. + /// + /// The . + public virtual OpenIddictBuilder AddSigningCertificate( + [NotNull] Stream stream, [NotNull] string password, X509KeyStorageFlags flags) { + if (stream == null) { + throw new ArgumentNullException(nameof(stream)); + } + + if (string.IsNullOrEmpty(password)) { + throw new ArgumentNullException(nameof(password)); + } + + return Configure(options => options.SigningCredentials.AddCertificate(stream, password, flags)); + } + + /// + /// Registers a retrieved from the + /// X.509 machine store to sign the tokens issued by OpenIddict. + /// + /// The thumbprint of the certificate used to identify it in the X.509 store. + /// The . + public virtual OpenIddictBuilder AddSigningCertificate([NotNull] string thumbprint) { + if (string.IsNullOrEmpty(thumbprint)) { + throw new ArgumentNullException(nameof(thumbprint)); + } + + return Configure(options => options.SigningCredentials.AddCertificate(thumbprint)); + } + + /// + /// Registers a retrieved from the + /// given X.509 store to sign the tokens issued by OpenIddict. + /// + /// The thumbprint of the certificate used to identify it in the X.509 store. + /// The name of the X.509 store. + /// The location of the X.509 store. + /// The . + public virtual OpenIddictBuilder AddSigningCertificate( + [NotNull] string thumbprint, StoreName name, StoreLocation location) { + if (string.IsNullOrEmpty(thumbprint)) { + throw new ArgumentNullException(nameof(thumbprint)); + } + + return Configure(options => options.SigningCredentials.AddCertificate(thumbprint, name, location)); + } + + /// + /// Disables the HTTPS requirement during development. + /// + /// The . + public virtual OpenIddictBuilder DisableHttpsRequirement() { + return Configure(options => options.AllowInsecureHttp = true); + } + + /// + /// Disables sliding expiration, which prevents OpenIddict from issuing a new + /// refresh token when receiving a grant_type=refresh_token token request. + /// + /// The . + public virtual OpenIddictBuilder DisableSlidingExpiration() { + return Configure(options => options.UseSlidingExpiration = false); + } + + /// + /// Sets the relative path corresponding to the authorization endpoint. + /// + /// The relative path of the authorization endpoint. + /// The . + public virtual OpenIddictBuilder SetAuthorizationEndpointPath(PathString path) { + return Configure(options => options.AuthorizationEndpointPath = path); + } + + /// + /// Sets the relative path corresponding to the logout endpoint. + /// + /// The relative path of the logout endpoint. + /// The . + public virtual OpenIddictBuilder SetLogoutEndpointPath(PathString path) { + return Configure(options => options.LogoutEndpointPath = path); + } + + /// + /// Sets the relative path corresponding to the token endpoint. + /// + /// The relative path of the token endpoint. + /// The . + public virtual OpenIddictBuilder SetTokenEndpointPath(PathString path) { + return Configure(options => options.TokenEndpointPath = path); + } + + /// + /// Sets the access token lifetime, after which client applications must retrieve + /// a new access token by making a grant_type=refresh_token token request + /// or a prompt=none authorization request, depending on the selected flow. + /// Using long-lived access tokens or tokens that never expire is not recommended. + /// + /// The access token lifetime. + /// The . + public virtual OpenIddictBuilder SetAccessTokenLifetime(TimeSpan lifetime) { + return Configure(options => options.AccessTokenLifetime = lifetime); + } + + /// + /// Sets the refresh token lifetime, after which client applications must get + /// a new authorization from the user. When sliding expiration is enabled, + /// a new refresh token is always issued to the client application, + /// which prolongs the validity period of the refresh token. /// - public ICollection Modules { get; } = new List(); + /// The refresh token lifetime. + /// The . + public virtual OpenIddictBuilder SetRefreshTokenLifetime(TimeSpan lifetime) { + return Configure(options => options.RefreshTokenLifetime = lifetime); + } /// - /// Gets or sets the options used by OpenIddict. + /// Sets JWT as the default token format for access tokens. /// - public OpenIddictOptions Options { get; set; } = new OpenIddictOptions(); + /// The . + public virtual OpenIddictBuilder UseJsonWebTokens() { + return Configure(options => options.UseJwtTokens()); + } } -} +} \ No newline at end of file diff --git a/src/OpenIddict.Core/OpenIddictConfiguration.cs b/src/OpenIddict.Core/OpenIddictConfiguration.cs deleted file mode 100644 index 7a89292b..00000000 --- a/src/OpenIddict.Core/OpenIddictConfiguration.cs +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) - * See https://github.com/openiddict/openiddict-core for more information concerning - * the license and the contributors participating to this project. - */ - -using System; -using Microsoft.Extensions.DependencyInjection; - -namespace OpenIddict { - public class OpenIddictConfiguration { - public OpenIddictConfiguration(IServiceCollection services) { - Services = services; - } - - /// - /// Gets or sets the type corresponding to the Application entity. - /// - public Type ApplicationType { get; set; } - - /// - /// Gets or sets the type corresponding to the Role entity. - /// - public Type RoleType { get; set; } - - /// - /// Gets or sets the type corresponding to the User entity. - /// - public Type UserType { get; set; } - - /// - /// Gets the services used by OpenIddict. - /// - public IServiceCollection Services { get; } - } -} \ No newline at end of file diff --git a/src/OpenIddict.Core/OpenIddictConstants.cs b/src/OpenIddict.Core/OpenIddictConstants.cs index 9f9f477b..a98a3ee7 100644 --- a/src/OpenIddict.Core/OpenIddictConstants.cs +++ b/src/OpenIddict.Core/OpenIddictConstants.cs @@ -1,14 +1,20 @@ -namespace OpenIddict { - public static class OpenIddictConstants { - public static class ApplicationTypes { - public const string Confidential = "confidential"; - public const string Public = "public"; - } +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ +namespace OpenIddict { + public static class OpenIddictConstants { public static class Claims { public const string Roles = "roles"; } + public static class ClientTypes { + public const string Confidential = "confidential"; + public const string Public = "public"; + } + public static class Scopes { public const string Roles = "roles"; } diff --git a/src/OpenIddict.Core/OpenIddictExtensions.cs b/src/OpenIddict.Core/OpenIddictExtensions.cs index ff7d5911..6dde32dc 100644 --- a/src/OpenIddict.Core/OpenIddictExtensions.cs +++ b/src/OpenIddict.Core/OpenIddictExtensions.cs @@ -5,160 +5,93 @@ */ using System; -using System.IO; using System.Linq; -using System.Reflection; -using System.Security.Cryptography.X509Certificates; -using AspNet.Security.OpenIdConnect.Server; using JetBrains.Annotations; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; -using Microsoft.IdentityModel.Tokens; +using Microsoft.Extensions.Options; using OpenIddict; +using OpenIddict.Infrastructure; namespace Microsoft.AspNetCore.Builder { public static class OpenIddictExtensions { - public static IdentityBuilder AddOpenIddictCore( - [NotNull] this IdentityBuilder builder, - [NotNull] Action configuration) - where TApplication : class { - if (builder == null) { - throw new ArgumentNullException(nameof(builder)); - } - - if (configuration == null) { - throw new ArgumentNullException(nameof(configuration)); - } - - builder.Services.AddAuthentication(); - builder.Services.AddDistributedMemoryCache(); - - builder.Services.TryAddSingleton( - typeof(IOpenIdConnectServerProvider), - typeof(OpenIddictProvider<,>).MakeGenericType( - builder.UserType, typeof(TApplication))); - - builder.Services.TryAddScoped( - typeof(OpenIddictManager<,>).MakeGenericType( - builder.UserType, typeof(TApplication))); - - builder.Services.TryAddTransient( - typeof(OpenIddictServices<,>).MakeGenericType( - builder.UserType, typeof(TApplication))); - - var instance = new OpenIddictConfiguration(builder.Services) { + /// + /// Registers the OpenIddict core services in the DI container. + /// When using this method, custom stores must be manually registered. + /// + /// The type of the User entity. + /// The type of the Application entity. + /// The type of the Authorization entity. + /// The type of the Scope entity. + /// The type of the Token entity. + /// The services collection. + /// + /// Note: the core services include native support for the non-interactive flows + /// (resource owner password credentials, client credentials, refresh token). + /// To support interactive flows like authorization code or implicit/hybrid, + /// consider adding the MVC module or creating your own authorization controller. + /// + /// The . + public static OpenIddictBuilder AddOpenIddict( + [NotNull] this IServiceCollection services) + where TUser : class + where TApplication : class + where TAuthorization : class + where TScope : class + where TToken : class { + if (services == null) { + throw new ArgumentNullException(nameof(services)); + } + + var builder = new OpenIddictBuilder(services) { ApplicationType = typeof(TApplication), - RoleType = builder.RoleType, - UserType = builder.UserType + AuthorizationType = typeof(TAuthorization), + ScopeType = typeof(TScope), + TokenType = typeof(TToken), + UserType = typeof(TUser) }; - builder.Services.TryAddSingleton(instance); - - configuration(instance); - - return builder; - } - - public static OpenIddictConfiguration UseManager([NotNull] this OpenIddictConfiguration configuration) { - if (configuration == null) { - throw new ArgumentNullException(nameof(configuration)); - } - - var contract = typeof(OpenIddictManager<,>).MakeGenericType(configuration.UserType, - configuration.ApplicationType); - if (!contract.IsAssignableFrom(typeof(TManager))) { - throw new InvalidOperationException("Custom managers must be derived from OpenIddictManager."); - } - - configuration.Services.Replace(ServiceDescriptor.Scoped(contract, typeof(TManager))); - - return configuration; - } - - public static OpenIddictConfiguration UseStore([NotNull] this OpenIddictConfiguration configuration) { - if (configuration == null) { - throw new ArgumentNullException(nameof(configuration)); - } - - var contract = typeof(IOpenIddictStore<,>).MakeGenericType(configuration.UserType, - configuration.ApplicationType); - if (!contract.IsAssignableFrom(typeof(TStore))) { - throw new InvalidOperationException("Custom stores must implement IOpenIddictStore."); - } - - configuration.Services.Replace(ServiceDescriptor.Scoped(contract, typeof(TStore))); - - return configuration; - } - - public static OpenIddictBuilder AddModule( - [NotNull] this OpenIddictBuilder builder, - [NotNull] string name, int position, - [NotNull] Action registration) { - if (builder == null) { - throw new ArgumentNullException(nameof(builder)); - } - - if (string.IsNullOrEmpty(name)) { - throw new ArgumentNullException(nameof(name)); - } - - if (registration == null) { - throw new ArgumentNullException(nameof(registration)); - } - - // Note: always call ToArray to make sure the foreach - // block doesn't iterate on the modified collection. - foreach (var module in builder.Modules.Where(module => string.Equals(module.Name, name)).ToArray()) { - builder.Modules.Remove(module); - } + // Register the services required by the OpenID Connect server middleware. + builder.Services.AddAuthentication(); + builder.Services.AddDistributedMemoryCache(); - builder.Modules.Add(new OpenIddictModule { - Name = name, - Position = position, - Registration = registration + builder.Configure(options => { + // Register the OpenID Connect server provider in the OpenIddict options. + options.Provider = new OpenIddictProvider(); }); - return builder; - } + // Register the OpenIddict core services in the DI container. + builder.Services.TryAddScoped>(); + builder.Services.TryAddScoped>(); + builder.Services.TryAddScoped>(); + builder.Services.TryAddScoped>(); + builder.Services.TryAddScoped>(); - public static IApplicationBuilder UseOpenIddictCore([NotNull] this IApplicationBuilder app) { - return app.UseOpenIddictCore(options => { }); + return builder; } - public static IApplicationBuilder UseOpenIddictCore( - [NotNull] this IApplicationBuilder app, - [NotNull] Action configuration) { + /// + /// Registers OpenIddict in the ASP.NET Core pipeline. + /// + /// The application builder used to register middleware instances. + /// The . + public static IApplicationBuilder UseOpenIddict([NotNull] this IApplicationBuilder app) { if (app == null) { throw new ArgumentNullException(nameof(app)); } - if (configuration == null) { - throw new ArgumentNullException(nameof(configuration)); - } + // Resolve the OpenIddict options from the DI container. + var options = app.ApplicationServices.GetRequiredService>().Value; - var builder = new OpenIddictBuilder(); - - // Resolve the OpenIddict provider from the services container. - builder.Options.Provider = app.ApplicationServices.GetRequiredService(); - - // By default, enable AllowInsecureHttp in development/testing environments. - var environment = app.ApplicationServices.GetRequiredService(); - builder.Options.AllowInsecureHttp = environment.IsDevelopment() || environment.IsEnvironment("Testing"); - - // Run the configuration delegate - // provided by the application. - configuration.Invoke(builder); - - // Add OpenIdConnectServerMiddleware to the ASP.NET Core pipeline. - builder.AddModule("ASOS", 0, map => map.UseOpenIdConnectServer(builder.Options)); + // Get the modules registered by the application + // and add the OpenID Connect server middleware. + var modules = options.Modules.ToList(); + modules.Add(new OpenIddictModule("OpenID Connect server", 0, builder => builder.UseOpenIdConnectServer(options))); // Register the OpenIddict modules in the ASP.NET Core pipeline. - foreach (var module in builder.Modules.OrderBy(module => module.Position)) { - if (module.Registration == null) { - throw new InvalidOperationException("The registration delegate cannot be null."); + foreach (var module in modules.OrderBy(module => module.Position)) { + if (module?.Registration == null) { + throw new InvalidOperationException("An invalid OpenIddict module was registered."); } module.Registration(app); @@ -166,129 +99,5 @@ namespace Microsoft.AspNetCore.Builder { return app; } - - /// - /// Registers a used to sign the tokens issued by OpenIddict. - /// - /// The builder used to configure OpenIddict. - /// The certificate used to sign the security tokens issued by the server. - /// The . - public static OpenIddictBuilder UseSigningCertificate( - [NotNull] this OpenIddictBuilder builder, [NotNull] X509Certificate2 certificate) { - if (builder == null) { - throw new ArgumentNullException(nameof(builder)); - } - - // Register the certificate in the ASOS/OpenIddict options. - builder.Options.SigningCredentials.AddCertificate(certificate); - - return builder; - } - - /// - /// Registers a retrieved from - /// an embedded resource to sign the tokens issued by OpenIddict. - /// - /// The builder used to configure OpenIddict. - /// The assembly containing the certificate. - /// The name of the embedded resource. - /// The password used to open the certificate. - /// The . - public static OpenIddictBuilder UseSigningCertificate( - [NotNull] this OpenIddictBuilder builder, [NotNull] Assembly assembly, - [NotNull] string resource, [NotNull] string password) { - if (builder == null) { - throw new ArgumentNullException(nameof(builder)); - } - - // Register the certificate in the ASOS/OpenIddict options. - builder.Options.SigningCredentials.AddCertificate(assembly, resource, password); - - return builder; - } - - /// - /// Registers a extracted - /// from a stream to sign the tokens issued by OpenIddict. - /// - /// The builder used to configure OpenIddict. - /// The stream containing the certificate. - /// The password used to open the certificate. - /// The . - public static OpenIddictBuilder UseSigningCertificate( - [NotNull] this OpenIddictBuilder builder, - [NotNull] Stream stream, [NotNull] string password) { - if (builder == null) { - throw new ArgumentNullException(nameof(builder)); - } - - // Register the certificate in the ASOS/OpenIddict options. - builder.Options.SigningCredentials.AddCertificate(stream, password); - - return builder; - } - - /// - /// Registers a extracted - /// from a stream to sign the tokens issued by OpenIddict. - /// - /// The builder used to configure OpenIddict. - /// The stream containing the certificate. - /// The password used to open the certificate. - /// An enumeration of flags indicating how and where to store the private key of the certificate. - /// The . - public static OpenIddictBuilder UseSigningCertificate( - [NotNull] this OpenIddictBuilder builder, [NotNull] Stream stream, - [NotNull] string password, X509KeyStorageFlags flags) { - if (builder == null) { - throw new ArgumentNullException(nameof(builder)); - } - - // Register the certificate in the ASOS/OpenIddict options. - builder.Options.SigningCredentials.AddCertificate(stream, password, flags); - - return builder; - } - - /// - /// Registers a retrieved from the - /// X.509 machine store to sign the tokens issued by OpenIddict. - /// - /// The builder used to configure OpenIddict. - /// The thumbprint of the certificate used to identify it in the X.509 store. - /// The . - public static OpenIddictBuilder UseSigningCertificate( - [NotNull] this OpenIddictBuilder builder, [NotNull] string thumbprint) { - if (builder == null) { - throw new ArgumentNullException(nameof(builder)); - } - - // Register the certificate in the ASOS/OpenIddict options. - builder.Options.SigningCredentials.AddCertificate(thumbprint); - - return builder; - } - - /// - /// Registers a retrieved from the - /// given X.509 store to sign the tokens issued by OpenIddict. - /// - /// The builder used to configure OpenIddict. - /// The thumbprint of the certificate used to identify it in the X.509 store. - /// The name of the X.509 store. - /// The location of the X.509 store. - /// The . - public static OpenIddictBuilder UseSigningCertificate( - [NotNull] this OpenIddictBuilder builder, - [NotNull] string thumbprint, StoreName name, StoreLocation location) { - if (builder == null) { - throw new ArgumentNullException(nameof(builder)); - } - - // Register the certificate in the ASOS/OpenIddict options. - builder.Options.SigningCredentials.AddCertificate(thumbprint, name, location); - - return builder; - } } } \ No newline at end of file diff --git a/src/OpenIddict.Core/OpenIddictHelpers.cs b/src/OpenIddict.Core/OpenIddictHelpers.cs deleted file mode 100644 index 05e127aa..00000000 --- a/src/OpenIddict.Core/OpenIddictHelpers.cs +++ /dev/null @@ -1,68 +0,0 @@ -using System; -using System.Threading.Tasks; -using JetBrains.Annotations; -using Microsoft.AspNetCore.Identity; - -namespace OpenIddict { - public static class OpenIddictHelpers { - public static async Task IsConfidentialApplicationAsync( - [NotNull] this OpenIddictManager manager, [NotNull] TApplication application) - where TUser : class - where TApplication : class { - if (manager == null) { - throw new ArgumentNullException(nameof(manager)); - } - - if (application == null) { - throw new ArgumentNullException(nameof(application)); - } - - var type = await manager.GetApplicationTypeAsync(application); - - return string.Equals(type, OpenIddictConstants.ApplicationTypes.Confidential, StringComparison.OrdinalIgnoreCase); - } - - public static async Task IsPublicApplicationAsync( - [NotNull] this OpenIddictManager manager, [NotNull] TApplication application) - where TUser : class - where TApplication : class { - if (manager == null) { - throw new ArgumentNullException(nameof(manager)); - } - - if (application == null) { - throw new ArgumentNullException(nameof(application)); - } - - var type = await manager.GetApplicationTypeAsync(application); - - return string.Equals(type, OpenIddictConstants.ApplicationTypes.Public, StringComparison.OrdinalIgnoreCase); - } - - public static async Task FindClaimAsync( - [NotNull] this UserManager manager, - [NotNull] TUser user, [NotNull] string type) where TUser : class { - if (manager == null) { - throw new ArgumentNullException(nameof(manager)); - } - - if (user == null) { - throw new ArgumentNullException(nameof(user)); - } - - if (string.IsNullOrEmpty(type)) { - throw new ArgumentNullException(nameof(type)); - } - - // Note: GetClaimsAsync will automatically throw an exception - // if the underlying store doesn't support custom claims. - - var claims = await manager.GetClaimsAsync(user); - if (claims.Count != 0) { - return claims[0]?.Value; - } - - return null; - } - } -} diff --git a/src/OpenIddict.Core/OpenIddictManager.cs b/src/OpenIddict.Core/OpenIddictManager.cs deleted file mode 100644 index e090d7a2..00000000 --- a/src/OpenIddict.Core/OpenIddictManager.cs +++ /dev/null @@ -1,195 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Security.Claims; -using System.Threading; -using System.Threading.Tasks; -using AspNet.Security.OpenIdConnect.Extensions; -using AspNet.Security.OpenIdConnect.Server; -using CryptoHelper; -using JetBrains.Annotations; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; - -namespace OpenIddict { - public class OpenIddictManager where TUser : class where TApplication : class { - public OpenIddictManager([NotNull] OpenIddictServices services) { - Services = services; - } - - /// - /// Gets the HTTP context associated with the current manager. - /// - protected virtual HttpContext Context => Services.Context; - - /// - /// Gets the cancellation token used to abort async operations. - /// - protected virtual CancellationToken CancellationToken => Context?.RequestAborted ?? CancellationToken.None; - - /// - /// Gets the logger associated with the current manager. - /// - protected virtual ILogger Logger => Services.Logger; - - /// - /// Gets the options associated with the current manager. - /// - protected virtual IdentityOptions Options => Services.Services.GetRequiredService>().Value; - - /// - /// Gets the servuces associated with the current manager. - /// - protected virtual OpenIddictServices Services { get; } - - /// - /// Gets the store associated with the current manager. - /// - protected virtual IOpenIddictStore Store => Services.Store; - - public virtual async Task CreateIdentityAsync(TUser user, IEnumerable scopes) { - if (user == null) { - throw new ArgumentNullException(nameof(user)); - } - - if (scopes == null) { - throw new ArgumentNullException(nameof(scopes)); - } - - var identity = new ClaimsIdentity( - OpenIdConnectServerDefaults.AuthenticationScheme, - Options.ClaimsIdentity.UserNameClaimType, - Options.ClaimsIdentity.RoleClaimType); - - // 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, await Services.Users.GetUserIdAsync(user)); - - // Resolve the email address associated with the user if the underlying store supports it. - var email = Services.Users.SupportsUserEmail ? await Services.Users.GetEmailAsync(user) : null; - - // Only add the name claim if the "profile" scope was granted. - if (scopes.Contains(OpenIdConnectConstants.Scopes.Profile)) { - var username = await Services.Users.GetUserNameAsync(user); - - // Throw an exception if the username corresponds to the registered - // email address and if the "email" scope has not been requested. - if (!scopes.Contains(OpenIdConnectConstants.Scopes.Email) && - !string.IsNullOrEmpty(email) && - string.Equals(username, email, StringComparison.OrdinalIgnoreCase)) { - throw new InvalidOperationException("The 'email' scope is required."); - } - - identity.AddClaim(ClaimTypes.Name, username, - OpenIdConnectConstants.Destinations.AccessToken, - OpenIdConnectConstants.Destinations.IdentityToken); - } - - // Only add the email address if the "email" scope was granted. - if (!string.IsNullOrEmpty(email) && scopes.Contains(OpenIdConnectConstants.Scopes.Email)) { - identity.AddClaim(ClaimTypes.Email, email, - OpenIdConnectConstants.Destinations.AccessToken, - OpenIdConnectConstants.Destinations.IdentityToken); - } - - if (Services.Users.SupportsUserRole && scopes.Contains(OpenIddictConstants.Scopes.Roles)) { - foreach (var role in await Services.Users.GetRolesAsync(user)) { - identity.AddClaim(identity.RoleClaimType, role, - OpenIdConnectConstants.Destinations.AccessToken, - OpenIdConnectConstants.Destinations.IdentityToken); - } - } - - if (Services.Users.SupportsUserSecurityStamp) { - var identifier = await Services.Users.GetSecurityStampAsync(user); - - if (!string.IsNullOrEmpty(identifier)) { - identity.AddClaim(Options.ClaimsIdentity.SecurityStampClaimType, identifier, - OpenIdConnectConstants.Destinations.AccessToken, - OpenIdConnectConstants.Destinations.IdentityToken); - } - } - - return identity; - } - - public virtual Task FindApplicationByIdAsync(string identifier) { - return Store.FindApplicationByIdAsync(identifier, CancellationToken); - } - - public virtual Task FindApplicationByLogoutRedirectUri(string url) { - return Store.FindApplicationByLogoutRedirectUri(url, CancellationToken); - } - - public virtual async Task GetApplicationTypeAsync(TApplication application) { - if (application == null) { - throw new ArgumentNullException(nameof(application)); - } - - var type = await Store.GetApplicationTypeAsync(application, CancellationToken); - - // Ensure the application type returned by the store is supported by the manager. - if (!string.Equals(type, OpenIddictConstants.ApplicationTypes.Confidential, StringComparison.OrdinalIgnoreCase) && - !string.Equals(type, OpenIddictConstants.ApplicationTypes.Public, StringComparison.OrdinalIgnoreCase)) { - throw new InvalidOperationException("Only 'confidential' or 'public' applications are " + - "supported by the default OpenIddict manager."); - } - - return type; - } - - public virtual Task GetDisplayNameAsync(TApplication application) { - if (application == null) { - throw new ArgumentNullException(nameof(application)); - } - - return Store.GetDisplayNameAsync(application, CancellationToken); - } - - public virtual async Task ValidateRedirectUriAsync(TApplication application, string address) { - if (application == null) { - throw new ArgumentNullException(nameof(application)); - } - - if (!string.Equals(address, await Store.GetRedirectUriAsync(application, CancellationToken), StringComparison.Ordinal)) { - Logger.LogWarning("Client validation failed because {RedirectUri} was not a valid redirect_uri " + - "for {Client}", address, await GetDisplayNameAsync(application)); - - return false; - } - - return true; - } - - public virtual async Task ValidateSecretAsync(TApplication application, string secret) { - if (application == null) { - throw new ArgumentNullException(nameof(application)); - } - - if (!await this.IsConfidentialApplicationAsync(application)) { - Logger.LogWarning("Client authentication cannot be enforced for non-confidential applications."); - - return false; - } - - var hash = await Store.GetHashedSecretAsync(application, CancellationToken); - if (string.IsNullOrEmpty(hash)) { - Logger.LogError("Client authentication failed for {Client} because " + - "no client secret was associated with the application."); - - return false; - } - - if (!Crypto.VerifyHashedPassword(hash, secret)) { - Logger.LogWarning("Client authentication failed for {Client}.", await GetDisplayNameAsync(application)); - - return false; - } - - return true; - } - } -} \ No newline at end of file diff --git a/src/OpenIddict.Core/OpenIddictModule.cs b/src/OpenIddict.Core/OpenIddictModule.cs index af799f47..10993eec 100644 --- a/src/OpenIddict.Core/OpenIddictModule.cs +++ b/src/OpenIddict.Core/OpenIddictModule.cs @@ -1,25 +1,56 @@ -using System; +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +using System; +using System.Diagnostics; +using JetBrains.Annotations; using Microsoft.AspNetCore.Builder; namespace OpenIddict { /// - /// Defines an OpenIddict module. + /// Represents an OpenIddict module. /// + [DebuggerDisplay("{Name,nq}")] public class OpenIddictModule { + /// + /// Initializes a new OpenIddict module. + /// + /// The name of the module. + /// The position of the module in the ASP.NET Core pipeline. + /// The delegate used to register the module in the pipeline. + public OpenIddictModule( + [NotNull] string name, int position, + [NotNull] Action registration) { + Name = name; + Position = position; + Registration = registration; + } + + /// + /// Initializes a new OpenIddict module. + /// + /// The delegate used to register the module in the pipeline. + public OpenIddictModule([NotNull] Action registration) { + Registration = registration; + } + /// /// Gets or sets the name of the module. /// - public string Name { get; set; } + public string Name { get; } /// - /// Gets or sets the position of the module in the ASP.NET pipeline. + /// Gets or sets the position of the module in the ASP.NET Core pipeline. /// - public int Position { get; set; } + public int Position { get; } /// - /// Gets or sets the delegate used to register - /// the OpenIddict module in the ASP.NET pipeline. + /// Gets or sets the delegate used to register the + /// OpenIddict module in the ASP.NET Core pipeline. /// - public Action Registration { get; set; } + public Action Registration { get; } } } diff --git a/src/OpenIddict.Core/OpenIddictOptions.cs b/src/OpenIddict.Core/OpenIddictOptions.cs index 185ceae5..4217b266 100644 --- a/src/OpenIddict.Core/OpenIddictOptions.cs +++ b/src/OpenIddict.Core/OpenIddictOptions.cs @@ -4,12 +4,28 @@ * the license and the contributors participating to this project. */ +using System; +using System.Collections.Generic; using AspNet.Security.OpenIdConnect.Server; +using Microsoft.AspNetCore.Http; namespace OpenIddict { + /// + /// Provides various settings needed to configure OpenIddict. + /// public class OpenIddictOptions : OpenIdConnectServerOptions { public OpenIddictOptions() { - ApplicationCanDisplayErrors = true; + // By default, disable the authorization and logout endpoints. + AuthorizationEndpointPath = LogoutEndpointPath = PathString.Empty; + + // Use the same lifespan as the default security stamp + // verification interval used by ASP.NET Core Identity. + AccessTokenLifetime = TimeSpan.FromMinutes(30); } + + /// + /// Gets the list of the OpenIddict modules registered in the application. + /// + public ICollection Modules { get; } = new List(); } } diff --git a/src/OpenIddict.Core/Stores/IOpenIddictApplicationStore.cs b/src/OpenIddict.Core/Stores/IOpenIddictApplicationStore.cs new file mode 100644 index 00000000..0bd0c9ed --- /dev/null +++ b/src/OpenIddict.Core/Stores/IOpenIddictApplicationStore.cs @@ -0,0 +1,72 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +using System.Threading; +using System.Threading.Tasks; + +namespace OpenIddict { + /// + /// Provides methods allowing to manage the applications stored in a database. + /// + /// The type of the Application entity. + public interface IOpenIddictApplicationStore where TApplication : class { + /// + /// Creates a new application. + /// + /// The application to create. + /// The that can be used to abort the operation. + /// A that can be used to monitor the asynchronous operation. + Task CreateAsync(TApplication application, CancellationToken cancellationToken); + + /// + /// Retrieves an application using its unique identifier. + /// + /// The unique identifier associated with the application. + /// The that can be used to abort the operation. + /// A that can be used to monitor the asynchronous operation. + Task FindByIdAsync(string identifier, CancellationToken cancellationToken); + + /// + /// Retrieves an application using its post_logout_redirect_uri. + /// + /// The post_logout_redirect_uri associated with the application. + /// The that can be used to abort the operation. + /// A that can be used to monitor the asynchronous operation. + Task FindByLogoutRedirectUri(string url, CancellationToken cancellationToken); + + /// + /// Retrieves the client type 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. + Task GetClientTypeAsync(TApplication application, CancellationToken cancellationToken); + + /// + /// Retrieves the display name 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. + Task GetDisplayNameAsync(TApplication application, CancellationToken cancellationToken); + + /// + /// Retrieves the callback address 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. + Task GetRedirectUriAsync(TApplication application, CancellationToken cancellationToken); + + /// + /// Retrieves the hashed secret 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. + Task GetHashedSecretAsync(TApplication application, CancellationToken cancellationToken); + } +} \ No newline at end of file diff --git a/src/OpenIddict.Core/Stores/IOpenIddictAuthorizationStore.cs b/src/OpenIddict.Core/Stores/IOpenIddictAuthorizationStore.cs new file mode 100644 index 00000000..f5be030c --- /dev/null +++ b/src/OpenIddict.Core/Stores/IOpenIddictAuthorizationStore.cs @@ -0,0 +1,13 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +namespace OpenIddict { + /// + /// Provides methods allowing to manage the authorizations stored in a database. + /// + /// The type of the Authorization entity. + public interface IOpenIddictAuthorizationStore where TAuthorization : class { } +} \ No newline at end of file diff --git a/src/OpenIddict.Core/Stores/IOpenIddictScopeStore.cs b/src/OpenIddict.Core/Stores/IOpenIddictScopeStore.cs new file mode 100644 index 00000000..dbe4ba9c --- /dev/null +++ b/src/OpenIddict.Core/Stores/IOpenIddictScopeStore.cs @@ -0,0 +1,13 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +namespace OpenIddict { + /// + /// Provides methods allowing to manage the scopes stored in a database. + /// + /// The type of the Scope entity. + public interface IOpenIddictScopeStore where TScope : class { } +} \ No newline at end of file diff --git a/src/OpenIddict.Core/Stores/IOpenIddictTokenStore.cs b/src/OpenIddict.Core/Stores/IOpenIddictTokenStore.cs new file mode 100644 index 00000000..7ce31f81 --- /dev/null +++ b/src/OpenIddict.Core/Stores/IOpenIddictTokenStore.cs @@ -0,0 +1,41 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +using System.Threading; +using System.Threading.Tasks; + +namespace OpenIddict { + /// + /// Provides methods allowing to manage the tokens stored in a database. + /// + /// The type of the Token entity. + public interface IOpenIddictTokenStore where TToken : class { + /// + /// Creates a new token, defined by a unique identifier and a token type. + /// + /// The unique identifier associated with the token to create. + /// The token type. + /// The that can be used to abort the operation. + /// A that can be used to monitor the asynchronous operation. + Task CreateAsync(string identifier, string type, CancellationToken cancellationToken); + + /// + /// Retrieves an token using its unique identifier. + /// + /// The unique identifier associated with the token. + /// The that can be used to abort the operation. + /// A that can be used to monitor the asynchronous operation. + Task FindByIdAsync(string identifier, CancellationToken cancellationToken); + + /// + /// Revokes a token. + /// + /// The token to revoke. + /// The that can be used to abort the operation. + /// A that can be used to monitor the asynchronous operation. + Task RevokeAsync(TToken token, CancellationToken cancellationToken); + } +} \ No newline at end of file diff --git a/src/OpenIddict.Core/project.json b/src/OpenIddict.Core/project.json index 5325f45b..847538ad 100644 --- a/src/OpenIddict.Core/project.json +++ b/src/OpenIddict.Core/project.json @@ -37,7 +37,6 @@ "CryptoHelper": "1.0.0-rc2-final", "JetBrains.Annotations": { "type": "build", "version": "10.1.4" }, "Microsoft.AspNetCore.Identity": "1.0.0-rc2-final", - "Microsoft.Extensions.Configuration": "1.0.0-rc2-final", "Microsoft.Extensions.Caching.Memory": "1.0.0-rc2-final" }, diff --git a/src/OpenIddict.EF/OpenIddictContext.cs b/src/OpenIddict.EF/OpenIddictContext.cs deleted file mode 100644 index b80885c0..00000000 --- a/src/OpenIddict.EF/OpenIddictContext.cs +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) - * See https://github.com/openiddict/openiddict-core for more information concerning - * the license and the contributors participating to this project. - */ - -using System; -using Microsoft.AspNetCore.Identity.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore; -using OpenIddict.Models; - -namespace OpenIddict { - public class OpenIddictContext : IdentityDbContext - where TUser : IdentityUser - where TApplication : Application - where TRole : IdentityRole - where TKey : IEquatable { - protected OpenIddictContext() { } - - public OpenIddictContext(DbContextOptions options) - : base(options) { } - - public DbSet Applications { get; set; } - } - - public class OpenIddictContext : OpenIddictContext where TUser : IdentityUser { - protected OpenIddictContext() { } - - public OpenIddictContext(DbContextOptions options) - : base(options) { } - } - - public class OpenIddictContext : OpenIddictContext { - protected OpenIddictContext() { } - - public OpenIddictContext(DbContextOptions options) - : base(options) { } - } -} \ No newline at end of file diff --git a/src/OpenIddict.EF/OpenIddictExtensions.cs b/src/OpenIddict.EF/OpenIddictExtensions.cs deleted file mode 100644 index 43a4ff2b..00000000 --- a/src/OpenIddict.EF/OpenIddictExtensions.cs +++ /dev/null @@ -1,117 +0,0 @@ -/* - * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) - * See https://github.com/openiddict/openiddict-core for more information concerning - * the license and the contributors participating to this project. - */ - -using System; -using System.Linq; -using System.Reflection; -using JetBrains.Annotations; -using Microsoft.AspNetCore.Identity; -using Microsoft.AspNetCore.Identity.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.DependencyInjection; -using OpenIddict; -using OpenIddict.Models; - -namespace Microsoft.AspNetCore.Builder { - public static class OpenIddictExtensions { - public static OpenIddictConfiguration UseEntityFramework([NotNull] this OpenIddictConfiguration configuration) { - if (configuration == null) { - throw new ArgumentNullException(nameof(configuration)); - } - - if (!IsSubclassOf(configuration.ApplicationType, typeof(Application<>))) { - throw new InvalidOperationException("The default store cannot be used with application " + - "entities that are not derived from Application."); - } - - configuration.Services.AddScoped( - typeof(IOpenIddictStore<,>).MakeGenericType(configuration.UserType, configuration.ApplicationType), - typeof(OpenIddictStore<,,,>).MakeGenericType( - /* TUser: */ configuration.UserType, - /* TApplication: */ configuration.ApplicationType, - /* TContext: */ ResolveContextType(configuration), - /* TKey: */ ResolveKeyType(configuration))); - - return configuration; - } - - private static Type ResolveContextType([NotNull] OpenIddictConfiguration configuration) { - var service = (from registration in configuration.Services - where registration.ServiceType.IsConstructedGenericType - let definition = registration.ServiceType.GetGenericTypeDefinition() - where definition == typeof(IUserStore<>) - select registration.ImplementationType).FirstOrDefault(); - - if (service == null) { - throw new InvalidOperationException( - "The type of the database context cannot be automatically inferred. " + - "Make sure 'AddOpenIddict()' is the last chained call when configuring the services."); - } - - TypeInfo type; - for (type = service.GetTypeInfo(); type != null; type = type.BaseType?.GetTypeInfo()) { - if (!type.IsGenericType) { - continue; - } - - var definition = type.GetGenericTypeDefinition(); - if (definition == null) { - continue; - } - - if (definition != typeof(UserStore<,,,>)) { - continue; - } - - return (from argument in type.AsType().GetGenericArguments() - where typeof(DbContext).IsAssignableFrom(argument) - select argument).Single(); - } - - throw new InvalidOperationException("The type of the database context cannot be automatically inferred."); - } - - private static Type ResolveKeyType([NotNull] OpenIddictConfiguration configuration) { - TypeInfo type; - for (type = configuration.UserType.GetTypeInfo(); type != null; type = type.BaseType?.GetTypeInfo()) { - if (!type.IsGenericType) { - continue; - } - - var definition = type.GetGenericTypeDefinition(); - if (definition == null) { - continue; - } - - if (definition != typeof(IdentityUser<>)) { - continue; - } - - return type.AsType().GetGenericArguments().Single(); - } - - throw new InvalidOperationException( - "The type of the key identifier used by the user " + - $"entity '{configuration.UserType}' cannot be automatically inferred."); - } - - private static bool IsSubclassOf([NotNull] Type type, [NotNull] Type generic) { - while (type != null && type != typeof(object)) { - var current = type.GetTypeInfo().IsGenericType ? - type.GetGenericTypeDefinition() : - type; - - if (current == generic) { - return true; - } - - type = type.GetTypeInfo().BaseType; - } - - return false; - } - } -} \ No newline at end of file diff --git a/src/OpenIddict.EF/OpenIddictStore.cs b/src/OpenIddict.EF/OpenIddictStore.cs deleted file mode 100644 index 40b2c5e5..00000000 --- a/src/OpenIddict.EF/OpenIddictStore.cs +++ /dev/null @@ -1,78 +0,0 @@ -using System; -using System.ComponentModel; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Identity.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore; -using OpenIddict.Models; - -namespace OpenIddict { - public class OpenIddictStore : IOpenIddictStore - where TUser : IdentityUser - where TApplication : Application - where TContext : DbContext - where TKey : IEquatable { - public OpenIddictStore(TContext context) { - Context = context; - } - - /// - /// Gets the database context associated with the current store. - /// - public virtual TContext Context { get; } - - public DbSet Applications { - get { return Context.Set(); } - } - - public virtual Task FindApplicationByIdAsync(string identifier, CancellationToken cancellationToken) { - var converter = TypeDescriptor.GetConverter(typeof(TKey)); - - // If the string key cannot be converted to TKey, return null - // to indicate that the requested application doesn't exist. - if (!converter.CanConvertFrom(typeof(string))) { - return Task.FromResult(null); - } - - var key = (TKey) converter.ConvertFromInvariantString(identifier); - - return Applications.SingleOrDefaultAsync(application => application.Id.Equals(key), cancellationToken); - } - - public virtual Task FindApplicationByLogoutRedirectUri(string url, CancellationToken cancellationToken) { - return Applications.SingleOrDefaultAsync(application => application.LogoutRedirectUri == url, cancellationToken); - } - - public virtual Task GetApplicationTypeAsync(TApplication application, CancellationToken cancellationToken) { - if (application == null) { - throw new ArgumentNullException(nameof(application)); - } - - return Task.FromResult(application.Type); - } - - public virtual Task GetDisplayNameAsync(TApplication application, CancellationToken cancellationToken) { - if (application == null) { - throw new ArgumentNullException(nameof(application)); - } - - return Task.FromResult(application.DisplayName); - } - - public virtual Task GetRedirectUriAsync(TApplication application, CancellationToken cancellationToken) { - if (application == null) { - throw new ArgumentNullException(nameof(application)); - } - - return Task.FromResult(application.RedirectUri); - } - - public virtual Task GetHashedSecretAsync(TApplication application, CancellationToken cancellationToken) { - if (application == null) { - throw new ArgumentNullException(nameof(application)); - } - - return Task.FromResult(application.Secret); - } - } -} \ No newline at end of file diff --git a/src/OpenIddict.EntityFramework/Models/OpenIddictApplication.cs b/src/OpenIddict.EntityFramework/Models/OpenIddictApplication.cs new file mode 100644 index 00000000..f9c20262 --- /dev/null +++ b/src/OpenIddict.EntityFramework/Models/OpenIddictApplication.cs @@ -0,0 +1,60 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +using System; + +namespace OpenIddict { + /// + /// Represents an OpenIddict application. + /// + public class OpenIddictApplication : OpenIddictApplication { + public OpenIddictApplication() { + // Generate a new string identifier. + Id = Guid.NewGuid().ToString(); + } + } + + /// + /// Represents an OpenIddict application. + /// + public class OpenIddictApplication where TKey : IEquatable { + /// + /// Gets or sets the display name + /// associated with the current application. + /// + public virtual string DisplayName { get; set; } + + /// + /// Gets or sets the unique identifier + /// associated with the current application. + /// + public virtual TKey Id { get; set; } + + /// + /// Gets or sets the logout callback URL + /// associated with the current application. + /// + public virtual string LogoutRedirectUri { get; set; } + + /// + /// Gets or sets the callback URL + /// associated with the current application. + /// + public virtual string RedirectUri { get; set; } + + /// + /// Gets or sets the hashed secret + /// associated with the current application. + /// + public virtual string Secret { get; set; } + + /// + /// Gets or sets the application type + /// associated with the current application. + /// + public virtual string Type { get; set; } + } +} \ No newline at end of file diff --git a/src/OpenIddict.EntityFramework/Models/OpenIddictAuthorization.cs b/src/OpenIddict.EntityFramework/Models/OpenIddictAuthorization.cs new file mode 100644 index 00000000..bd02983b --- /dev/null +++ b/src/OpenIddict.EntityFramework/Models/OpenIddictAuthorization.cs @@ -0,0 +1,54 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +using System; +using System.Collections.Generic; + +namespace OpenIddict { + /// + /// Represents an OpenIddict authorization. + /// + public class OpenIddictAuthorization : OpenIddictAuthorization { } + + /// + /// Represents an OpenIddict authorization. + /// + public class OpenIddictAuthorization : OpenIddictAuthorization { + public OpenIddictAuthorization() { + // Generate a new string identifier. + Id = Guid.NewGuid().ToString(); + } + } + + /// + /// Represents an OpenIddict authorization. + /// + public class OpenIddictAuthorization where TKey : IEquatable { + /// + /// Gets or sets the unique identifier + /// associated with the current authorization. + /// + public virtual TKey Id { get; set; } + + /// + /// Gets or sets the space-delimited scopes + /// associated with the current authorization. + /// + public virtual string Scope { get; set; } + + /// + /// Gets or sets the list of tokens + /// associated with the current authorization. + /// + public virtual IList Tokens { get; } = new List(); + + /// + /// Gets or sets the identifier of the user profile + /// associated with the current authorization. + /// + public virtual TKey UserId { get; set; } + } +} diff --git a/src/OpenIddict.EntityFramework/Models/OpenIddictScope.cs b/src/OpenIddict.EntityFramework/Models/OpenIddictScope.cs new file mode 100644 index 00000000..8ad408b8 --- /dev/null +++ b/src/OpenIddict.EntityFramework/Models/OpenIddictScope.cs @@ -0,0 +1,31 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +using System; + +namespace OpenIddict { + /// + /// Represents an OpenIddict scope. + /// + public class OpenIddictScope : OpenIddictScope { } + + /// + /// Represents an OpenIddict scope. + /// + public class OpenIddictScope where TKey : IEquatable { + /// + /// Gets or sets the public description + /// associated with the current scope. + /// + public virtual string Description { get; set; } + + /// + /// Gets or sets the unique identifier + /// associated with the current scope. + /// + public virtual TKey Id { get; set; } + } +} diff --git a/src/OpenIddict.EntityFramework/Models/OpenIddictToken.cs b/src/OpenIddict.EntityFramework/Models/OpenIddictToken.cs new file mode 100644 index 00000000..487830ec --- /dev/null +++ b/src/OpenIddict.EntityFramework/Models/OpenIddictToken.cs @@ -0,0 +1,48 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +using System; + +namespace OpenIddict { + /// + /// Represents an OpenIddict token. + /// + public class OpenIddictToken : OpenIddictToken { + public OpenIddictToken() { + // Generate a new string identifier. + Id = Guid.NewGuid().ToString(); + } + } + + /// + /// Represents an OpenIddict token. + /// + public class OpenIddictToken where TKey : IEquatable { + /// + /// Gets or sets the identifier of the authorization attached with the current token. + /// This property may be null if the token was issued without + /// requiring the user consent or is bound to a client application. + /// + public virtual TKey AuthorizationId { get; set; } + + /// + /// Gets or sets the unique identifier + /// associated with the current token. + /// + public virtual TKey Id { get; set; } + + /// + /// Gets or sets the type of the current token. + /// + public virtual string Type { get; set; } + + /// + /// Gets or sets the identifier of the user attached with the current token. + /// This property is null if the token represents a client application. + /// + public virtual TKey UserId { get; set; } + } +} diff --git a/src/OpenIddict.EntityFramework/Models/OpenIddictUser.cs b/src/OpenIddict.EntityFramework/Models/OpenIddictUser.cs new file mode 100644 index 00000000..125cca43 --- /dev/null +++ b/src/OpenIddict.EntityFramework/Models/OpenIddictUser.cs @@ -0,0 +1,41 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +using System; +using System.Collections.Generic; +using Microsoft.AspNetCore.Identity.EntityFrameworkCore; + +namespace OpenIddict { + /// + /// Represents an OpenIddict user. + /// + public class OpenIddictUser : OpenIddictUser { } + + /// + /// Represents an OpenIddict user. + /// + public class OpenIddictUser : OpenIddictUser { + public OpenIddictUser() { + // Generate a new string identifier. + Id = Guid.NewGuid().ToString(); + } + } + + /// + /// Represents an OpenIddict user. + /// + public class OpenIddictUser : IdentityUser where TKey : IEquatable { + /// + /// Gets the list of the authorizations associated with this user profile. + /// + public virtual IList Authorizations { get; } = new List(); + + /// + /// Gets the list of the tokens associated with this user profile. + /// + public virtual IList Tokens { get; } = new List(); + } +} diff --git a/src/OpenIddict.EF/OpenIddict.EF.xproj b/src/OpenIddict.EntityFramework/OpenIddict.EntityFramework.xproj similarity index 100% rename from src/OpenIddict.EF/OpenIddict.EF.xproj rename to src/OpenIddict.EntityFramework/OpenIddict.EntityFramework.xproj diff --git a/src/OpenIddict.EntityFramework/OpenIddictContext.cs b/src/OpenIddict.EntityFramework/OpenIddictContext.cs new file mode 100644 index 00000000..c0aa666a --- /dev/null +++ b/src/OpenIddict.EntityFramework/OpenIddictContext.cs @@ -0,0 +1,152 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +using System; +using Microsoft.AspNetCore.Identity.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; + +namespace OpenIddict { + /// + /// Represents an OpenIddict-powered Entity Framework context. + /// + public class OpenIddictContext : OpenIddictContext { + /// + /// Initializes a new OpenIddict context without configuring the Entity Framework options. + /// + protected OpenIddictContext() { } + + /// + /// Initializes a new OpenIddict context. + /// + /// The options used to configure the Entity Framework context. + public OpenIddictContext(DbContextOptions options) : base(options) { } + } + + /// + /// Represents an OpenIddict-powered Entity Framework context. + /// + /// The type of the User entity. + public class OpenIddictContext : OpenIddictContext + where TUser : OpenIddictUser { + /// + /// Initializes a new OpenIddict context without configuring the Entity Framework options. + /// + protected OpenIddictContext() { } + + /// + /// Initializes a new OpenIddict context. + /// + /// The options used to configure the Entity Framework context. + public OpenIddictContext(DbContextOptions options) : base(options) { } + } + + /// + /// Represents an OpenIddict-powered Entity Framework context. + /// + /// The type of the User entity. + /// The type of the Application entity. + /// The type of the Authorization entity. + /// The type of the Scope entity. + /// The type of the Token entity. + /// The type of the Role entity. + /// The type of the primary key used by the Identity/OpenIddict entities. + public class OpenIddictContext : IdentityDbContext + where TUser : OpenIddictUser + where TApplication : OpenIddictApplication + where TAuthorization : OpenIddictAuthorization + where TScope : OpenIddictScope + where TToken : OpenIddictToken + where TRole : IdentityRole + where TKey : IEquatable { + /// + /// Initializes a new OpenIddict context without configuring the Entity Framework options. + /// + protected OpenIddictContext() { } + + /// + /// Initializes a new OpenIddict context. + /// + /// The options used to configure the Entity Framework context. + public OpenIddictContext(DbContextOptions options) : base(options) { } + + /// + /// Gets or sets the database set containing the applications. + /// + public DbSet Applications { get; set; } + + /// + /// Gets or sets the database set containing the authorizations. + /// + public DbSet Authorizations { get; set; } + + /// + /// Gets or sets the database set containing the scopes. + /// + public DbSet Scopes { get; set; } + + /// + /// Gets or sets the database set containing the tokens. + /// + public DbSet Tokens { get; set; } + + /// + /// Registers the OpenIddict entities in the Entity Framework context. + /// + /// The model builder used by Entity Framework. + protected override void OnModelCreating(ModelBuilder builder) { + base.OnModelCreating(builder); + + // Configure the TApplication entity. + builder.Entity(entity => { + entity.HasKey(application => application.Id); + + entity.ToTable("OpenIddictApplications"); + }); + + // Configure the TAuthorization entity. + builder.Entity(entity => { + entity.HasKey(authorization => authorization.Id); + + entity.HasMany(authorization => authorization.Tokens) + .WithOne() + .HasForeignKey(token => token.AuthorizationId) + .IsRequired(required: false); + + entity.ToTable("OpenIddictAuthorizations"); + }); + + // Configure the TScope entity. + builder.Entity(entity => { + entity.HasKey(scope => scope.Id); + + entity.ToTable("OpenIddictScopes"); + }); + + // Configure the TToken entity. + builder.Entity(entity => { + entity.HasKey(token => token.Id); + + entity.ToTable("OpenIddictTokens"); + }); + + // Configure the TUser entity. + builder.Entity(entity => { + entity.HasMany(user => user.Authorizations) + .WithOne() + .HasForeignKey(authorization => authorization.UserId) + .IsRequired(required: false); + + entity.HasMany(user => user.Tokens) + .WithOne() + .HasForeignKey(token => token.UserId) + .IsRequired(required: false); + }); + } + } +} \ No newline at end of file diff --git a/src/OpenIddict.EntityFramework/OpenIddictExtensions.cs b/src/OpenIddict.EntityFramework/OpenIddictExtensions.cs new file mode 100644 index 00000000..c79e4518 --- /dev/null +++ b/src/OpenIddict.EntityFramework/OpenIddictExtensions.cs @@ -0,0 +1,79 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +using System; +using System.Diagnostics; +using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection.Extensions; +using OpenIddict; + +namespace Microsoft.AspNetCore.Builder { + public static class OpenIddictExtensions { + /// + /// Registers the Entity Framework stores. + /// + /// The services builder used by OpenIddict to register new services. + /// The . + public static OpenIddictBuilder AddEntityFramework([NotNull] this OpenIddictBuilder builder) + where TContext : DbContext { + return builder.AddEntityFramework(); + } + + /// + /// Registers the Entity Framework stores. + /// + /// The services builder used by OpenIddict to register new services. + /// The . + public static OpenIddictBuilder AddEntityFramework([NotNull] this OpenIddictBuilder builder) + where TContext : DbContext + where TKey : IEquatable { + if (builder == null) { + throw new ArgumentNullException(nameof(builder)); + } + + Debug.Assert(builder.ApplicationType != null && + builder.AuthorizationType != null && + builder.ScopeType != null && + builder.TokenType != null, "The entity types exposed by OpenIddictBuilder shouldn't be null."); + + // Register the application store in the DI container. + builder.Services.TryAddScoped( + typeof(IOpenIddictApplicationStore<>).MakeGenericType(builder.ApplicationType), + typeof(OpenIddictApplicationStore<,,>).MakeGenericType( + /* TApplication: */ builder.ApplicationType, + /* TContext: */ typeof(TContext), + /* TKey: */ typeof(TKey))); + + // Register the authorization store in the DI container. + builder.Services.TryAddScoped( + typeof(IOpenIddictAuthorizationStore<>).MakeGenericType(builder.AuthorizationType), + typeof(OpenIddictAuthorizationStore<,,,>).MakeGenericType( + /* TAuthorization: */ builder.AuthorizationType, + /* TToken: */ builder.TokenType, + /* TContext: */ typeof(TContext), + /* TKey: */ typeof(TKey))); + + // Register the scope store in the DI container. + builder.Services.TryAddScoped( + typeof(IOpenIddictScopeStore<>).MakeGenericType(builder.ScopeType), + typeof(OpenIddictScopeStore<,,>).MakeGenericType( + /* TScope: */ builder.ScopeType, + /* TContext: */ typeof(TContext), + /* TKey: */ typeof(TKey))); + + // Register the token store in the DI container. + builder.Services.TryAddScoped( + typeof(IOpenIddictTokenStore<>).MakeGenericType(builder.TokenType), + typeof(OpenIddictTokenStore<,,>).MakeGenericType( + /* TToken: */ builder.TokenType, + /* TContext: */ typeof(TContext), + /* TKey: */ typeof(TKey))); + + return builder; + } + } +} \ No newline at end of file diff --git a/src/OpenIddict.EntityFramework/Stores/OpenIddictApplicationStore.cs b/src/OpenIddict.EntityFramework/Stores/OpenIddictApplicationStore.cs new file mode 100644 index 00000000..93f9a679 --- /dev/null +++ b/src/OpenIddict.EntityFramework/Stores/OpenIddictApplicationStore.cs @@ -0,0 +1,140 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +using System; +using System.ComponentModel; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; + +namespace OpenIddict { + /// + /// Provides methods allowing to manage the applications stored in a database. + /// + /// The type of the Application entity. + /// The type of the Entity Framework database context. + /// The type of the entity primary keys. + public class OpenIddictApplicationStore : IOpenIddictApplicationStore + where TApplication : OpenIddictApplication + where TContext : DbContext + where TKey : IEquatable { + public OpenIddictApplicationStore(TContext context) { + Context = context; + } + + /// + /// Gets the database context associated with the current store. + /// + protected virtual TContext Context { get; } + + /// + /// Gets the database set corresponding to the entity. + /// + protected DbSet Applications => Context.Set(); + + /// + /// Creates a new application. + /// + /// The application to create. + /// The that can be used to abort the operation. + /// A that can be used to monitor the asynchronous operation. + public virtual Task CreateAsync(TApplication application, CancellationToken cancellationToken) { + if (application == null) { + throw new ArgumentNullException(nameof(application)); + } + + Context.Add(application); + + return Context.SaveChangesAsync(cancellationToken); + } + + /// + /// Retrieves an application using its unique identifier. + /// + /// The unique identifier 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 FindByIdAsync(string identifier, CancellationToken cancellationToken) { + var converter = TypeDescriptor.GetConverter(typeof(TKey)); + + // If the string key cannot be converted to TKey, return null + // to indicate that the requested application doesn't exist. + if (!converter.CanConvertFrom(typeof(string))) { + return Task.FromResult(null); + } + + var key = (TKey) converter.ConvertFromInvariantString(identifier); + + return Applications.SingleOrDefaultAsync(application => application.Id.Equals(key), cancellationToken); + } + + /// + /// Retrieves an application using its post_logout_redirect_uri. + /// + /// The post_logout_redirect_uri 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 FindByLogoutRedirectUri(string url, CancellationToken cancellationToken) { + return Applications.SingleOrDefaultAsync(application => application.LogoutRedirectUri == url, cancellationToken); + } + + /// + /// Retrieves the client type 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. + public virtual Task GetClientTypeAsync(TApplication application, CancellationToken cancellationToken) { + if (application == null) { + throw new ArgumentNullException(nameof(application)); + } + + return Task.FromResult(application.Type); + } + + /// + /// Retrieves the display name 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. + public virtual Task GetDisplayNameAsync(TApplication application, CancellationToken cancellationToken) { + if (application == null) { + throw new ArgumentNullException(nameof(application)); + } + + return Task.FromResult(application.DisplayName); + } + + /// + /// Retrieves the callback address 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. + public virtual Task GetRedirectUriAsync(TApplication application, CancellationToken cancellationToken) { + if (application == null) { + throw new ArgumentNullException(nameof(application)); + } + + return Task.FromResult(application.RedirectUri); + } + + /// + /// Retrieves the hashed secret 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. + public virtual Task GetHashedSecretAsync(TApplication application, CancellationToken cancellationToken) { + if (application == null) { + throw new ArgumentNullException(nameof(application)); + } + + return Task.FromResult(application.Secret); + } + } +} \ No newline at end of file diff --git a/src/OpenIddict.EntityFramework/Stores/OpenIddictAuthorizationStore.cs b/src/OpenIddict.EntityFramework/Stores/OpenIddictAuthorizationStore.cs new file mode 100644 index 00000000..9056ce97 --- /dev/null +++ b/src/OpenIddict.EntityFramework/Stores/OpenIddictAuthorizationStore.cs @@ -0,0 +1,37 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +using System; +using Microsoft.EntityFrameworkCore; + +namespace OpenIddict { + /// + /// Provides methods allowing to manage the authorizations stored in a database. + /// + /// The type of the Authorization entity. + /// The type of the Token entity. + /// The type of the Entity Framework database context. + /// The type of the entity primary keys. + public class OpenIddictAuthorizationStore : IOpenIddictAuthorizationStore + where TAuthorization : OpenIddictAuthorization + where TToken : OpenIddictToken + where TContext : DbContext + where TKey : IEquatable { + public OpenIddictAuthorizationStore(TContext context) { + Context = context; + } + + /// + /// Gets the database context associated with the current store. + /// + protected virtual TContext Context { get; } + + /// + /// Gets the database set corresponding to the entity. + /// + protected DbSet Authorizations => Context.Set(); + } +} \ No newline at end of file diff --git a/src/OpenIddict.EntityFramework/Stores/OpenIddictScopeStore.cs b/src/OpenIddict.EntityFramework/Stores/OpenIddictScopeStore.cs new file mode 100644 index 00000000..16586d11 --- /dev/null +++ b/src/OpenIddict.EntityFramework/Stores/OpenIddictScopeStore.cs @@ -0,0 +1,35 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +using System; +using Microsoft.EntityFrameworkCore; + +namespace OpenIddict { + /// + /// Provides methods allowing to manage the scopes stored in a database. + /// + /// The type of the Scope entity. + /// The type of the Entity Framework database context. + /// The type of the entity primary keys. + public class OpenIddictScopeStore : IOpenIddictScopeStore + where TScope : OpenIddictScope + where TContext : DbContext + where TKey : IEquatable { + public OpenIddictScopeStore(TContext context) { + Context = context; + } + + /// + /// Gets the database context associated with the current store. + /// + protected virtual TContext Context { get; } + + /// + /// Gets the database set corresponding to the entity. + /// + protected DbSet Authorizations => Context.Set(); + } +} \ No newline at end of file diff --git a/src/OpenIddict.EntityFramework/Stores/OpenIddictTokenStore.cs b/src/OpenIddict.EntityFramework/Stores/OpenIddictTokenStore.cs new file mode 100644 index 00000000..3986f8cb --- /dev/null +++ b/src/OpenIddict.EntityFramework/Stores/OpenIddictTokenStore.cs @@ -0,0 +1,97 @@ +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +using System; +using System.ComponentModel; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; + +namespace OpenIddict { + /// + /// Provides methods allowing to manage the tokens stored in a database. + /// + /// The type of the Token entity. + /// The type of the Entity Framework database context. + /// The type of the entity primary keys. + public class OpenIddictTokenStore : IOpenIddictTokenStore + where TToken : OpenIddictToken, new() + where TContext : DbContext + where TKey : IEquatable { + public OpenIddictTokenStore(TContext context) { + Context = context; + } + + /// + /// Gets the database context associated with the current store. + /// + protected virtual TContext Context { get; } + + /// + /// Gets the database set corresponding to the entity. + /// + protected DbSet Tokens => Context.Set(); + + /// + /// Creates a new token, defined by a unique identifier and a token type. + /// + /// The unique identifier associated with the token to create. + /// The token type. + /// The that can be used to abort the operation. + /// A that can be used to monitor the asynchronous operation. + public virtual Task CreateAsync(string identifier, string type, CancellationToken cancellationToken) { + var converter = TypeDescriptor.GetConverter(typeof(TKey)); + + // Ensure that the key type is compatible with string keys. + if (!converter.CanConvertFrom(typeof(string))) { + return Task.FromResult(0); + } + + var key = (TKey) converter.ConvertFromInvariantString(identifier); + + var token = new TToken { Id = key, Type = type }; + Tokens.Add(token); + + return Context.SaveChangesAsync(cancellationToken); + } + + /// + /// Retrieves an token using its unique identifier. + /// + /// The unique identifier associated with the token. + /// The that can be used to abort the operation. + /// A that can be used to monitor the asynchronous operation. + public virtual Task FindByIdAsync(string identifier, CancellationToken cancellationToken) { + var converter = TypeDescriptor.GetConverter(typeof(TKey)); + + // If the string key cannot be converted to TKey, return null + // to indicate that the requested token doesn't exist. + if (!converter.CanConvertFrom(typeof(string))) { + return Task.FromResult(null); + } + + var key = (TKey) converter.ConvertFromInvariantString(identifier); + + return Tokens.SingleOrDefaultAsync(token => token.Id.Equals(key), cancellationToken); + } + + /// + /// Revokes a token. + /// + /// The token to revoke. + /// The that can be used to abort the operation. + /// A that can be used to monitor the asynchronous operation. + public virtual Task RevokeAsync(TToken token, CancellationToken cancellationToken) { + if (token == null) { + throw new ArgumentNullException(nameof(token)); + } + + Context.Remove(token); + + return Context.SaveChangesAsync(cancellationToken); + } + } +} \ No newline at end of file diff --git a/src/OpenIddict.EF/project.json b/src/OpenIddict.EntityFramework/project.json similarity index 91% rename from src/OpenIddict.EF/project.json rename to src/OpenIddict.EntityFramework/project.json index bc514ef5..c4cab766 100644 --- a/src/OpenIddict.EF/project.json +++ b/src/OpenIddict.EntityFramework/project.json @@ -35,8 +35,7 @@ "dependencies": { "JetBrains.Annotations": { "type": "build", "version": "10.1.4" }, "Microsoft.AspNetCore.Identity.EntityFrameworkCore": "1.0.0-rc2-final", - "OpenIddict.Core": { "target": "project" }, - "OpenIddict.Models": { "target": "project" } + "OpenIddict.Core": { "target": "project" } }, "frameworks": { diff --git a/src/OpenIddict.Models/Application.cs b/src/OpenIddict.Models/Application.cs deleted file mode 100644 index 7d0833ea..00000000 --- a/src/OpenIddict.Models/Application.cs +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) - * See https://github.com/openiddict/openiddict-core for more information concerning - * the license and the contributors participating to this project. - */ - -using System; - -namespace OpenIddict.Models { - public class Application : Application { } - - public class Application where TKey : IEquatable { - public TKey Id { get; set; } - public string DisplayName { get; set; } - public string RedirectUri { get; set; } - public string LogoutRedirectUri { get; set; } - public string Secret { get; set; } - public string Type { get; set; } - } -} \ No newline at end of file diff --git a/src/OpenIddict.Models/OpenIddict.Models.xproj b/src/OpenIddict.Models/OpenIddict.Models.xproj deleted file mode 100644 index e8904b7d..00000000 --- a/src/OpenIddict.Models/OpenIddict.Models.xproj +++ /dev/null @@ -1,18 +0,0 @@ - - - - 14.0 - $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) - - - - 79ae02c3-2ab4-4495-bedd-685a714ea51c - OpenIddict.Models - .\obj - .\bin\ - - - 2.0 - - - \ No newline at end of file diff --git a/src/OpenIddict.Models/project.json b/src/OpenIddict.Models/project.json deleted file mode 100644 index ca2edc4e..00000000 --- a/src/OpenIddict.Models/project.json +++ /dev/null @@ -1,49 +0,0 @@ -{ - "version": "1.0.0-alpha2-*", - - "description": "Contains the default models used by OpenIddict.", - "authors": [ "Kévin Chalet" ], - - "packOptions": { - "owners": [ "Kévin Chalet" ], - - "projectUrl": "https://github.com/openiddict/openiddict-core", - "iconUrl": "https://avatars3.githubusercontent.com/u/13908567?s=64", - "licenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.html", - - "repository": { - "type": "git", - "url": "git://github.com/openiddict/openiddict-core" - }, - - "tags": [ - "aspnetcore", - "authentication", - "jwt", - "openidconnect", - "openiddict", - "security" - ] - }, - - "buildOptions": { - "warningsAsErrors": true, - "nowarn": [ "CS1591" ], - "xmlDoc": true - }, - - "frameworks": { - "net451": { }, - - "netstandard1.3": { - "dependencies": { - "System.Runtime": "4.1.0-rc2-24027" - }, - - "imports": [ - "dotnet5.4", - "portable-net451+win8" - ] - } - } -} \ No newline at end of file diff --git a/src/OpenIddict.Mvc/OpenIddictController.cs b/src/OpenIddict.Mvc/OpenIddictController.cs index 1abbaca0..9ede6c57 100644 --- a/src/OpenIddict.Mvc/OpenIddictController.cs +++ b/src/OpenIddict.Mvc/OpenIddictController.cs @@ -10,30 +10,23 @@ using System.Linq; using System.Security.Claims; using System.Threading.Tasks; using AspNet.Security.OpenIdConnect.Extensions; -using JetBrains.Annotations; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http.Authentication; +using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Options; using Microsoft.IdentityModel.Protocols.OpenIdConnect; namespace OpenIddict.Mvc { // Note: this controller is generic and doesn't need to be marked as internal to prevent MVC from discovering it. - public class OpenIddictController : Controller where TUser : class where TApplication : class { - public OpenIddictController([NotNull] OpenIddictServices services) { - Services = services; - } - - /// - /// Gets the OpenIddict services used by the controller. - /// - protected virtual OpenIddictServices Services { get; } - + public class OpenIddictController : Controller + where TUser : class where TApplication : class where TAuthorization : class where TToken : class { [HttpGet, HttpPost] - public virtual async Task Authorize() { + public virtual async Task Authorize([FromServices] OpenIddictApplicationManager applications) { // Note: when a fatal error occurs during the request processing, an OpenID Connect response - // is prematurely forged and added to the ASP.NET context by OpenIdConnectServerHandler. + // is prematurely forged and added to the ASP.NET Core context by OpenIdConnectServerHandler. var response = HttpContext.GetOpenIdConnectResponse(); if (response != null) { return View("Error", response); @@ -63,8 +56,8 @@ namespace OpenIddict.Mvc { // Note: AspNet.Security.OpenIdConnect.Server automatically ensures an application // corresponds to the client_id specified in the authorization request using - // IOpenIdConnectServerProvider.ValidateClientRedirectUri (see OpenIddictProvider.cs). - var application = await Services.Applications.FindApplicationByIdAsync(request.ClientId); + // IOpenIdConnectServerProvider.ValidateAuthorizationRequest (see OpenIddictProvider.cs). + var application = await applications.FindByIdAsync(request.ClientId); if (application == null) { return View("Error", new OpenIdConnectMessage { Error = OpenIdConnectConstants.Errors.InvalidClient, @@ -72,11 +65,15 @@ namespace OpenIddict.Mvc { }); } - return View("Authorize", Tuple.Create(request, await Services.Applications.GetDisplayNameAsync(application))); + return View("Authorize", Tuple.Create(request, await applications.GetDisplayNameAsync(application))); } [Authorize, HttpPost, ValidateAntiForgeryToken] - public virtual async Task Accept() { + public virtual async Task Accept( + [FromServices] UserManager users, + [FromServices] OpenIddictApplicationManager applications, + [FromServices] OpenIddictTokenManager tokens, + [FromServices] IOptions options) { var response = HttpContext.GetOpenIdConnectResponse(); if (response != null) { return View("Error", response); @@ -91,7 +88,7 @@ namespace OpenIddict.Mvc { } // Retrieve the user data using the unique identifier. - var user = await Services.Users.GetUserAsync(User); + var user = await users.GetUserAsync(User); if (user == null) { return View("Error", new OpenIdConnectMessage { Error = OpenIdConnectConstants.Errors.ServerError, @@ -101,10 +98,10 @@ namespace OpenIddict.Mvc { // Create a new ClaimsIdentity containing the claims that // will be used to create an id_token, a token or a code. - var identity = await Services.Applications.CreateIdentityAsync(user, request.GetScopes()); + var identity = await tokens.CreateIdentityAsync(user, request.GetScopes()); Debug.Assert(identity != null); - var application = await Services.Applications.FindApplicationByIdAsync(request.ClientId); + var application = await applications.FindByIdAsync(request.ClientId); if (application == null) { return View("Error", new OpenIdConnectMessage { Error = OpenIdConnectConstants.Errors.InvalidClient, @@ -116,7 +113,7 @@ namespace OpenIddict.Mvc { var ticket = new AuthenticationTicket( new ClaimsPrincipal(identity), new AuthenticationProperties(), - Services.Options.AuthenticationScheme); + options.Value.AuthenticationScheme); ticket.SetResources(request.GetResources()); ticket.SetScopes(request.GetScopes()); @@ -128,7 +125,7 @@ namespace OpenIddict.Mvc { } [Authorize, HttpPost, ValidateAntiForgeryToken] - public virtual Task Deny() { + public virtual Task Deny([FromServices] IOptions options) { var response = HttpContext.GetOpenIdConnectResponse(); if (response != null) { return Task.FromResult(View("Error", response)); @@ -145,7 +142,7 @@ 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. - return Task.FromResult(Forbid(Services.Options.AuthenticationScheme)); + return Task.FromResult(Forbid(options.Value.AuthenticationScheme)); } [HttpGet] @@ -167,15 +164,17 @@ namespace OpenIddict.Mvc { } [ActionName(nameof(Logout)), HttpPost, ValidateAntiForgeryToken] - public virtual async Task Signout() { + public virtual async Task Signout( + [FromServices] SignInManager signin, + [FromServices] IOptions options) { // Instruct the cookies middleware to delete the local cookie created // when the user agent is redirected from the external identity provider // after a successful authentication flow (e.g Google or Facebook). - await Services.SignIn.SignOutAsync(); + 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. - return SignOut(Services.Options.AuthenticationScheme); + return SignOut(options.Value.AuthenticationScheme); } } } \ No newline at end of file diff --git a/src/OpenIddict.Mvc/OpenIddictExtensions.cs b/src/OpenIddict.Mvc/OpenIddictExtensions.cs index 1e954ff1..ef4aa170 100644 --- a/src/OpenIddict.Mvc/OpenIddictExtensions.cs +++ b/src/OpenIddict.Mvc/OpenIddictExtensions.cs @@ -8,12 +8,12 @@ using System; using System.Collections.Generic; using System.Diagnostics; using System.Reflection; +using AspNet.Security.OpenIdConnect.Server; using JetBrains.Annotations; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc.ApplicationModels; using Microsoft.AspNetCore.Mvc.ApplicationParts; -using Microsoft.AspNetCore.Mvc.Razor.Compilation; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.Options; @@ -22,43 +22,67 @@ using OpenIddict.Mvc; namespace Microsoft.AspNetCore.Builder { public static class OpenIddictExtensions { - public static OpenIddictBuilder UseMvc([NotNull] this OpenIddictBuilder builder) { + /// + /// Registers the MVC module, including the built-in + /// authorization controller and the default consent views. + /// + /// The services builder used by OpenIddict to register new services. + /// The . + public static OpenIddictBuilder AddMvc([NotNull] this OpenIddictBuilder builder) { if (builder == null) { throw new ArgumentNullException(nameof(builder)); } - // Run MVC in an isolated environment. + builder.Services.AddMvc(); + + builder.Configure(options => { + // Set ApplicationCanDisplayErrors to true to allow OpenIddictController + // to intercept the error responses returned by the OpenID Connect server. + options.ApplicationCanDisplayErrors = true; + + if (!options.AuthorizationEndpointPath.HasValue) { + // Restore the default authorization endpoint path in the OpenIddict options. + options.AuthorizationEndpointPath = OpenIdConnectServerDefaults.AuthorizationEndpointPath; + } + + if (!options.LogoutEndpointPath.HasValue) { + // Restore the default logout endpoint path in the OpenIddict options. + options.LogoutEndpointPath = OpenIdConnectServerDefaults.LogoutEndpointPath; + } + }); + + // Run the MVC module in an isolated environment. return builder.AddModule("MVC", 10, app => app.Isolate(map => map.UseMvc(routes => { + var options = app.ApplicationServices.GetRequiredService>().Value; + // Register the actions corresponding to the authorization endpoint. - if (builder.Options.AuthorizationEndpointPath.HasValue) { - routes.MapRoute("{D97891B4}", builder.Options.AuthorizationEndpointPath.Value.Substring(1), new { - controller = "OpenIddict", action = nameof(OpenIddictController.Authorize) + if (options.AuthorizationEndpointPath.HasValue) { + routes.MapRoute("{D97891B4}", options.AuthorizationEndpointPath.Value.Substring(1), new { + controller = "OpenIddict", action = nameof(OpenIddictController.Authorize) }); - routes.MapRoute("{7148DB83}", builder.Options.AuthorizationEndpointPath.Value.Substring(1) + "/accept", new { - controller = "OpenIddict", action = nameof(OpenIddictController.Accept) + routes.MapRoute("{7148DB83}", options.AuthorizationEndpointPath.Value.Substring(1) + "/accept", new { + controller = "OpenIddict", action = nameof(OpenIddictController.Accept) }); - routes.MapRoute("{23438BCC}", builder.Options.AuthorizationEndpointPath.Value.Substring(1) + "/deny", new { - controller = "OpenIddict", action = nameof(OpenIddictController.Deny) + routes.MapRoute("{23438BCC}", options.AuthorizationEndpointPath.Value.Substring(1) + "/deny", new { + controller = "OpenIddict", action = nameof(OpenIddictController.Deny) }); } // Register the action corresponding to the logout endpoint. - if (builder.Options.LogoutEndpointPath.HasValue) { - routes.MapRoute("{C7DB102A}", builder.Options.LogoutEndpointPath.Value.Substring(1), new { - controller = "OpenIddict", action = nameof(OpenIddictController.Logout) + if (options.LogoutEndpointPath.HasValue) { + routes.MapRoute("{C7DB102A}", options.LogoutEndpointPath.Value.Substring(1), new { + controller = "OpenIddict", action = nameof(OpenIddictController.Logout) }); } }), services => { - var configuration = app.ApplicationServices.GetRequiredService(); - services.AddMvc() // Note: ConfigureApplicationPartManager() must be // called before AddControllersAsServices(). .ConfigureApplicationPartManager(manager => { manager.ApplicationParts.Clear(); - manager.ApplicationParts.Add(new OpenIddictPart(configuration)); + manager.ApplicationParts.Add(new OpenIddictPart(builder)); }) .AddControllersAsServices() @@ -71,62 +95,72 @@ namespace Microsoft.AspNetCore.Builder { // Update the Razor options to also use an embedded file provider that // falls back to the current assembly when searching for views. options.FileProviders.Add(new EmbeddedFileProvider( - assembly: typeof(OpenIddictController<,>).GetTypeInfo().Assembly, - baseNamespace: typeof(OpenIddictController<,>).Namespace)); + assembly: typeof(OpenIddictController<,,,>).GetTypeInfo().Assembly, + baseNamespace: typeof(OpenIddictController<,,,>).Namespace)); }); - // Register the user manager in the isolated container. - services.AddScoped(typeof(OpenIddictManager<,>).MakeGenericType(configuration.UserType, configuration.ApplicationType), provider => { + // Register the application manager in the isolated container. + services.AddScoped(typeof(OpenIddictApplicationManager<>).MakeGenericType(builder.ApplicationType), provider => { var accessor = provider.GetRequiredService(); var container = (IServiceProvider) accessor.HttpContext.Items[typeof(IServiceProvider)]; - Debug.Assert(container != null); + Debug.Assert(container != null, "The parent DI container cannot be resolved from the HTTP context."); - // Resolve the user manager from the parent container. - return container.GetRequiredService(typeof(OpenIddictManager<,>).MakeGenericType(configuration.UserType, configuration.ApplicationType)); + // Resolve the application manager from the parent container. + return container.GetRequiredService(typeof(OpenIddictApplicationManager<>).MakeGenericType(builder.ApplicationType)); }); - // Register the services context in the isolated container. - services.AddScoped(typeof(OpenIddictServices<,>).MakeGenericType(configuration.UserType, configuration.ApplicationType), provider => { + // Register the authorization manager in the isolated container. + services.AddScoped(typeof(OpenIddictAuthorizationManager<>).MakeGenericType(builder.AuthorizationType), provider => { var accessor = provider.GetRequiredService(); var container = (IServiceProvider) accessor.HttpContext.Items[typeof(IServiceProvider)]; - Debug.Assert(container != null); + Debug.Assert(container != null, "The parent DI container cannot be resolved from the HTTP context."); - // Resolve the services context from the parent container. - return container.GetRequiredService(typeof(OpenIddictServices<,>).MakeGenericType(configuration.UserType, configuration.ApplicationType)); + // Resolve the authorization manager from the parent container. + return container.GetRequiredService(typeof(OpenIddictAuthorizationManager<>).MakeGenericType(builder.AuthorizationType)); }); // Register the sign-in manager in the isolated container. - services.AddScoped(typeof(SignInManager<>).MakeGenericType(configuration.UserType), provider => { + services.AddScoped(typeof(SignInManager<>).MakeGenericType(builder.UserType), provider => { var accessor = provider.GetRequiredService(); var container = (IServiceProvider) accessor.HttpContext.Items[typeof(IServiceProvider)]; - Debug.Assert(container != null); + Debug.Assert(container != null, "The parent DI container cannot be resolved from the HTTP context."); // Resolve the sign-in manager from the parent container. - return container.GetRequiredService(typeof(SignInManager<>).MakeGenericType(configuration.UserType)); + return container.GetRequiredService(typeof(SignInManager<>).MakeGenericType(builder.UserType)); }); - // Register the user manager in the isolated container. - services.AddScoped(typeof(UserManager<>).MakeGenericType(configuration.UserType), provider => { + // Register the token manager in the isolated container. + services.AddScoped(typeof(OpenIddictTokenManager<,>).MakeGenericType( + /* TToken: */ builder.TokenType, + /* TUser: */ builder.UserType), provider => { var accessor = provider.GetRequiredService(); var container = (IServiceProvider) accessor.HttpContext.Items[typeof(IServiceProvider)]; - Debug.Assert(container != null); + Debug.Assert(container != null, "The parent DI container cannot be resolved from the HTTP context."); - // Resolve the user manager from the parent container. - return container.GetRequiredService(typeof(UserManager<>).MakeGenericType(configuration.UserType)); + // Resolve the token manager from the parent container. + return container.GetRequiredService(typeof(OpenIddictTokenManager<,>).MakeGenericType( + /* TToken: */ builder.TokenType, /* TUser: */ builder.UserType)); }); - // Register the compilation service in the isolated container. - services.AddScoped(provider => { + // Register the user manager in the isolated container. + services.AddScoped(typeof(UserManager<>).MakeGenericType(builder.UserType), provider => { var accessor = provider.GetRequiredService(); var container = (IServiceProvider) accessor.HttpContext.Items[typeof(IServiceProvider)]; - Debug.Assert(container != null); + Debug.Assert(container != null, "The parent DI container cannot be resolved from the HTTP context."); - // Resolve the compilation service from the parent container. - return container.GetRequiredService(); + // Resolve the user manager from the parent container. + return container.GetRequiredService(typeof(UserManager<>).MakeGenericType(builder.UserType)); }); // Register the options in the isolated container. - services.AddSingleton(Options.Create(builder.Options)); + services.AddSingleton(provider => { + var accessor = provider.GetRequiredService(); + var container = (IServiceProvider) accessor.HttpContext.Items[typeof(IServiceProvider)]; + Debug.Assert(container != null, "The parent DI container cannot be resolved from the HTTP context."); + + // Resolve the user manager from the parent container. + return container.GetRequiredService>(); + }); })); } @@ -135,7 +169,7 @@ namespace Microsoft.AspNetCore.Builder { // 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<,>)); + 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. @@ -144,12 +178,13 @@ namespace Microsoft.AspNetCore.Builder { } private class OpenIddictPart : ApplicationPart, IApplicationPartTypeProvider { - public OpenIddictPart(OpenIddictConfiguration configuration) { + public OpenIddictPart(OpenIddictBuilder builder) { Types = new[] { - typeof(OpenIddictController<,>) - .MakeGenericType(configuration.UserType, - configuration.ApplicationType) - .GetTypeInfo() + typeof(OpenIddictController<,,,>).MakeGenericType( + /* TUser: */ builder.UserType, + /* TApplication: */ builder.ApplicationType, + /* TAuthorization: */ builder.AuthorizationType, + /* TToken: */ builder.TokenType).GetTypeInfo() }; } diff --git a/src/OpenIddict.Security/OpenIddictExtensions.cs b/src/OpenIddict.Security/OpenIddictExtensions.cs index 2d7329ba..0b7b2f43 100644 --- a/src/OpenIddict.Security/OpenIddictExtensions.cs +++ b/src/OpenIddict.Security/OpenIddictExtensions.cs @@ -1,24 +1,42 @@ -using System; +/* + * Licensed under the Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) + * See https://github.com/openiddict/openiddict-core for more information concerning + * the license and the contributors participating to this project. + */ + +using System; using JetBrains.Annotations; using Microsoft.AspNetCore.Cors.Infrastructure; +using Microsoft.Extensions.DependencyInjection; using NWebsec.AspNetCore.Middleware; namespace Microsoft.AspNetCore.Builder { public static class OpenIddictExtensions { - public static OpenIddictBuilder UseNWebsec([NotNull] this OpenIddictBuilder builder) { + /// + /// Registers the NWebsec module using the default Content Security Policy. + /// + /// The services builder used by OpenIddict to register new services. + /// The . + public static OpenIddictBuilder AddNWebsec([NotNull] this OpenIddictBuilder builder) { if (builder == null) { throw new ArgumentNullException(nameof(builder)); } - return builder.UseNWebsec(options => { + return builder.AddNWebsec(options => { options.DefaultSources(directive => directive.Self()) - .ImageSources(directive => directive.Self().CustomSources("*")) - .ScriptSources(directive => directive.Self().UnsafeInline()) - .StyleSources(directive => directive.Self().UnsafeInline()); + .ImageSources(directive => directive.Self()) + .ScriptSources(directive => directive.Self()) + .StyleSources(directive => directive.Self()); }); } - public static OpenIddictBuilder UseNWebsec( + /// + /// Registers the NWebsec module using the specified Content Security Policy. + /// + /// The services builder used by OpenIddict to register new services. + /// The delegate used to configure the Content Security Policy options. + /// The . + public static OpenIddictBuilder AddNWebsec( [NotNull] this OpenIddictBuilder builder, [NotNull] Action configuration) { if (builder == null) { @@ -40,15 +58,21 @@ namespace Microsoft.AspNetCore.Builder { // Insert a new middleware responsible of setting the X-Frame-Options header. // See https://nwebsec.codeplex.com/wikipage?title=Configuring%20security%20headers&referringTitle=NWebsec - app.UseXfo(options => options.Deny()); + app.UseXfo(xfo => xfo.Deny()); // Insert a new middleware responsible of setting the X-Xss-Protection header. // See https://nwebsec.codeplex.com/wikipage?title=Configuring%20security%20headers&referringTitle=NWebsec - app.UseXXssProtection(options => options.EnabledWithBlockMode()); + app.UseXXssProtection(xss => xss.EnabledWithBlockMode()); }); } - public static OpenIddictBuilder UseCors( + /// + /// Registers the CORS module. + /// + /// The services builder used by OpenIddict to register new services. + /// The delegate used to configure the CORS policy. + /// The . + public static OpenIddictBuilder AddCors( [NotNull] this OpenIddictBuilder builder, [NotNull] Action configuration) { if (builder == null) { @@ -59,6 +83,7 @@ namespace Microsoft.AspNetCore.Builder { throw new ArgumentNullException(nameof(configuration)); } + builder.Services.AddCors(); builder.AddModule("CORS", -10, map => map.UseCors(configuration)); return builder; diff --git a/src/OpenIddict/OpenIddictExtensions.cs b/src/OpenIddict/OpenIddictExtensions.cs index 810c9009..820a1801 100644 --- a/src/OpenIddict/OpenIddictExtensions.cs +++ b/src/OpenIddict/OpenIddictExtensions.cs @@ -6,56 +6,96 @@ using System; using JetBrains.Annotations; -using Microsoft.AspNetCore.Identity; -using OpenIddict.Models; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using OpenIddict; namespace Microsoft.AspNetCore.Builder { public static class OpenIddictExtensions { - public static IdentityBuilder AddOpenIddict([NotNull] this IdentityBuilder builder) { - if (builder == null) { - throw new ArgumentNullException(nameof(builder)); + /// + /// Registers the default OpenIddict services in the DI container, + /// including the Entity Framework stores and the built-in entities. + /// + /// The type of the Entity Framework database context. + /// The services collection. + /// + /// Note: the core services include native support for the non-interactive flows + /// (resource owner password credentials, client credentials, refresh token). + /// To support interactive flows like authorization code or implicit/hybrid, + /// consider adding the MVC module or creating your own authorization controller. + /// + /// The . + public static OpenIddictBuilder AddOpenIddict([NotNull] this IServiceCollection services) + where TContext : DbContext { + if (services == null) { + throw new ArgumentNullException(nameof(services)); } - return builder.AddOpenIddictCore(configuration => { - // Use the EF adapter by default. - configuration.UseEntityFramework(); - }); + return services.AddOpenIddict(); } - public static IdentityBuilder AddOpenIddict([NotNull] this IdentityBuilder builder) - where TApplication : class { - if (builder == null) { - throw new ArgumentNullException(nameof(builder)); + /// + /// Registers the default OpenIddict services in the DI container, + /// including the Entity Framework stores and the specified entities. + /// + /// The type of the User entity. + /// The type of the Entity Framework database context. + /// The services collection. + /// + /// Note: the core services include native support for the non-interactive flows + /// (resource owner password credentials, client credentials, refresh token). + /// To support interactive flows like authorization code or implicit/hybrid, + /// consider adding the MVC module or creating your own authorization controller. + /// + /// The . + public static OpenIddictBuilder AddOpenIddict([NotNull] this IServiceCollection services) + where TUser : OpenIddictUser + where TContext : DbContext { + if (services == null) { + throw new ArgumentNullException(nameof(services)); } - return builder.AddOpenIddictCore(configuration => { - // Use the EF adapter by default. - configuration.UseEntityFramework(); - }); + return services.AddOpenIddict(); } - public static IApplicationBuilder UseOpenIddict([NotNull] this IApplicationBuilder app) { - return app.UseOpenIddict(options => { }); - } - - public static IApplicationBuilder UseOpenIddict( - [NotNull] this IApplicationBuilder app, - [NotNull] Action configuration) { - if (app == null) { - throw new ArgumentNullException(nameof(app)); + /// + /// Registers the default OpenIddict services in the DI container, + /// including the Entity Framework stores and the specified entities. + /// + /// The type of the User entity. + /// The type of the Application entity. + /// The type of the Authorization entity. + /// The type of the Scope entity. + /// The type of the Token entity. + /// The type of the Entity Framework database context. + /// The type of the entity primary keys. + /// The services collection. + /// + /// Note: the core services include native support for the non-interactive flows + /// (resource owner password credentials, client credentials, refresh token). + /// To support interactive flows like authorization code or implicit/hybrid, + /// consider adding the MVC module or creating your own authorization controller. + /// + /// The . + public static OpenIddictBuilder AddOpenIddict( + [NotNull] this IServiceCollection services) + where TUser : OpenIddictUser + where TApplication : OpenIddictApplication + where TAuthorization : OpenIddictAuthorization + where TScope : OpenIddictScope + where TToken : OpenIddictToken + where TContext : DbContext + where TKey : IEquatable { + if (services == null) { + throw new ArgumentNullException(nameof(services)); } - if (configuration == null) { - throw new ArgumentNullException(nameof(configuration)); - } - - return app.UseOpenIddictCore(builder => { - builder.UseAssets(); - builder.UseNWebsec(); - builder.UseMvc(); - - configuration(builder); - }); + // Register the OpenIddict core services and the default EntityFramework stores. + return services.AddOpenIddict() + .AddEntityFramework(); } } } \ No newline at end of file diff --git a/src/OpenIddict/project.json b/src/OpenIddict/project.json index f0374ec6..c8e4da03 100644 --- a/src/OpenIddict/project.json +++ b/src/OpenIddict/project.json @@ -34,18 +34,15 @@ "dependencies": { "JetBrains.Annotations": { "type": "build", "version": "10.1.4" }, - "OpenIddict.Assets": { "target": "project" }, - "OpenIddict.EF": { "target": "project" }, - "OpenIddict.Mvc": { "target": "project" }, - "OpenIddict.Security": { "target": "project" } + "OpenIddict.EntityFramework": { "target": "project" } }, "frameworks": { "net451": { }, - "netstandard1.5": { + "netstandard1.4": { "imports": [ - "dotnet5.6", + "dotnet5.5", "portable-net451+win8" ] }