From 5e7a5c103ba017a357d2ecaf19fbfc4a3fb9a91b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Chalet?= Date: Sat, 22 Apr 2017 04:13:17 +0200 Subject: [PATCH 01/11] Port OpenIddict to the new ASP.NET Core 2.0 authentication stack --- .travis.yml | 2 - NuGet.config | 1 + build/common.props | 6 + build/dependencies.props | 19 +- build/version.props | 4 +- .../Controllers/AuthenticationController.cs | 18 +- .../Mvc.Client/Controllers/HomeController.cs | 3 +- samples/Mvc.Client/Mvc.Client.csproj | 4 +- samples/Mvc.Client/Startup.cs | 61 ++- .../Controllers/AuthorizationController.cs | 49 +- .../Controllers/ManageController.cs | 2 +- .../Controllers/ResourceController.cs | 2 +- .../Controllers/UserinfoController.cs | 2 +- .../Extensions/AppBuilderExtensions.cs | 49 -- samples/Mvc.Server/Models/ApplicationUser.cs | 2 +- samples/Mvc.Server/Mvc.Server.csproj | 4 +- samples/Mvc.Server/Startup.cs | 79 +-- .../Manage/ManageLoginsViewModel.cs | 4 +- samples/Mvc.Server/Views/Account/Login.cshtml | 4 +- .../Views/Manage/ManageLogins.cshtml | 2 +- src/OpenIddict.Core/OpenIddict.Core.csproj | 7 +- src/OpenIddict.Core/OpenIddictExtensions.cs | 17 +- .../OpenIddict.EntityFrameworkCore.csproj | 6 +- .../OpenIddictCustomizer.cs | 5 + .../OpenIddictExtension.cs | 12 +- src/OpenIddict.Mvc/OpenIddict.Mvc.csproj | 2 +- src/OpenIddict/OpenIddict.csproj | 3 +- src/OpenIddict/OpenIddictExtensions.cs | 135 ++---- src/OpenIddict/OpenIddictHandler.cs | 23 + src/OpenIddict/OpenIddictInitializer.cs | 101 ++++ src/OpenIddict/OpenIddictOptions.cs | 6 + .../OpenIddictProvider.Authentication.cs | 83 ++-- .../OpenIddictProvider.Discovery.cs | 16 +- src/OpenIddict/OpenIddictProvider.Exchange.cs | 57 +-- .../OpenIddictProvider.Introspection.cs | 30 +- .../OpenIddictProvider.Revocation.cs | 36 +- .../OpenIddictProvider.Serialization.cs | 34 +- src/OpenIddict/OpenIddictProvider.Session.cs | 40 +- src/OpenIddict/OpenIddictProvider.Userinfo.cs | 2 +- src/OpenIddict/OpenIddictProvider.cs | 41 +- .../OpenIddict.Core.Tests.csproj | 4 +- ...penIddict.EntityFrameworkCore.Tests.csproj | 4 +- .../OpenIddict.Mvc.Tests.csproj | 4 +- test/OpenIddict.Tests/OpenIddict.Tests.csproj | 6 +- .../OpenIddictExtensionsTests.cs | 459 +++++------------- .../OpenIddictInitializerTests.cs | 164 +++++++ .../OpenIddictProviderTests.Authentication.cs | 14 +- .../OpenIddictProviderTests.Exchange.cs | 1 - .../OpenIddictProviderTests.Introspection.cs | 1 - .../OpenIddictProviderTests.Revocation.cs | 1 - .../OpenIddictProviderTests.Session.cs | 3 +- .../OpenIddictProviderTests.cs | 93 ++-- 52 files changed, 810 insertions(+), 917 deletions(-) delete mode 100644 samples/Mvc.Server/Extensions/AppBuilderExtensions.cs create mode 100644 src/OpenIddict/OpenIddictHandler.cs create mode 100644 src/OpenIddict/OpenIddictInitializer.cs create mode 100644 test/OpenIddict.Tests/OpenIddictInitializerTests.cs diff --git a/.travis.yml b/.travis.yml index f62f9896..7f87ae83 100644 --- a/.travis.yml +++ b/.travis.yml @@ -14,8 +14,6 @@ env: global: - DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true - DOTNET_CLI_TELEMETRY_OPTOUT: 1 - - KOREBUILD_DOTNET_VERSION: 2.0.0-preview1-005418 - - KOREBUILD_DOTNET_SHARED_RUNTIME_VERSION: 2.0.0-preview1-001919-00 mono: none os: - linux diff --git a/NuGet.config b/NuGet.config index ae8aea9e..8b9726ce 100644 --- a/NuGet.config +++ b/NuGet.config @@ -2,6 +2,7 @@ + \ No newline at end of file diff --git a/build/common.props b/build/common.props index 2ced2fd2..9df4a916 100644 --- a/build/common.props +++ b/build/common.props @@ -12,4 +12,10 @@ portable + + + <_GenerateBindingRedirectsIntermediateAppConfig>$(IntermediateOutputPath)$(TargetFileName).config + + + diff --git a/build/dependencies.props b/build/dependencies.props index f0fb20a2..8377df10 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -1,16 +1,19 @@ - 1.0.0 - 1.0.0 - 1.0.0 + 2.0.0-* + 2.0.0-* + 2.0.0-* + 4.4.0-* 2.0.0 10.3.0 - 1.6.0 - 4.7.8 - 1.0.0 - 15.0.0 - 2.2.0 + 1.0.1 + 4.7.63 + 2.0.0-* + 2.0.0-* + 2.0.0-* + 15.3.0-* + 2.3.0-* diff --git a/build/version.props b/build/version.props index ba6ff1a7..1c84ac4f 100644 --- a/build/version.props +++ b/build/version.props @@ -1,8 +1,8 @@ - 1.0.0 - beta2 + 2.0.0 + preview1 $(VersionSuffix)-$(BuildNumber) diff --git a/samples/Mvc.Client/Controllers/AuthenticationController.cs b/samples/Mvc.Client/Controllers/AuthenticationController.cs index da8b2cd7..c2abf32f 100644 --- a/samples/Mvc.Client/Controllers/AuthenticationController.cs +++ b/samples/Mvc.Client/Controllers/AuthenticationController.cs @@ -1,7 +1,6 @@ -using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Authentication.OpenIdConnect; -using Microsoft.AspNetCore.Http.Authentication; using Microsoft.AspNetCore.Mvc; namespace Mvc.Client.Controllers @@ -13,21 +12,16 @@ namespace Mvc.Client.Controllers { // Instruct the OIDC client middleware to redirect the user agent to the identity provider. // Note: the authenticationType parameter must match the value configured in Startup.cs - return new ChallengeResult(OpenIdConnectDefaults.AuthenticationScheme, new AuthenticationProperties - { - RedirectUri = "/" - }); + return Challenge(new AuthenticationProperties { RedirectUri = "/" }, OpenIdConnectDefaults.AuthenticationScheme); } [HttpGet("~/signout"), HttpPost("~/signout")] - public async Task SignOut() + public ActionResult SignOut() { // Instruct the cookies middleware to delete the local cookie created when the user agent - // is redirected from the identity provider after a successful authorization flow. - await HttpContext.Authentication.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); - - // Instruct the OpenID Connect middleware to redirect the user agent to the identity provider to sign out. - await HttpContext.Authentication.SignOutAsync(OpenIdConnectDefaults.AuthenticationScheme); + // is redirected from the identity provider after a successful authorization flow and + // to redirect the user agent to the identity provider to sign out. + return SignOut(CookieAuthenticationDefaults.AuthenticationScheme, OpenIdConnectDefaults.AuthenticationScheme); } } } \ No newline at end of file diff --git a/samples/Mvc.Client/Controllers/HomeController.cs b/samples/Mvc.Client/Controllers/HomeController.cs index 25ee9450..49b6892a 100644 --- a/samples/Mvc.Client/Controllers/HomeController.cs +++ b/samples/Mvc.Client/Controllers/HomeController.cs @@ -4,6 +4,7 @@ using System.Net.Http.Headers; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.IdentityModel.Protocols.OpenIdConnect; @@ -28,7 +29,7 @@ namespace Mvc.Client.Controllers [Authorize, HttpPost("~/")] public async Task Index(CancellationToken cancellationToken) { - var token = await HttpContext.Authentication.GetTokenAsync(OpenIdConnectParameterNames.AccessToken); + var token = await HttpContext.GetTokenAsync(CookieAuthenticationDefaults.AuthenticationScheme, OpenIdConnectParameterNames.AccessToken); if (string.IsNullOrEmpty(token)) { throw new InvalidOperationException("The access token cannot be found in the authentication ticket. " + diff --git a/samples/Mvc.Client/Mvc.Client.csproj b/samples/Mvc.Client/Mvc.Client.csproj index 7502f6e6..087e662a 100644 --- a/samples/Mvc.Client/Mvc.Client.csproj +++ b/samples/Mvc.Client/Mvc.Client.csproj @@ -1,9 +1,9 @@  - + - net451;netcoreapp1.0 + net461;netcoreapp2.0 diff --git a/samples/Mvc.Client/Startup.cs b/samples/Mvc.Client/Startup.cs index 9ae46b44..02206b22 100644 --- a/samples/Mvc.Client/Startup.cs +++ b/samples/Mvc.Client/Startup.cs @@ -14,53 +14,52 @@ namespace Mvc.Client { services.AddAuthentication(options => { - options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme; - }); - - services.AddMvc(); + options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme; + }) - services.AddSingleton(); - } - - public void Configure(IApplicationBuilder app) - { - app.UseDeveloperExceptionPage(); - - app.UseStaticFiles(); - - // Insert a new cookies middleware in the pipeline to store the user - // identity after he has been redirected from the identity provider. - app.UseCookieAuthentication(new CookieAuthenticationOptions + .AddCookie(options => { - AutomaticAuthenticate = true, - AutomaticChallenge = true, - LoginPath = new PathString("/signin") - }); + options.LoginPath = new PathString("/signin"); + }) - app.UseOpenIdConnectAuthentication(new OpenIdConnectOptions + .AddOpenIdConnect(options => { // Note: these settings must match the application details // inserted in the database at the server level. - ClientId = "mvc", - ClientSecret = "901564A5-E7FE-42CB-B10D-61EF6A8F3654", - PostLogoutRedirectUri = "http://localhost:53507/", + options.ClientId = "mvc"; + options.ClientSecret = "901564A5-E7FE-42CB-B10D-61EF6A8F3654"; - RequireHttpsMetadata = false, - GetClaimsFromUserInfoEndpoint = true, - SaveTokens = true, + options.RequireHttpsMetadata = false; + options.GetClaimsFromUserInfoEndpoint = true; + options.SaveTokens = true; // Use the authorization code flow. - ResponseType = OpenIdConnectResponseType.Code, - AuthenticationMethod = OpenIdConnectRedirectBehavior.RedirectGet, + options.ResponseType = OpenIdConnectResponseType.Code; + options.AuthenticationMethod = OpenIdConnectRedirectBehavior.RedirectGet; // Note: setting the Authority allows the OIDC client middleware to automatically // retrieve the identity provider's configuration and spare you from setting // the different endpoints URIs or the token validation parameters explicitly. - Authority = "http://localhost:54540/", + options.Authority = "http://localhost:54540/"; - Scope = { "email", "roles", "offline_access" } + options.Scope.Add("email"); + options.Scope.Add("roles"); + options.Scope.Add("offline_access"); }); + services.AddMvc(); + + services.AddSingleton(); + } + + public void Configure(IApplicationBuilder app) + { + app.UseDeveloperExceptionPage(); + + app.UseStaticFiles(); + + app.UseAuthentication(); + app.UseMvc(); } } diff --git a/samples/Mvc.Server/Controllers/AuthorizationController.cs b/samples/Mvc.Server/Controllers/AuthorizationController.cs index 9b05179e..50145847 100644 --- a/samples/Mvc.Server/Controllers/AuthorizationController.cs +++ b/samples/Mvc.Server/Controllers/AuthorizationController.cs @@ -14,7 +14,6 @@ using AspNet.Security.OpenIdConnect.Server; 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; @@ -168,44 +167,10 @@ namespace Mvc.Server }); } - // Ensure the user is allowed to sign in. - if (!await _signInManager.CanSignInAsync(user)) - { - return BadRequest(new OpenIdConnectResponse - { - Error = OpenIdConnectConstants.Errors.InvalidGrant, - ErrorDescription = "The specified user is not allowed to sign in." - }); - } - - // Reject the token request if two-factor authentication has been enabled by the user. - if (_userManager.SupportsUserTwoFactor && await _userManager.GetTwoFactorEnabledAsync(user)) - { - return BadRequest(new OpenIdConnectResponse - { - Error = OpenIdConnectConstants.Errors.InvalidGrant, - ErrorDescription = "The specified user is not allowed to sign in." - }); - } - - // Ensure the user is not already locked out. - if (_userManager.SupportsUserLockout && await _userManager.IsLockedOutAsync(user)) - { - return BadRequest(new OpenIdConnectResponse - { - Error = OpenIdConnectConstants.Errors.InvalidGrant, - ErrorDescription = "The username/password couple is invalid." - }); - } - - // Ensure the password is valid. - if (!await _userManager.CheckPasswordAsync(user, request.Password)) + // Validate the username/password parameters and ensure the account is not locked out. + var result = await _signInManager.CheckPasswordSignInAsync(user, request.Password, lockoutOnFailure: true); + if (!result.Succeeded) { - if (_userManager.SupportsUserLockout) - { - await _userManager.AccessFailedAsync(user); - } - return BadRequest(new OpenIdConnectResponse { Error = OpenIdConnectConstants.Errors.InvalidGrant, @@ -213,11 +178,6 @@ namespace Mvc.Server }); } - if (_userManager.SupportsUserLockout) - { - await _userManager.ResetAccessFailedCountAsync(user); - } - // Create a new authentication ticket. var ticket = await CreateTicketAsync(request, user); @@ -227,8 +187,7 @@ namespace Mvc.Server else if (request.IsAuthorizationCodeGrantType() || request.IsRefreshTokenGrantType()) { // Retrieve the claims principal stored in the authorization code/refresh token. - var info = await HttpContext.Authentication.GetAuthenticateInfoAsync( - OpenIdConnectServerDefaults.AuthenticationScheme); + var info = await HttpContext.AuthenticateAsync(OpenIdConnectServerDefaults.AuthenticationScheme); // Retrieve the user profile corresponding to the authorization code/refresh token. // Note: if you want to automatically invalidate the authorization code/refresh token diff --git a/samples/Mvc.Server/Controllers/ManageController.cs b/samples/Mvc.Server/Controllers/ManageController.cs index 31e22adf..99b742e1 100644 --- a/samples/Mvc.Server/Controllers/ManageController.cs +++ b/samples/Mvc.Server/Controllers/ManageController.cs @@ -272,7 +272,7 @@ namespace Mvc.Server.Controllers return View("Error"); } var userLogins = await _userManager.GetLoginsAsync(user); - var otherLogins = _signInManager.GetExternalAuthenticationSchemes().Where(auth => userLogins.All(ul => auth.AuthenticationScheme != ul.LoginProvider)).ToList(); + var otherLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()).Where(auth => userLogins.All(ul => auth.Name != ul.LoginProvider)).ToList(); ViewData["ShowRemoveButton"] = user.PasswordHash != null || userLogins.Count > 1; return View(new ManageLoginsViewModel { diff --git a/samples/Mvc.Server/Controllers/ResourceController.cs b/samples/Mvc.Server/Controllers/ResourceController.cs index 94642400..6d754dbc 100644 --- a/samples/Mvc.Server/Controllers/ResourceController.cs +++ b/samples/Mvc.Server/Controllers/ResourceController.cs @@ -17,7 +17,7 @@ namespace Mvc.Server.Controllers _userManager = userManager; } - [Authorize(ActiveAuthenticationSchemes = OAuthValidationDefaults.AuthenticationScheme)] + [Authorize(AuthenticationSchemes = OAuthValidationDefaults.AuthenticationScheme)] [HttpGet("message")] public async Task GetMessage() { diff --git a/samples/Mvc.Server/Controllers/UserinfoController.cs b/samples/Mvc.Server/Controllers/UserinfoController.cs index 129711b6..643d7d2c 100644 --- a/samples/Mvc.Server/Controllers/UserinfoController.cs +++ b/samples/Mvc.Server/Controllers/UserinfoController.cs @@ -22,7 +22,7 @@ namespace Mvc.Server.Controllers // // GET: /api/userinfo - [Authorize(ActiveAuthenticationSchemes = OAuthValidationDefaults.AuthenticationScheme)] + [Authorize(AuthenticationSchemes = OAuthValidationDefaults.AuthenticationScheme)] [HttpGet("userinfo"), Produces("application/json")] public async Task Userinfo() { diff --git a/samples/Mvc.Server/Extensions/AppBuilderExtensions.cs b/samples/Mvc.Server/Extensions/AppBuilderExtensions.cs deleted file mode 100644 index 58f91546..00000000 --- a/samples/Mvc.Server/Extensions/AppBuilderExtensions.cs +++ /dev/null @@ -1,49 +0,0 @@ -using System; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Http; - -namespace Mvc.Server.Extensions -{ - public static class AppBuilderExtensions - { - public static IApplicationBuilder UseWhen(this IApplicationBuilder app, - Func condition, Action configuration) - { - if (app == null) - { - throw new ArgumentNullException(nameof(app)); - } - - if (condition == null) - { - throw new ArgumentNullException(nameof(condition)); - } - - if (configuration == null) - { - throw new ArgumentNullException(nameof(configuration)); - } - - var builder = app.New(); - configuration(builder); - - return app.Use(next => - { - builder.Run(next); - - var branch = builder.Build(); - - return context => - { - if (condition(context)) - { - return branch(context); - } - - return next(context); - }; - }); - } - } -} diff --git a/samples/Mvc.Server/Models/ApplicationUser.cs b/samples/Mvc.Server/Models/ApplicationUser.cs index 905c7c47..2f3f4a21 100644 --- a/samples/Mvc.Server/Models/ApplicationUser.cs +++ b/samples/Mvc.Server/Models/ApplicationUser.cs @@ -1,4 +1,4 @@ -using Microsoft.AspNetCore.Identity.EntityFrameworkCore; +using Microsoft.AspNetCore.Identity; namespace Mvc.Server.Models { diff --git a/samples/Mvc.Server/Mvc.Server.csproj b/samples/Mvc.Server/Mvc.Server.csproj index dcf02fb7..8c129d28 100644 --- a/samples/Mvc.Server/Mvc.Server.csproj +++ b/samples/Mvc.Server/Mvc.Server.csproj @@ -1,9 +1,9 @@  - + - net451;netcoreapp1.0 + net461;netcoreapp2.0 diff --git a/samples/Mvc.Server/Startup.cs b/samples/Mvc.Server/Startup.cs index 4b9d6c7f..37b6f37a 100644 --- a/samples/Mvc.Server/Startup.cs +++ b/samples/Mvc.Server/Startup.cs @@ -3,11 +3,10 @@ using System.Threading; using System.Threading.Tasks; using AspNet.Security.OpenIdConnect.Primitives; using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Identity.EntityFrameworkCore; +using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; -using Mvc.Server.Extensions; using Mvc.Server.Models; using Mvc.Server.Services; using OpenIddict.Core; @@ -52,6 +51,21 @@ namespace Mvc.Server options.ClaimsIdentity.RoleClaimType = OpenIdConnectConstants.Claims.Role; }); + services.AddAuthentication() + .AddGoogle(options => + { + options.ClientId = "560027070069-37ldt4kfuohhu3m495hk2j4pjp92d382.apps.googleusercontent.com"; + options.ClientSecret = "n2Q-GEw9RQjzcRbU3qhfTj8f"; + }) + + .AddTwitter(options => + { + options.ConsumerKey = "6XaCTaLbMqfj6ww3zvZ5g"; + options.ConsumerSecret = "Il2eFzGIrYhz6BWjYhVXBPQSfZuS4xoHpSSyD9PI"; + }) + + .AddOAuthValidation(); + // Register the OpenIddict services. services.AddOpenIddict(options => { @@ -105,64 +119,9 @@ namespace Mvc.Server app.UseStaticFiles(); - app.UseWhen(context => context.Request.Path.StartsWithSegments("/api"), branch => - { - // Add a middleware used to validate access - // tokens and protect the API endpoints. - branch.UseOAuthValidation(); - - // If you prefer using JWT, don't forget to disable the automatic - // JWT -> WS-Federation claims mapping used by the JWT middleware: - // - // JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); - // JwtSecurityTokenHandler.DefaultOutboundClaimTypeMap.Clear(); - // - // branch.UseJwtBearerAuthentication(new JwtBearerOptions - // { - // Authority = "http://localhost:54540/", - // Audience = "resource_server", - // RequireHttpsMetadata = false, - // TokenValidationParameters = new TokenValidationParameters - // { - // NameClaimType = OpenIdConnectConstants.Claims.Subject, - // RoleClaimType = OpenIdConnectConstants.Claims.Role - // } - // }); - - // Alternatively, you can also use the introspection middleware. - // Using it is recommended if your resource server is in a - // different application/separated from the authorization server. - // - // branch.UseOAuthIntrospection(options => - // { - // options.Authority = new Uri("http://localhost:54540/"); - // options.Audiences.Add("resource_server"); - // options.ClientId = "resource_server"; - // options.ClientSecret = "875sqd4s5d748z78z7ds1ff8zz8814ff88ed8ea4z4zzd"; - // options.RequireHttpsMetadata = false; - // }); - }); - - app.UseWhen(context => !context.Request.Path.StartsWithSegments("/api"), branch => - { - branch.UseStatusCodePagesWithReExecute("/error"); - - branch.UseIdentity(); - - branch.UseGoogleAuthentication(new GoogleOptions - { - ClientId = "560027070069-37ldt4kfuohhu3m495hk2j4pjp92d382.apps.googleusercontent.com", - ClientSecret = "n2Q-GEw9RQjzcRbU3qhfTj8f" - }); - - branch.UseTwitterAuthentication(new TwitterOptions - { - ConsumerKey = "6XaCTaLbMqfj6ww3zvZ5g", - ConsumerSecret = "Il2eFzGIrYhz6BWjYhVXBPQSfZuS4xoHpSSyD9PI" - }); - }); + app.UseStatusCodePagesWithReExecute("/error"); - app.UseOpenIddict(); + app.UseAuthentication(); app.UseMvcWithDefaultRoute(); @@ -187,7 +146,7 @@ namespace Mvc.Server { ClientId = "mvc", DisplayName = "MVC client application", - LogoutRedirectUri = "http://localhost:53507/", + LogoutRedirectUri = "http://localhost:53507/signout-callback-oidc", RedirectUri = "http://localhost:53507/signin-oidc" }; diff --git a/samples/Mvc.Server/ViewModels/Manage/ManageLoginsViewModel.cs b/samples/Mvc.Server/ViewModels/Manage/ManageLoginsViewModel.cs index 8f063701..8e1adfb4 100644 --- a/samples/Mvc.Server/ViewModels/Manage/ManageLoginsViewModel.cs +++ b/samples/Mvc.Server/ViewModels/Manage/ManageLoginsViewModel.cs @@ -1,5 +1,5 @@ using System.Collections.Generic; -using Microsoft.AspNetCore.Http.Authentication; +using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Identity; namespace Mvc.Server.ViewModels.Manage @@ -8,6 +8,6 @@ namespace Mvc.Server.ViewModels.Manage { public IList CurrentLogins { get; set; } - public IList OtherLogins { get; set; } + public IList OtherLogins { get; set; } } } diff --git a/samples/Mvc.Server/Views/Account/Login.cshtml b/samples/Mvc.Server/Views/Account/Login.cshtml index 7721654a..f560d1cb 100644 --- a/samples/Mvc.Server/Views/Account/Login.cshtml +++ b/samples/Mvc.Server/Views/Account/Login.cshtml @@ -57,7 +57,7 @@

Use another service to log in.


@{ - var loginProviders = SignInManager.GetExternalAuthenticationSchemes().ToList(); + var loginProviders = (await SignInManager.GetExternalAuthenticationSchemesAsync()).ToList(); if (loginProviders.Count == 0) {
@@ -74,7 +74,7 @@

@foreach (var provider in loginProviders) { - + }

diff --git a/samples/Mvc.Server/Views/Manage/ManageLogins.cshtml b/samples/Mvc.Server/Views/Manage/ManageLogins.cshtml index 8e77a488..98ebfd34 100644 --- a/samples/Mvc.Server/Views/Manage/ManageLogins.cshtml +++ b/samples/Mvc.Server/Views/Manage/ManageLogins.cshtml @@ -46,7 +46,7 @@

@foreach (var provider in Model.OtherLogins) { - + }

diff --git a/src/OpenIddict.Core/OpenIddict.Core.csproj b/src/OpenIddict.Core/OpenIddict.Core.csproj index 41e36a8b..2446f756 100644 --- a/src/OpenIddict.Core/OpenIddict.Core.csproj +++ b/src/OpenIddict.Core/OpenIddict.Core.csproj @@ -3,7 +3,7 @@ - net451;netstandard1.3 + netstandard2.0 @@ -25,9 +25,4 @@
- - - - -
diff --git a/src/OpenIddict.Core/OpenIddictExtensions.cs b/src/OpenIddict.Core/OpenIddictExtensions.cs index 3ed52d03..d0e2a93c 100644 --- a/src/OpenIddict.Core/OpenIddictExtensions.cs +++ b/src/OpenIddict.Core/OpenIddictExtensions.cs @@ -66,22 +66,19 @@ namespace Microsoft.Extensions.DependencyInjection services.AddOptions(); - var builder = new OpenIddictBuilder(services) + // Register the OpenIddict core services in the DI container. + services.TryAddScoped>(); + services.TryAddScoped>(); + services.TryAddScoped>(); + services.TryAddScoped>(); + + return new OpenIddictBuilder(services) { ApplicationType = typeof(TApplication), AuthorizationType = typeof(TAuthorization), ScopeType = typeof(TScope), TokenType = typeof(TToken) }; - - // Register the OpenIddict core services in the DI container. - builder.Services.TryAddSingleton(builder); - builder.Services.TryAddScoped>(); - builder.Services.TryAddScoped>(); - builder.Services.TryAddScoped>(); - builder.Services.TryAddScoped>(); - - return builder; } /// diff --git a/src/OpenIddict.EntityFrameworkCore/OpenIddict.EntityFrameworkCore.csproj b/src/OpenIddict.EntityFrameworkCore/OpenIddict.EntityFrameworkCore.csproj index 15217e43..3a7c4e6e 100644 --- a/src/OpenIddict.EntityFrameworkCore/OpenIddict.EntityFrameworkCore.csproj +++ b/src/OpenIddict.EntityFrameworkCore/OpenIddict.EntityFrameworkCore.csproj @@ -3,7 +3,7 @@ - net451;netstandard1.3 + netstandard2.0 @@ -21,8 +21,4 @@ - - - - diff --git a/src/OpenIddict.EntityFrameworkCore/OpenIddictCustomizer.cs b/src/OpenIddict.EntityFrameworkCore/OpenIddictCustomizer.cs index 861c845e..87c418d2 100644 --- a/src/OpenIddict.EntityFrameworkCore/OpenIddictCustomizer.cs +++ b/src/OpenIddict.EntityFrameworkCore/OpenIddictCustomizer.cs @@ -24,6 +24,11 @@ namespace OpenIddict.EntityFrameworkCore where TToken : OpenIddictToken, new() where TKey : IEquatable { + public OpenIddictCustomizer([NotNull] ModelCustomizerDependencies dependencies) + : base(dependencies) + { + } + public override void Customize([NotNull] ModelBuilder builder, [NotNull] DbContext context) { if (builder == null) diff --git a/src/OpenIddict.EntityFrameworkCore/OpenIddictExtension.cs b/src/OpenIddict.EntityFrameworkCore/OpenIddictExtension.cs index ad20237b..93aa71d7 100644 --- a/src/OpenIddict.EntityFrameworkCore/OpenIddictExtension.cs +++ b/src/OpenIddict.EntityFrameworkCore/OpenIddictExtension.cs @@ -19,7 +19,9 @@ namespace OpenIddict.EntityFrameworkCore where TToken : OpenIddictToken, new() where TKey : IEquatable { - public void ApplyServices([NotNull] IServiceCollection services) + public string LogFragment => null; + + public bool ApplyServices([NotNull] IServiceCollection services) { if (services == null) { @@ -27,6 +29,14 @@ namespace OpenIddict.EntityFrameworkCore } services.AddSingleton>(); + + // Return false to indicate that no database + // provider was registered by this extension. + return false; } + + public long GetServiceProviderHashCode() => 0; + + public void Validate([NotNull] IDbContextOptions options) { } } } diff --git a/src/OpenIddict.Mvc/OpenIddict.Mvc.csproj b/src/OpenIddict.Mvc/OpenIddict.Mvc.csproj index 7a350f53..883679fb 100644 --- a/src/OpenIddict.Mvc/OpenIddict.Mvc.csproj +++ b/src/OpenIddict.Mvc/OpenIddict.Mvc.csproj @@ -3,7 +3,7 @@ - net451;netstandard1.6 + netstandard2.0 diff --git a/src/OpenIddict/OpenIddict.csproj b/src/OpenIddict/OpenIddict.csproj index 49e0c916..e1f26ed7 100644 --- a/src/OpenIddict/OpenIddict.csproj +++ b/src/OpenIddict/OpenIddict.csproj @@ -3,7 +3,7 @@ - net451;netstandard1.4 + netstandard2.0 @@ -21,6 +21,7 @@ + diff --git a/src/OpenIddict/OpenIddictExtensions.cs b/src/OpenIddict/OpenIddictExtensions.cs index 6ce4490f..a4970cd0 100644 --- a/src/OpenIddict/OpenIddictExtensions.cs +++ b/src/OpenIddict/OpenIddictExtensions.cs @@ -8,16 +8,16 @@ using System; using System.Collections.Generic; using System.IdentityModel.Tokens.Jwt; using System.IO; -using System.Linq; using System.Reflection; using System.Security.Cryptography.X509Certificates; using AspNet.Security.OpenIdConnect.Primitives; using AspNet.Security.OpenIdConnect.Server; using JetBrains.Annotations; +using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.DataProtection; using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Caching.Distributed; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Options; using Microsoft.IdentityModel.Tokens; using OpenIddict; @@ -26,98 +26,6 @@ namespace Microsoft.AspNetCore.Builder { public static class OpenIddictExtensions { - /// - /// 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)); - } - - // Resolve the OpenIddict builder from the DI container. - // If it cannot be found, throw an invalid operation exception. - var builder = app.ApplicationServices.GetService(); - if (builder == null) - { - throw new InvalidOperationException("The OpenIddict services cannot be resolved from the dependency injection container. " + - "Make sure 'services.AddOpenIddict()' is correctly called from 'ConfigureServices()'."); - } - - // Resolve the OpenIddict options from the DI container. - var options = app.ApplicationServices.GetRequiredService>().Value; - - // When no authorization provider has been registered in the options, - // create a new OpenIddictProvider instance using the specified entities. - if (options.Provider == null) - { - options.Provider = (OpenIdConnectServerProvider) Activator.CreateInstance( - typeof(OpenIddictProvider<,,,>).MakeGenericType( - /* TApplication: */ builder.ApplicationType, - /* TAuthorization: */ builder.AuthorizationType, - /* TScope: */ builder.ScopeType, - /* TToken: */ builder.TokenType)); - } - - // When no distributed cache has been registered in the options, - // try to resolve it from the dependency injection container. - if (options.Cache == null) - { - options.Cache = app.ApplicationServices.GetService(); - - if (options.EnableRequestCaching && options.Cache == null) - { - throw new InvalidOperationException("A distributed cache implementation must be registered in the OpenIddict options " + - "or in the dependency injection container when enabling request caching support."); - } - } - - // Ensure at least one flow has been enabled. - if (options.GrantTypes.Count == 0) - { - throw new InvalidOperationException("At least one OAuth2/OpenID Connect flow must be enabled."); - } - - // Ensure the authorization endpoint has been enabled when - // the authorization code or implicit grants are supported. - if (!options.AuthorizationEndpointPath.HasValue && (options.GrantTypes.Contains(OpenIdConnectConstants.GrantTypes.AuthorizationCode) || - options.GrantTypes.Contains(OpenIdConnectConstants.GrantTypes.Implicit))) - { - throw new InvalidOperationException("The authorization endpoint must be enabled to use " + - "the authorization code and implicit flows."); - } - - // Ensure the token endpoint has been enabled when the authorization code, - // client credentials, password or refresh token grants are supported. - if (!options.TokenEndpointPath.HasValue && (options.GrantTypes.Contains(OpenIdConnectConstants.GrantTypes.AuthorizationCode) || - options.GrantTypes.Contains(OpenIdConnectConstants.GrantTypes.ClientCredentials) || - options.GrantTypes.Contains(OpenIdConnectConstants.GrantTypes.Password) || - options.GrantTypes.Contains(OpenIdConnectConstants.GrantTypes.RefreshToken))) - { - throw new InvalidOperationException("The token endpoint must be enabled to use the authorization code, " + - "client credentials, password and refresh token flows."); - } - - if (options.RevocationEndpointPath.HasValue && options.DisableTokenRevocation) - { - throw new InvalidOperationException("The revocation endpoint cannot be enabled when token revocation is disabled."); - } - - // Ensure at least one asymmetric signing certificate/key was registered if the implicit flow was enabled. - if (!options.SigningCredentials.Any(credentials => credentials.Key is AsymmetricSecurityKey) && - options.GrantTypes.Contains(OpenIdConnectConstants.GrantTypes.Implicit)) - { - throw new InvalidOperationException("At least one asymmetric signing key must be registered when enabling the implicit flow. " + - "Consider registering a X.509 certificate using 'services.AddOpenIddict().AddSigningCertificate()' " + - "or call 'services.AddOpenIddict().AddEphemeralSigningKey()' to use an ephemeral key."); - } - - return app.UseOpenIdConnectServer(options); - } - /// /// Amends the default OpenIddict configuration. /// @@ -139,7 +47,44 @@ namespace Microsoft.AspNetCore.Builder throw new ArgumentNullException(nameof(configuration)); } - builder.Services.Configure(configuration); + // Register the OpenIddict handler/provider. + builder.Services.TryAddScoped(); + builder.Services.TryAddScoped( + typeof(OpenIdConnectServerProvider), + typeof(OpenIddictProvider<,,,>).MakeGenericType( + /* TApplication: */ builder.ApplicationType, + /* TAuthorization: */ builder.AuthorizationType, + /* TScope: */ builder.ScopeType, + /* TToken: */ builder.TokenType)); + + // Register the options initializers used by the OpenID Connect server handler and OpenIddict. + // Note: TryAddEnumerable() is used here to ensure the initializers are only registered once. + builder.Services.TryAddEnumerable( + ServiceDescriptor.Singleton, + OpenIdConnectServerInitializer>()); + + builder.Services.TryAddEnumerable( + ServiceDescriptor.Singleton, + OpenIddictInitializer>()); + + // Register the OpenID Connect server handler in the authentication options, + // so it can be discovered by the default authentication handler provider. + builder.Services.Configure(options => + { + // Note: similarly to Identity, OpenIddict should be registered only once. + // To prevent multiple schemes from being registered, a check is made here. + if (options.SchemeMap.ContainsKey(OpenIdConnectServerDefaults.AuthenticationScheme)) + { + return; + } + + options.AddScheme(OpenIdConnectServerDefaults.AuthenticationScheme, scheme => + { + scheme.HandlerType = typeof(OpenIddictHandler); + }); + }); + + builder.Services.Configure(OpenIdConnectServerDefaults.AuthenticationScheme, configuration); return builder; } diff --git a/src/OpenIddict/OpenIddictHandler.cs b/src/OpenIddict/OpenIddictHandler.cs new file mode 100644 index 00000000..69157b74 --- /dev/null +++ b/src/OpenIddict/OpenIddictHandler.cs @@ -0,0 +1,23 @@ +using System.ComponentModel; +using System.Text.Encodings.Web; +using AspNet.Security.OpenIdConnect.Server; +using JetBrains.Annotations; +using Microsoft.AspNetCore.Authentication; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace OpenIddict +{ + [EditorBrowsable(EditorBrowsableState.Never)] + public class OpenIddictHandler : OpenIdConnectServerHandler + { + public OpenIddictHandler( + [NotNull] IOptionsMonitor options, + [NotNull] ILoggerFactory logger, + [NotNull] UrlEncoder encoder, + [NotNull] ISystemClock clock) + : base(options, logger, encoder, clock) + { + } + } +} diff --git a/src/OpenIddict/OpenIddictInitializer.cs b/src/OpenIddict/OpenIddictInitializer.cs new file mode 100644 index 00000000..3a713893 --- /dev/null +++ b/src/OpenIddict/OpenIddictInitializer.cs @@ -0,0 +1,101 @@ +/* + * 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.Linq; +using AspNet.Security.OpenIdConnect.Primitives; +using JetBrains.Annotations; +using Microsoft.Extensions.Caching.Distributed; +using Microsoft.Extensions.Options; +using Microsoft.IdentityModel.Tokens; + +namespace OpenIddict +{ + /// + /// Contains the methods required to ensure that the configuration + /// used by OpenIddict is in a consistent and valid state. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public class OpenIddictInitializer : IPostConfigureOptions + { + private readonly IDistributedCache _cache; + + /// + /// Creates a new instance of the class. + /// + public OpenIddictInitializer([NotNull] IDistributedCache cache) + { + _cache = cache; + } + + /// + /// Populates the default OpenID Connect server options and ensure + /// that the configuration is in a consistent and valid state. + /// + /// The authentication scheme associated with the handler instance. + /// The options instance to initialize. + public void PostConfigure([NotNull] string name, [NotNull] OpenIddictOptions options) + { + if (options == null) + { + throw new ArgumentNullException(nameof(options)); + } + + if (string.IsNullOrEmpty(name)) + { + throw new ArgumentException("The options instance name cannot be null or empty.", nameof(name)); + } + + // When no distributed cache has been registered in the options, + // try to resolve it from the dependency injection container. + if (options.Cache == null) + { + options.Cache = _cache; + } + + // Ensure at least one flow has been enabled. + if (options.GrantTypes.Count == 0) + { + throw new InvalidOperationException("At least one OAuth2/OpenID Connect flow must be enabled."); + } + + // Ensure the authorization endpoint has been enabled when + // the authorization code or implicit grants are supported. + if (!options.AuthorizationEndpointPath.HasValue && (options.GrantTypes.Contains(OpenIdConnectConstants.GrantTypes.AuthorizationCode) || + options.GrantTypes.Contains(OpenIdConnectConstants.GrantTypes.Implicit))) + { + throw new InvalidOperationException("The authorization endpoint must be enabled to use " + + "the authorization code and implicit flows."); + } + + // Ensure the token endpoint has been enabled when the authorization code, + // client credentials, password or refresh token grants are supported. + if (!options.TokenEndpointPath.HasValue && (options.GrantTypes.Contains(OpenIdConnectConstants.GrantTypes.AuthorizationCode) || + options.GrantTypes.Contains(OpenIdConnectConstants.GrantTypes.ClientCredentials) || + options.GrantTypes.Contains(OpenIdConnectConstants.GrantTypes.Password) || + options.GrantTypes.Contains(OpenIdConnectConstants.GrantTypes.RefreshToken))) + { + throw new InvalidOperationException("The token endpoint must be enabled to use the authorization code, " + + "client credentials, password and refresh token flows."); + } + + if (options.RevocationEndpointPath.HasValue && options.DisableTokenRevocation) + { + throw new InvalidOperationException("The revocation endpoint cannot be enabled when token revocation is disabled."); + } + + // Ensure at least one asymmetric signing certificate/key was registered if the implicit flow was enabled. + if (!options.SigningCredentials.Any(credentials => credentials.Key is AsymmetricSecurityKey) && + options.GrantTypes.Contains(OpenIdConnectConstants.GrantTypes.Implicit)) + { + throw new InvalidOperationException("At least one asymmetric signing key must be registered when enabling the implicit flow. " + + "Consider registering a X.509 certificate using 'services.AddOpenIddict().AddSigningCertificate()' " + + "or call 'services.AddOpenIddict().AddEphemeralSigningKey()' to use an ephemeral key."); + } + } + } +} diff --git a/src/OpenIddict/OpenIddictOptions.cs b/src/OpenIddict/OpenIddictOptions.cs index 0960ae63..61aa03e2 100644 --- a/src/OpenIddict/OpenIddictOptions.cs +++ b/src/OpenIddict/OpenIddictOptions.cs @@ -16,9 +16,15 @@ namespace OpenIddict /// public class OpenIddictOptions : OpenIdConnectServerOptions { + /// + /// Creates a new instance of the class. + /// public OpenIddictOptions() { + // Note: OpenIdConnectServerProvider is automatically mapped + // to the generic OpenIddictProvider class by the DI container. Provider = null; + ProviderType = typeof(OpenIdConnectServerProvider); } /// diff --git a/src/OpenIddict/OpenIddictProvider.Authentication.cs b/src/OpenIddict/OpenIddictProvider.Authentication.cs index 66623dc1..f2dfc781 100644 --- a/src/OpenIddict/OpenIddictProvider.Authentication.cs +++ b/src/OpenIddict/OpenIddictProvider.Authentication.cs @@ -14,9 +14,7 @@ using JetBrains.Annotations; using Microsoft.AspNetCore.Diagnostics; using Microsoft.AspNetCore.WebUtilities; using Microsoft.Extensions.Caching.Distributed; -using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; using Newtonsoft.Json; using Newtonsoft.Json.Bson; using Newtonsoft.Json.Linq; @@ -29,13 +27,12 @@ namespace OpenIddict { public override async Task ExtractAuthorizationRequest([NotNull] ExtractAuthorizationRequestContext context) { - var logger = context.HttpContext.RequestServices.GetRequiredService>>(); - var options = context.HttpContext.RequestServices.GetRequiredService>(); + var options = (OpenIddictOptions) context.Options; // Reject requests using the unsupported request parameter. if (!string.IsNullOrEmpty(context.Request.Request)) { - logger.LogError("The authorization request was rejected because it contained " + + Logger.LogError("The authorization request was rejected because it contained " + "an unsupported parameter: {Parameter}.", "request"); context.Reject( @@ -48,7 +45,7 @@ namespace OpenIddict // Reject requests using the unsupported request_uri parameter. if (!string.IsNullOrEmpty(context.Request.RequestUri)) { - logger.LogError("The authorization request was rejected because it contained " + + Logger.LogError("The authorization request was rejected because it contained " + "an unsupported parameter: {Parameter}.", "request_uri"); context.Reject( @@ -63,9 +60,9 @@ namespace OpenIddict if (!string.IsNullOrEmpty(context.Request.RequestId)) { // Return an error if request caching support was not enabled. - if (!options.Value.EnableRequestCaching) + if (!options.EnableRequestCaching) { - logger.LogError("The authorization request was rejected because " + + Logger.LogError("The authorization request was rejected because " + "request caching support was not enabled."); context.Reject( @@ -79,10 +76,10 @@ namespace OpenIddict // to avoid collisions with the other types of cached requests. var key = OpenIddictConstants.Environment.AuthorizationRequest + context.Request.RequestId; - var payload = await options.Value.Cache.GetAsync(key); + var payload = await options.Cache.GetAsync(key); if (payload == null) { - logger.LogError("The authorization request was rejected because an unknown " + + Logger.LogError("The authorization request was rejected because an unknown " + "or invalid request_id parameter was specified."); context.Reject( @@ -93,7 +90,7 @@ namespace OpenIddict } // Restore the authorization request parameters from the serialized payload. - using (var reader = new BsonReader(new MemoryStream(payload))) + using (var reader = new BsonDataReader(new MemoryStream(payload))) { foreach (var parameter in JObject.Load(reader)) { @@ -111,16 +108,14 @@ namespace OpenIddict public override async Task ValidateAuthorizationRequest([NotNull] ValidateAuthorizationRequestContext context) { - var applications = context.HttpContext.RequestServices.GetRequiredService>(); - var logger = context.HttpContext.RequestServices.GetRequiredService>>(); - var options = context.HttpContext.RequestServices.GetRequiredService>(); + var options = (OpenIddictOptions) context.Options; // Note: the OpenID Connect server middleware supports authorization code, implicit, hybrid, // none and custom flows but OpenIddict uses a stricter policy rejecting unknown flows. if (!context.Request.IsAuthorizationCodeFlow() && !context.Request.IsHybridFlow() && !context.Request.IsImplicitFlow() && !context.Request.IsNoneFlow()) { - logger.LogError("The authorization request was rejected because the '{ResponseType}' " + + Logger.LogError("The authorization request was rejected because the '{ResponseType}' " + "response type is not supported.", context.Request.ResponseType); context.Reject( @@ -132,9 +127,9 @@ namespace OpenIddict // Reject code flow authorization requests if the authorization code flow is not enabled. if (context.Request.IsAuthorizationCodeFlow() && - !options.Value.GrantTypes.Contains(OpenIdConnectConstants.GrantTypes.AuthorizationCode)) + !options.GrantTypes.Contains(OpenIdConnectConstants.GrantTypes.AuthorizationCode)) { - logger.LogError("The authorization request was rejected because " + + Logger.LogError("The authorization request was rejected because " + "the authorization code flow was not enabled."); context.Reject( @@ -145,9 +140,9 @@ namespace OpenIddict } // Reject implicit flow authorization requests if the implicit flow is not enabled. - if (context.Request.IsImplicitFlow() && !options.Value.GrantTypes.Contains(OpenIdConnectConstants.GrantTypes.Implicit)) + if (context.Request.IsImplicitFlow() && !options.GrantTypes.Contains(OpenIdConnectConstants.GrantTypes.Implicit)) { - logger.LogError("The authorization request was rejected because the implicit flow was not enabled."); + Logger.LogError("The authorization request was rejected because the implicit flow was not enabled."); context.Reject( error: OpenIdConnectConstants.Errors.UnsupportedResponseType, @@ -157,10 +152,10 @@ namespace OpenIddict } // Reject hybrid flow authorization requests if the authorization code or the implicit flows are not enabled. - if (context.Request.IsHybridFlow() && (!options.Value.GrantTypes.Contains(OpenIdConnectConstants.GrantTypes.AuthorizationCode) || - !options.Value.GrantTypes.Contains(OpenIdConnectConstants.GrantTypes.Implicit))) + if (context.Request.IsHybridFlow() && (!options.GrantTypes.Contains(OpenIdConnectConstants.GrantTypes.AuthorizationCode) || + !options.GrantTypes.Contains(OpenIdConnectConstants.GrantTypes.Implicit))) { - logger.LogError("The authorization request was rejected because the " + + Logger.LogError("The authorization request was rejected because the " + "authorization code flow or the implicit flow was not enabled."); context.Reject( @@ -172,7 +167,7 @@ namespace OpenIddict // Reject authorization requests that specify scope=offline_access if the refresh token flow is not enabled. if (context.Request.HasScope(OpenIdConnectConstants.Scopes.OfflineAccess) && - !options.Value.GrantTypes.Contains(OpenIdConnectConstants.GrantTypes.RefreshToken)) + !options.GrantTypes.Contains(OpenIdConnectConstants.GrantTypes.RefreshToken)) { context.Reject( error: OpenIdConnectConstants.Errors.InvalidRequest, @@ -188,7 +183,7 @@ namespace OpenIddict !context.Request.IsFragmentResponseMode() && !context.Request.IsQueryResponseMode()) { - logger.LogError("The authorization request was rejected because the '{ResponseMode}' " + + Logger.LogError("The authorization request was rejected because the '{ResponseMode}' " + "response mode is not supported.", context.Request.ResponseMode); context.Reject( @@ -219,7 +214,7 @@ namespace OpenIddict // reject the authorization request if the code_challenge_method is missing. if (string.IsNullOrEmpty(context.Request.CodeChallengeMethod)) { - logger.LogError("The authorization request was rejected because the " + + Logger.LogError("The authorization request was rejected because the " + "required 'code_challenge_method' parameter was missing."); context.Reject( @@ -233,7 +228,7 @@ namespace OpenIddict // See https://tools.ietf.org/html/rfc7636#section-7.2 for more information. if (string.Equals(context.Request.CodeChallengeMethod, OpenIdConnectConstants.CodeChallengeMethods.Plain)) { - logger.LogError("The authorization request was rejected because the " + + Logger.LogError("The authorization request was rejected because the " + "'code_challenge_method' parameter was set to 'plain'."); context.Reject( @@ -246,7 +241,7 @@ namespace OpenIddict // Reject authorization requests that contain response_type=token when a code_challenge is specified. if (context.Request.HasResponseType(OpenIdConnectConstants.ResponseTypes.Token)) { - logger.LogError("The authorization request was rejected because the " + + Logger.LogError("The authorization request was rejected because the " + "specified response type was not compatible with PKCE."); context.Reject( @@ -258,10 +253,10 @@ namespace OpenIddict } // Retrieve the application details corresponding to the requested client_id. - var application = await applications.FindByClientIdAsync(context.ClientId, context.HttpContext.RequestAborted); + var application = await Applications.FindByClientIdAsync(context.ClientId, context.HttpContext.RequestAborted); if (application == null) { - logger.LogError("The authorization request was rejected because the client " + + Logger.LogError("The authorization request was rejected because the client " + "application was not found: '{ClientId}'.", context.ClientId); context.Reject( @@ -272,9 +267,9 @@ namespace OpenIddict } // Ensure a redirect_uri was associated with the application. - if (!await applications.HasRedirectUriAsync(application, context.HttpContext.RequestAborted)) + if (!await Applications.HasRedirectUriAsync(application, context.HttpContext.RequestAborted)) { - logger.LogError("The authorization request was rejected because no redirect_uri " + + Logger.LogError("The authorization request was rejected because no redirect_uri " + "was registered with the application '{ClientId}'.", context.ClientId); context.Reject( @@ -285,9 +280,9 @@ namespace OpenIddict } // Ensure the redirect_uri is valid. - if (!await applications.ValidateRedirectUriAsync(application, context.RedirectUri, context.HttpContext.RequestAborted)) + if (!await Applications.ValidateRedirectUriAsync(application, context.RedirectUri, context.HttpContext.RequestAborted)) { - logger.LogError("The authorization request was rejected because the redirect_uri " + + Logger.LogError("The authorization request was rejected because the redirect_uri " + "was invalid: '{RedirectUri}'.", context.RedirectUri); context.Reject( @@ -301,7 +296,7 @@ namespace OpenIddict // from the authorization endpoint are rejected if the client_id corresponds to a confidential application. // 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 applications.IsConfidentialAsync(application, context.HttpContext.RequestAborted) && + if (await Applications.IsConfidentialAsync(application, context.HttpContext.RequestAborted) && context.Request.HasResponseType(OpenIdConnectConstants.ResponseTypes.Token)) { context.Reject( @@ -317,12 +312,12 @@ namespace OpenIddict public override async Task HandleAuthorizationRequest([NotNull] HandleAuthorizationRequestContext context) { - var options = context.HttpContext.RequestServices.GetRequiredService>(); + var options = (OpenIddictOptions) context.Options; // If no request_id parameter can be found in the current request, assume the OpenID Connect request // was not serialized yet and store the entire payload in the distributed cache to make it easier // to flow across requests and internal/external authentication/registration workflows. - if (options.Value.EnableRequestCaching && string.IsNullOrEmpty(context.Request.RequestId)) + if (options.EnableRequestCaching && string.IsNullOrEmpty(context.Request.RequestId)) { // Generate a request identifier. Note: using a crypto-secure // random number generator is not necessary in this case. @@ -330,7 +325,7 @@ namespace OpenIddict // Store the serialized authorization request parameters in the distributed cache. var stream = new MemoryStream(); - using (var writer = new BsonWriter(stream)) + using (var writer = new BsonDataWriter(stream)) { writer.CloseOutput = false; @@ -342,7 +337,7 @@ namespace OpenIddict // to avoid collisions with the other types of cached requests. var key = OpenIddictConstants.Environment.AuthorizationRequest + context.Request.RequestId; - await options.Value.Cache.SetAsync(key, stream.ToArray(), new DistributedCacheEntryOptions + await options.Cache.SetAsync(key, stream.ToArray(), new DistributedCacheEntryOptions { AbsoluteExpiration = context.Options.SystemClock.UtcNow + TimeSpan.FromMinutes(30), SlidingExpiration = TimeSpan.FromMinutes(10) @@ -363,15 +358,15 @@ namespace OpenIddict return; } - context.SkipToNextMiddleware(); + context.SkipHandler(); } public override async Task ApplyAuthorizationResponse([NotNull] ApplyAuthorizationResponseContext context) { - var options = context.HttpContext.RequestServices.GetRequiredService>(); + var options = (OpenIddictOptions) context.Options; // Remove the authorization request from the distributed cache. - if (options.Value.EnableRequestCaching && !string.IsNullOrEmpty(context.Request.RequestId)) + if (options.EnableRequestCaching && !string.IsNullOrEmpty(context.Request.RequestId)) { // Note: the cache key is always prefixed with a specific marker // to avoid collisions with the other types of cached requests. @@ -380,11 +375,11 @@ namespace OpenIddict // Note: the ApplyAuthorizationResponse event is called for both successful // and errored authorization responses but discrimination is not necessary here, // as the authorization request must be removed from the distributed cache in both cases. - await options.Value.Cache.RemoveAsync(key); + await options.Cache.RemoveAsync(key); } - if (!options.Value.ApplicationCanDisplayErrors && !string.IsNullOrEmpty(context.Error) && - string.IsNullOrEmpty(context.RedirectUri)) + if (!options.ApplicationCanDisplayErrors && !string.IsNullOrEmpty(context.Error) && + string.IsNullOrEmpty(context.RedirectUri)) { // Determine if the status code pages middleware has been enabled for this request. // If it was not registered or enabled, let the OpenID Connect server middleware render diff --git a/src/OpenIddict/OpenIddictProvider.Discovery.cs b/src/OpenIddict/OpenIddictProvider.Discovery.cs index 7e04869c..af037899 100644 --- a/src/OpenIddict/OpenIddictProvider.Discovery.cs +++ b/src/OpenIddict/OpenIddictProvider.Discovery.cs @@ -9,8 +9,8 @@ using System.Threading.Tasks; using AspNet.Security.OpenIdConnect.Primitives; using AspNet.Security.OpenIdConnect.Server; using JetBrains.Annotations; +using Microsoft.AspNetCore.Authentication; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Options; using Newtonsoft.Json.Linq; using OpenIddict.Core; @@ -19,9 +19,9 @@ namespace OpenIddict public partial class OpenIddictProvider : OpenIdConnectServerProvider where TApplication : class where TAuthorization : class where TScope : class where TToken : class { - public override Task HandleConfigurationRequest([NotNull] HandleConfigurationRequestContext context) + public override async Task HandleConfigurationRequest([NotNull] HandleConfigurationRequestContext context) { - var options = context.HttpContext.RequestServices.GetRequiredService>(); + var options = (OpenIddictOptions) context.Options; // Note: though it's natively supported by the OpenID Connect server middleware, // OpenIddict disallows the use of the unsecure code_challenge_method=plain method, @@ -32,7 +32,7 @@ namespace OpenIddict // Note: the OpenID Connect server middleware automatically populates grant_types_supported // by determining whether the authorization and token endpoints are enabled or not but // OpenIddict uses a different approach and relies on a configurable "grants list". - context.GrantTypes.IntersectWith(options.Value.GrantTypes); + context.GrantTypes.IntersectWith(options.GrantTypes); // Note: the "openid" scope is automatically // added by the OpenID Connect server middleware. @@ -47,12 +47,12 @@ namespace OpenIddict context.Scopes.Add(OpenIdConnectConstants.Scopes.OfflineAccess); } + var schemes = context.HttpContext.RequestServices.GetRequiredService(); + context.Metadata[OpenIddictConstants.Metadata.ExternalProvidersSupported] = new JArray( - from provider in context.HttpContext.Authentication.GetAuthenticationSchemes() + from provider in await schemes.GetAllSchemesAsync() where !string.IsNullOrEmpty(provider.DisplayName) - select provider.AuthenticationScheme); - - return Task.FromResult(0); + select provider.Name); } } } \ No newline at end of file diff --git a/src/OpenIddict/OpenIddictProvider.Exchange.cs b/src/OpenIddict/OpenIddictProvider.Exchange.cs index ee95f42a..a26d1816 100644 --- a/src/OpenIddict/OpenIddictProvider.Exchange.cs +++ b/src/OpenIddict/OpenIddictProvider.Exchange.cs @@ -10,10 +10,7 @@ using AspNet.Security.OpenIdConnect.Extensions; using AspNet.Security.OpenIdConnect.Primitives; using AspNet.Security.OpenIdConnect.Server; using JetBrains.Annotations; -using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using OpenIddict.Core; namespace OpenIddict { @@ -22,14 +19,12 @@ namespace OpenIddict { public override async Task ValidateTokenRequest([NotNull] ValidateTokenRequestContext context) { - var applications = context.HttpContext.RequestServices.GetRequiredService>(); - var logger = context.HttpContext.RequestServices.GetRequiredService>>(); - var options = context.HttpContext.RequestServices.GetRequiredService>(); + var options = (OpenIddictOptions) context.Options; // Reject token requests that don't specify a supported grant type. - if (!options.Value.GrantTypes.Contains(context.Request.GrantType)) + if (!options.GrantTypes.Contains(context.Request.GrantType)) { - logger.LogError("The token request was rejected because the '{Grant}' " + + Logger.LogError("The token request was rejected because the '{Grant}' " + "grant is not supported.", context.Request.GrantType); context.Reject( @@ -41,7 +36,7 @@ namespace OpenIddict // Reject token requests that specify scope=offline_access if the refresh token flow is not enabled. if (context.Request.HasScope(OpenIdConnectConstants.Scopes.OfflineAccess) && - !options.Value.GrantTypes.Contains(OpenIdConnectConstants.GrantTypes.RefreshToken)) + !options.GrantTypes.Contains(OpenIdConnectConstants.GrantTypes.RefreshToken)) { context.Reject( error: OpenIdConnectConstants.Errors.InvalidRequest, @@ -100,7 +95,7 @@ namespace OpenIddict if (string.IsNullOrEmpty(context.ClientId)) { // Reject the request if client identification is mandatory. - if (options.Value.RequireClientIdentification) + if (options.RequireClientIdentification) { context.Reject( error: OpenIdConnectConstants.Errors.InvalidRequest, @@ -109,7 +104,7 @@ namespace OpenIddict return; } - logger.LogDebug("The token request validation process was partially skipped " + + Logger.LogDebug("The token request validation process was partially skipped " + "because the 'client_id' parameter was missing or empty."); context.Skip(); @@ -118,10 +113,10 @@ namespace OpenIddict } // Retrieve the application details corresponding to the requested client_id. - var application = await applications.FindByClientIdAsync(context.ClientId, context.HttpContext.RequestAborted); + var application = await Applications.FindByClientIdAsync(context.ClientId, context.HttpContext.RequestAborted); if (application == null) { - logger.LogError("The token request was rejected because the client " + + Logger.LogError("The token request was rejected because the client " + "application was not found: '{ClientId}'.", context.ClientId); context.Reject( @@ -131,12 +126,12 @@ namespace OpenIddict return; } - if (await applications.IsPublicAsync(application, context.HttpContext.RequestAborted)) + if (await Applications.IsPublicAsync(application, context.HttpContext.RequestAborted)) { // Note: public applications are not allowed to use the client credentials grant. if (context.Request.IsClientCredentialsGrantType()) { - logger.LogError("The token request was rejected because the public client application '{ClientId}' " + + Logger.LogError("The token request was rejected because the public client application '{ClientId}' " + "was not allowed to use the client credentials grant.", context.Request.ClientId); context.Reject( @@ -149,7 +144,7 @@ namespace OpenIddict // Reject tokens requests containing a client_secret when the client is a public application. if (!string.IsNullOrEmpty(context.ClientSecret)) { - logger.LogError("The token request was rejected because the public application '{ClientId}' " + + Logger.LogError("The token request was rejected because the public application '{ClientId}' " + "was not allowed to send a client secret.", context.ClientId); context.Reject( @@ -159,7 +154,7 @@ namespace OpenIddict return; } - logger.LogInformation("The token request validation process was not fully validated because " + + Logger.LogInformation("The token request validation process was not fully validated because " + "the client '{ClientId}' was a public application.", context.ClientId); // If client authentication cannot be enforced, call context.Skip() to inform @@ -173,7 +168,7 @@ namespace OpenIddict // to protect them from impersonation attacks. if (string.IsNullOrEmpty(context.ClientSecret)) { - logger.LogError("The token request was rejected because the confidential application " + + Logger.LogError("The token request was rejected because the confidential application " + "'{ClientId}' didn't specify a client secret.", context.ClientId); context.Reject( @@ -183,9 +178,9 @@ namespace OpenIddict return; } - if (!await applications.ValidateClientSecretAsync(application, context.ClientSecret, context.HttpContext.RequestAborted)) + if (!await Applications.ValidateClientSecretAsync(application, context.ClientSecret, context.HttpContext.RequestAborted)) { - logger.LogError("The token request was rejected because the confidential application " + + Logger.LogError("The token request was rejected because the confidential application " + "'{ClientId}' didn't specify valid client credentials.", context.ClientId); context.Reject( @@ -200,12 +195,10 @@ namespace OpenIddict public override async Task HandleTokenRequest([NotNull] HandleTokenRequestContext context) { - var options = context.HttpContext.RequestServices.GetRequiredService>(); - var logger = context.HttpContext.RequestServices.GetRequiredService>>(); - var tokens = context.HttpContext.RequestServices.GetRequiredService>(); + var options = (OpenIddictOptions) context.Options; - if (!options.Value.DisableTokenRevocation && (context.Request.IsAuthorizationCodeGrantType() || - context.Request.IsRefreshTokenGrantType())) + if (!options.DisableTokenRevocation && (context.Request.IsAuthorizationCodeGrantType() || + context.Request.IsRefreshTokenGrantType())) { Debug.Assert(context.Ticket != null, "The authentication ticket shouldn't be null."); @@ -216,10 +209,10 @@ namespace OpenIddict if (context.Request.IsAuthorizationCodeGrantType()) { // Retrieve the token from the database and ensure it is still valid. - var token = await tokens.FindByIdAsync(identifier, context.HttpContext.RequestAborted); + var token = await Tokens.FindByIdAsync(identifier, context.HttpContext.RequestAborted); if (token == null) { - logger.LogError("The token request was rejected because the authorization code was revoked."); + Logger.LogError("The token request was rejected because the authorization code was revoked."); context.Reject( error: OpenIdConnectConstants.Errors.InvalidGrant, @@ -229,16 +222,16 @@ namespace OpenIddict } // Revoke the authorization code to prevent token reuse. - await tokens.RevokeAsync(token, context.HttpContext.RequestAborted); + await Tokens.RevokeAsync(token, context.HttpContext.RequestAborted); } else if (context.Request.IsRefreshTokenGrantType()) { // Retrieve the token from the database and ensure it is still valid. - var token = await tokens.FindByIdAsync(identifier, context.HttpContext.RequestAborted); + var token = await Tokens.FindByIdAsync(identifier, context.HttpContext.RequestAborted); if (token == null) { - logger.LogError("The token request was rejected because the refresh token was revoked."); + Logger.LogError("The token request was rejected because the refresh token was revoked."); context.Reject( error: OpenIdConnectConstants.Errors.InvalidGrant, @@ -252,14 +245,14 @@ namespace OpenIddict // See https://tools.ietf.org/html/rfc6749#section-6. if (context.Options.UseSlidingExpiration) { - await tokens.RevokeAsync(token, context.HttpContext.RequestAborted); + await Tokens.RevokeAsync(token, context.HttpContext.RequestAborted); } } } // Invoke the rest of the pipeline to allow // the user code to handle the token request. - context.SkipToNextMiddleware(); + context.SkipHandler(); } } } \ No newline at end of file diff --git a/src/OpenIddict/OpenIddictProvider.Introspection.cs b/src/OpenIddict/OpenIddictProvider.Introspection.cs index 46472dca..1350f2c4 100644 --- a/src/OpenIddict/OpenIddictProvider.Introspection.cs +++ b/src/OpenIddict/OpenIddictProvider.Introspection.cs @@ -11,10 +11,7 @@ using AspNet.Security.OpenIdConnect.Extensions; using AspNet.Security.OpenIdConnect.Primitives; using AspNet.Security.OpenIdConnect.Server; using JetBrains.Annotations; -using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using OpenIddict.Core; namespace OpenIddict { @@ -39,9 +36,6 @@ namespace OpenIddict public override async Task ValidateIntrospectionRequest([NotNull] ValidateIntrospectionRequestContext context) { - var applications = context.HttpContext.RequestServices.GetRequiredService>(); - var logger = context.HttpContext.RequestServices.GetRequiredService>>(); - // Note: the OpenID Connect server middleware supports unauthenticated introspection requests // but OpenIddict uses a stricter policy preventing unauthenticated/public applications // from using the introspection endpoint, as required by the specifications. @@ -56,10 +50,10 @@ namespace OpenIddict } // Retrieve the application details corresponding to the requested client_id. - var application = await applications.FindByClientIdAsync(context.ClientId, context.HttpContext.RequestAborted); + var application = await Applications.FindByClientIdAsync(context.ClientId, context.HttpContext.RequestAborted); if (application == null) { - logger.LogError("The introspection request was rejected because the client " + + Logger.LogError("The introspection request was rejected because the client " + "application was not found: '{ClientId}'.", context.ClientId); context.Reject( @@ -70,9 +64,9 @@ namespace OpenIddict } // Reject non-confidential applications. - if (!await applications.IsConfidentialAsync(application, context.HttpContext.RequestAborted)) + if (!await Applications.IsConfidentialAsync(application, context.HttpContext.RequestAborted)) { - logger.LogError("The introspection request was rejected because the public application " + + Logger.LogError("The introspection request was rejected because the public application " + "'{ClientId}' was not allowed to use this endpoint.", context.ClientId); context.Reject( @@ -83,9 +77,9 @@ namespace OpenIddict } // Validate the client credentials. - if (!await applications.ValidateClientSecretAsync(application, context.ClientSecret, context.HttpContext.RequestAborted)) + if (!await Applications.ValidateClientSecretAsync(application, context.ClientSecret, context.HttpContext.RequestAborted)) { - logger.LogError("The introspection request was rejected because the confidential application " + + Logger.LogError("The introspection request was rejected because the confidential application " + "'{ClientId}' didn't specify valid client credentials.", context.ClientId); context.Reject( @@ -100,9 +94,7 @@ namespace OpenIddict public override async Task HandleIntrospectionRequest([NotNull] HandleIntrospectionRequestContext context) { - var options = context.HttpContext.RequestServices.GetRequiredService>(); - var logger = context.HttpContext.RequestServices.GetRequiredService>>(); - var tokens = context.HttpContext.RequestServices.GetRequiredService>(); + var options = (OpenIddictOptions) context.Options; Debug.Assert(context.Ticket != null, "The authentication ticket shouldn't be null."); Debug.Assert(!string.IsNullOrEmpty(context.Request.ClientId), "The client_id parameter shouldn't be null."); @@ -115,7 +107,7 @@ namespace OpenIddict // doesn't have any audience: in this case, the caller is allowed to introspect the token even if it's not listed as a valid audience. if (context.Ticket.IsAccessToken() && context.Ticket.HasAudience() && !context.Ticket.HasAudience(context.Request.ClientId)) { - logger.LogWarning("The client application '{ClientId}' is not allowed to introspect the access " + + Logger.LogWarning("The client application '{ClientId}' is not allowed to introspect the access " + "token '{Identifier}' because it's not listed as a valid audience.", context.Request.ClientId, identifier); @@ -125,14 +117,14 @@ namespace OpenIddict } // When the received ticket is revocable, ensure it is still valid. - if (!options.Value.DisableTokenRevocation && (context.Ticket.IsAuthorizationCode() || context.Ticket.IsRefreshToken())) + if (!options.DisableTokenRevocation && (context.Ticket.IsAuthorizationCode() || context.Ticket.IsRefreshToken())) { // Retrieve the token from the database using the unique identifier stored in the authentication ticket: // if the corresponding entry cannot be found, return Active = false to indicate that is is no longer valid. - var token = await tokens.FindByIdAsync(identifier, context.HttpContext.RequestAborted); + var token = await Tokens.FindByIdAsync(identifier, context.HttpContext.RequestAborted); if (token == null) { - logger.LogInformation("The token {Identifier} was declared as inactive because " + + Logger.LogInformation("The token {Identifier} was declared as inactive because " + "it was revoked.", identifier); context.Active = false; diff --git a/src/OpenIddict/OpenIddictProvider.Revocation.cs b/src/OpenIddict/OpenIddictProvider.Revocation.cs index afb4e3ae..49d0c392 100644 --- a/src/OpenIddict/OpenIddictProvider.Revocation.cs +++ b/src/OpenIddict/OpenIddictProvider.Revocation.cs @@ -10,10 +10,7 @@ using AspNet.Security.OpenIdConnect.Extensions; using AspNet.Security.OpenIdConnect.Primitives; using AspNet.Security.OpenIdConnect.Server; using JetBrains.Annotations; -using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using OpenIddict.Core; namespace OpenIddict { @@ -22,11 +19,9 @@ namespace OpenIddict { public override async Task ValidateRevocationRequest([NotNull] ValidateRevocationRequestContext context) { - var applications = context.HttpContext.RequestServices.GetRequiredService>(); - var logger = context.HttpContext.RequestServices.GetRequiredService>>(); - var options = context.HttpContext.RequestServices.GetRequiredService>(); + var options = (OpenIddictOptions) context.Options; - Debug.Assert(!options.Value.DisableTokenRevocation, "Token revocation support shouldn't be disabled at this stage."); + Debug.Assert(!options.DisableTokenRevocation, "Token revocation support shouldn't be disabled at this stage."); // When token_type_hint is specified, reject the request if it doesn't correspond to a revocable token. if (!string.IsNullOrEmpty(context.Request.TokenTypeHint) && @@ -49,9 +44,9 @@ namespace OpenIddict if (string.IsNullOrEmpty(context.ClientId)) { // Reject the request if client identification is mandatory. - if (options.Value.RequireClientIdentification) + if (options.RequireClientIdentification) { - logger.LogError("The revocation request was rejected becaused the " + + Logger.LogError("The revocation request was rejected becaused the " + "mandatory client_id parameter was missing or empty."); context.Reject( @@ -61,7 +56,7 @@ namespace OpenIddict return; } - logger.LogInformation("The revocation request validation process was skipped " + + Logger.LogInformation("The revocation request validation process was skipped " + "because the client_id parameter was missing or empty."); context.Skip(); @@ -70,7 +65,7 @@ namespace OpenIddict } // Retrieve the application details corresponding to the requested client_id. - var application = await applications.FindByClientIdAsync(context.ClientId, context.HttpContext.RequestAborted); + var application = await Applications.FindByClientIdAsync(context.ClientId, context.HttpContext.RequestAborted); if (application == null) { context.Reject( @@ -81,7 +76,7 @@ namespace OpenIddict } // Reject revocation requests containing a client_secret if the client application is not confidential. - if (await applications.IsPublicAsync(application, context.HttpContext.RequestAborted)) + if (await Applications.IsPublicAsync(application, context.HttpContext.RequestAborted)) { // Reject tokens requests containing a client_secret when the client is a public application. if (!string.IsNullOrEmpty(context.ClientSecret)) @@ -93,7 +88,7 @@ namespace OpenIddict return; } - logger.LogInformation("The revocation request validation process was not fully validated because " + + Logger.LogInformation("The revocation request validation process was not fully validated because " + "the client '{ClientId}' was a public application.", context.ClientId); // If client authentication cannot be enforced, call context.Skip() to inform @@ -114,7 +109,7 @@ namespace OpenIddict return; } - if (!await applications.ValidateClientSecretAsync(application, context.ClientSecret, context.HttpContext.RequestAborted)) + if (!await Applications.ValidateClientSecretAsync(application, context.ClientSecret, context.HttpContext.RequestAborted)) { context.Reject( error: OpenIdConnectConstants.Errors.InvalidClient, @@ -128,16 +123,13 @@ namespace OpenIddict public override async Task HandleRevocationRequest([NotNull] HandleRevocationRequestContext context) { - var logger = context.HttpContext.RequestServices.GetRequiredService>>(); - var tokens = context.HttpContext.RequestServices.GetRequiredService>(); - Debug.Assert(context.Ticket != null, "The authentication ticket shouldn't be null."); // If the received token is not an authorization code or a refresh token, // return an error to indicate that the token cannot be revoked. if (!context.Ticket.IsAuthorizationCode() && !context.Ticket.IsRefreshToken()) { - logger.LogError("The revocation request was rejected because the token was not revocable."); + Logger.LogError("The revocation request was rejected because the token was not revocable."); context.Reject( error: OpenIdConnectConstants.Errors.UnsupportedTokenType, @@ -152,10 +144,10 @@ namespace OpenIddict // 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 tokens.FindByIdAsync(identifier, context.HttpContext.RequestAborted); + var token = await Tokens.FindByIdAsync(identifier, context.HttpContext.RequestAborted); if (token == null) { - logger.LogInformation("The token '{Identifier}' was already revoked.", identifier); + Logger.LogInformation("The token '{Identifier}' was already revoked.", identifier); context.Revoked = true; @@ -163,9 +155,9 @@ namespace OpenIddict } // Revoke the token. - await tokens.RevokeAsync(token, context.HttpContext.RequestAborted); + await Tokens.RevokeAsync(token, context.HttpContext.RequestAborted); - logger.LogInformation("The token '{Identifier}' was successfully revoked.", identifier); + Logger.LogInformation("The token '{Identifier}' was successfully revoked.", identifier); context.Revoked = true; } diff --git a/src/OpenIddict/OpenIddictProvider.Serialization.cs b/src/OpenIddict/OpenIddictProvider.Serialization.cs index da3bcd00..454f68d7 100644 --- a/src/OpenIddict/OpenIddictProvider.Serialization.cs +++ b/src/OpenIddict/OpenIddictProvider.Serialization.cs @@ -11,8 +11,6 @@ using AspNet.Security.OpenIdConnect.Extensions; using AspNet.Security.OpenIdConnect.Primitives; using AspNet.Security.OpenIdConnect.Server; using JetBrains.Annotations; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Options; using OpenIddict.Core; namespace OpenIddict @@ -22,13 +20,11 @@ namespace OpenIddict { public override async Task SerializeAuthorizationCode([NotNull] SerializeAuthorizationCodeContext context) { - var applications = context.HttpContext.RequestServices.GetRequiredService>(); - var options = context.HttpContext.RequestServices.GetRequiredService>(); - var tokens = context.HttpContext.RequestServices.GetRequiredService>(); + var options = (OpenIddictOptions) context.Options; Debug.Assert(!string.IsNullOrEmpty(context.Request.ClientId), "The client identifier shouldn't be null or empty."); - if (!options.Value.DisableTokenRevocation) + if (!options.DisableTokenRevocation) { // Resolve the subject from the authentication ticket. If it cannot be found, throw an exception. var subject = context.Ticket.Principal.GetClaim(OpenIdConnectConstants.Claims.Subject); @@ -38,14 +34,14 @@ namespace OpenIddict } // If a null value was returned by CreateAsync, return immediately. - var token = await tokens.CreateAsync(OpenIdConnectConstants.TokenTypeHints.AuthorizationCode, subject, context.HttpContext.RequestAborted); + var token = await Tokens.CreateAsync(OpenIdConnectConstants.TokenTypeHints.AuthorizationCode, subject, context.HttpContext.RequestAborted); if (token == null) { return; } // Throw an exception if the token identifier can't be resolved. - var identifier = await tokens.GetIdAsync(token, context.HttpContext.RequestAborted); + var identifier = await Tokens.GetIdAsync(token, context.HttpContext.RequestAborted); if (string.IsNullOrEmpty(identifier)) { throw new InvalidOperationException("The unique key associated with an authorization code cannot be null or empty."); @@ -56,30 +52,28 @@ namespace OpenIddict // generated by the OpenID Connect server middleware. context.Ticket.SetProperty(OpenIdConnectConstants.Properties.TokenId, identifier); - var application = await applications.FindByClientIdAsync(context.Request.ClientId, context.HttpContext.RequestAborted); + var application = await Applications.FindByClientIdAsync(context.Request.ClientId, context.HttpContext.RequestAborted); if (application == null) { throw new InvalidOperationException("The client application cannot be retrieved from the database."); } - await tokens.SetClientAsync(token, await applications.GetIdAsync(application, context.HttpContext.RequestAborted), context.HttpContext.RequestAborted); + await Tokens.SetClientAsync(token, await Applications.GetIdAsync(application, context.HttpContext.RequestAborted), context.HttpContext.RequestAborted); // If an authorization identifier was specified, bind it to the token. var authorization = context.Ticket.GetProperty(OpenIddictConstants.Properties.AuthorizationId); if (!string.IsNullOrEmpty(authorization)) { - await tokens.SetAuthorizationAsync(token, authorization, context.HttpContext.RequestAborted); + await Tokens.SetAuthorizationAsync(token, authorization, context.HttpContext.RequestAborted); } } } public override async Task SerializeRefreshToken([NotNull] SerializeRefreshTokenContext context) { - var applications = context.HttpContext.RequestServices.GetRequiredService>(); - var options = context.HttpContext.RequestServices.GetRequiredService>(); - var tokens = context.HttpContext.RequestServices.GetRequiredService>(); + var options = (OpenIddictOptions) context.Options; - if (!options.Value.DisableTokenRevocation) + if (!options.DisableTokenRevocation) { // Resolve the subject from the authentication ticket. If it cannot be found, throw an exception. var subject = context.Ticket.Principal.GetClaim(OpenIdConnectConstants.Claims.Subject); @@ -89,14 +83,14 @@ namespace OpenIddict } // If a null value was returned by CreateAsync, return immediately. - var token = await tokens.CreateAsync(OpenIdConnectConstants.TokenTypeHints.RefreshToken, subject, context.HttpContext.RequestAborted); + var token = await Tokens.CreateAsync(OpenIdConnectConstants.TokenTypeHints.RefreshToken, subject, context.HttpContext.RequestAborted); if (token == null) { return; } // Throw an exception if the token identifier can't be resolved. - var identifier = await tokens.GetIdAsync(token, context.HttpContext.RequestAborted); + var identifier = await Tokens.GetIdAsync(token, context.HttpContext.RequestAborted); if (string.IsNullOrEmpty(identifier)) { throw new InvalidOperationException("The unique key associated with a refresh token cannot be null or empty."); @@ -110,20 +104,20 @@ namespace OpenIddict // If the client application is known, associate it with the token. if (!string.IsNullOrEmpty(context.Request.ClientId)) { - var application = await applications.FindByClientIdAsync(context.Request.ClientId, context.HttpContext.RequestAborted); + var application = await Applications.FindByClientIdAsync(context.Request.ClientId, context.HttpContext.RequestAborted); if (application == null) { throw new InvalidOperationException("The client application cannot be retrieved from the database."); } - await tokens.SetClientAsync(token, await applications.GetIdAsync(application, context.HttpContext.RequestAborted), context.HttpContext.RequestAborted); + await Tokens.SetClientAsync(token, await Applications.GetIdAsync(application, context.HttpContext.RequestAborted), context.HttpContext.RequestAborted); } // If an authorization identifier was specified, bind it to the token. var authorization = context.Ticket.GetProperty(OpenIddictConstants.Properties.AuthorizationId); if (!string.IsNullOrEmpty(authorization)) { - await tokens.SetAuthorizationAsync(token, authorization, context.HttpContext.RequestAborted); + await Tokens.SetAuthorizationAsync(token, authorization, context.HttpContext.RequestAborted); } } } diff --git a/src/OpenIddict/OpenIddictProvider.Session.cs b/src/OpenIddict/OpenIddictProvider.Session.cs index 0dfc66c1..e8e6c5f6 100644 --- a/src/OpenIddict/OpenIddictProvider.Session.cs +++ b/src/OpenIddict/OpenIddictProvider.Session.cs @@ -13,9 +13,7 @@ using JetBrains.Annotations; using Microsoft.AspNetCore.Diagnostics; using Microsoft.AspNetCore.WebUtilities; using Microsoft.Extensions.Caching.Distributed; -using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; using Newtonsoft.Json; using Newtonsoft.Json.Bson; using Newtonsoft.Json.Linq; @@ -28,17 +26,16 @@ namespace OpenIddict { public override async Task ExtractLogoutRequest([NotNull] ExtractLogoutRequestContext context) { - var logger = context.HttpContext.RequestServices.GetRequiredService>>(); - var options = context.HttpContext.RequestServices.GetRequiredService>(); + var options = (OpenIddictOptions) context.Options; // If a request_id parameter can be found in the logout request, // restore the complete logout request from the distributed cache. if (!string.IsNullOrEmpty(context.Request.RequestId)) { // Return an error if request caching support was not enabled. - if (!options.Value.EnableRequestCaching) + if (!options.EnableRequestCaching) { - logger.LogError("The logout request was rejected because " + + Logger.LogError("The logout request was rejected because " + "request caching support was not enabled."); context.Reject( @@ -52,10 +49,10 @@ namespace OpenIddict // to avoid collisions with the other types of cached requests. var key = OpenIddictConstants.Environment.LogoutRequest + context.Request.RequestId; - var payload = await options.Value.Cache.GetAsync(key); + var payload = await options.Cache.GetAsync(key); if (payload == null) { - logger.LogError("The logout request was rejected because an unknown " + + Logger.LogError("The logout request was rejected because an unknown " + "or invalid request_id parameter was specified."); context.Reject( @@ -66,7 +63,7 @@ namespace OpenIddict } // Restore the logout request parameters from the serialized payload. - using (var reader = new BsonReader(new MemoryStream(payload))) + using (var reader = new BsonDataReader(new MemoryStream(payload))) { foreach (var parameter in JObject.Load(reader)) { @@ -84,16 +81,13 @@ namespace OpenIddict public override async Task ValidateLogoutRequest([NotNull] ValidateLogoutRequestContext context) { - var applications = context.HttpContext.RequestServices.GetRequiredService>(); - var logger = context.HttpContext.RequestServices.GetRequiredService>>(); - // If an optional post_logout_redirect_uri was provided, validate it. if (!string.IsNullOrEmpty(context.PostLogoutRedirectUri)) { - var application = await applications.FindByLogoutRedirectUri(context.PostLogoutRedirectUri, context.HttpContext.RequestAborted); + var application = await Applications.FindByLogoutRedirectUri(context.PostLogoutRedirectUri, context.HttpContext.RequestAborted); if (application == null) { - logger.LogError("The logout request was rejected because the client application corresponding " + + Logger.LogError("The logout request was rejected because the client application corresponding " + "to the specified post_logout_redirect_uri was not found in the database: " + "'{PostLogoutRedirectUri}'.", context.PostLogoutRedirectUri); @@ -110,12 +104,12 @@ namespace OpenIddict public override async Task HandleLogoutRequest([NotNull] HandleLogoutRequestContext context) { - var options = context.HttpContext.RequestServices.GetRequiredService>(); + var options = (OpenIddictOptions) context.Options; // If no request_id parameter can be found in the current request, assume the OpenID Connect // request was not serialized yet and store the entire payload in the distributed cache // to make it easier to flow across requests and internal/external logout workflows. - if (options.Value.EnableRequestCaching && string.IsNullOrEmpty(context.Request.RequestId)) + if (options.EnableRequestCaching && string.IsNullOrEmpty(context.Request.RequestId)) { // Generate a request identifier. Note: using a crypto-secure // random number generator is not necessary in this case. @@ -123,7 +117,7 @@ namespace OpenIddict // Store the serialized logout request parameters in the distributed cache. var stream = new MemoryStream(); - using (var writer = new BsonWriter(stream)) + using (var writer = new BsonDataWriter(stream)) { writer.CloseOutput = false; @@ -135,7 +129,7 @@ namespace OpenIddict // to avoid collisions with the other types of cached requests. var key = OpenIddictConstants.Environment.LogoutRequest + context.Request.RequestId; - await options.Value.Cache.SetAsync(key, stream.ToArray(), new DistributedCacheEntryOptions + await options.Cache.SetAsync(key, stream.ToArray(), new DistributedCacheEntryOptions { AbsoluteExpiration = context.Options.SystemClock.UtcNow + TimeSpan.FromMinutes(30), SlidingExpiration = TimeSpan.FromMinutes(10) @@ -159,10 +153,10 @@ namespace OpenIddict public override async Task ApplyLogoutResponse([NotNull] ApplyLogoutResponseContext context) { - var options = context.HttpContext.RequestServices.GetRequiredService>(); + var options = (OpenIddictOptions) context.Options; // Remove the logout request from the distributed cache. - if (options.Value.EnableRequestCaching && !string.IsNullOrEmpty(context.Request.RequestId)) + if (options.EnableRequestCaching && !string.IsNullOrEmpty(context.Request.RequestId)) { // Note: the cache key is always prefixed with a specific marker // to avoid collisions with the other types of cached requests. @@ -171,11 +165,11 @@ namespace OpenIddict // Note: the ApplyLogoutResponse event is called for both successful // and errored logout responses but discrimination is not necessary here, // as the logout request must be removed from the distributed cache in both cases. - await options.Value.Cache.RemoveAsync(key); + await options.Cache.RemoveAsync(key); } - if (!options.Value.ApplicationCanDisplayErrors && !string.IsNullOrEmpty(context.Error) && - string.IsNullOrEmpty(context.PostLogoutRedirectUri)) + if (!options.ApplicationCanDisplayErrors && !string.IsNullOrEmpty(context.Error) && + string.IsNullOrEmpty(context.PostLogoutRedirectUri)) { // Determine if the status code pages middleware has been enabled for this request. // If it was not registered or enabled, let the OpenID Connect server middleware render diff --git a/src/OpenIddict/OpenIddictProvider.Userinfo.cs b/src/OpenIddict/OpenIddictProvider.Userinfo.cs index 214cf1b7..f6a8a72b 100644 --- a/src/OpenIddict/OpenIddictProvider.Userinfo.cs +++ b/src/OpenIddict/OpenIddictProvider.Userinfo.cs @@ -23,7 +23,7 @@ namespace OpenIddict // Invoke the rest of the pipeline to allow // the user code to handle the userinfo request. - context.SkipToNextMiddleware(); + context.SkipHandler(); return Task.FromResult(0); } diff --git a/src/OpenIddict/OpenIddictProvider.cs b/src/OpenIddict/OpenIddictProvider.cs index c6b3a336..7e942429 100644 --- a/src/OpenIddict/OpenIddictProvider.cs +++ b/src/OpenIddict/OpenIddictProvider.cs @@ -6,13 +6,52 @@ using System.ComponentModel; using AspNet.Security.OpenIdConnect.Server; +using JetBrains.Annotations; +using Microsoft.Extensions.Logging; +using OpenIddict.Core; namespace OpenIddict { + /// + /// Provides the logic necessary to extract, validate and handle OpenID Connect requests. + /// [EditorBrowsable(EditorBrowsableState.Never)] public partial class OpenIddictProvider : OpenIdConnectServerProvider where TApplication : class where TAuthorization : class where TScope : class where TToken : class { - // Note: this class is split into specialized partial classes. + /// + /// Creates a new instance of the class. + /// + public OpenIddictProvider( + [NotNull] ILogger> logger, + [NotNull] OpenIddictApplicationManager applications, + [NotNull] OpenIddictAuthorizationManager authorizations, + [NotNull] OpenIddictTokenManager tokens) + { + Applications = applications; + Authorizations = authorizations; + Logger = logger; + Tokens = tokens; + } + + /// + /// Gets the applications manager. + /// + public OpenIddictApplicationManager Applications { get; } + + /// + /// Gets the authorizations manager. + /// + public OpenIddictAuthorizationManager Authorizations { get; } + + /// + /// Gets the logger associated with the current class. + /// + public ILogger Logger { get; } + + /// + /// Gets the tokens manager. + /// + public OpenIddictTokenManager Tokens { get; } } } \ No newline at end of file diff --git a/test/OpenIddict.Core.Tests/OpenIddict.Core.Tests.csproj b/test/OpenIddict.Core.Tests/OpenIddict.Core.Tests.csproj index b1190b84..4034e462 100644 --- a/test/OpenIddict.Core.Tests/OpenIddict.Core.Tests.csproj +++ b/test/OpenIddict.Core.Tests/OpenIddict.Core.Tests.csproj @@ -3,8 +3,8 @@ - netcoreapp1.0;net452 - netcoreapp1.0 + netcoreapp2.0;net461 + netcoreapp2.0 diff --git a/test/OpenIddict.EntityFrameworkCore.Tests/OpenIddict.EntityFrameworkCore.Tests.csproj b/test/OpenIddict.EntityFrameworkCore.Tests/OpenIddict.EntityFrameworkCore.Tests.csproj index d7e11a3b..a86dfab0 100644 --- a/test/OpenIddict.EntityFrameworkCore.Tests/OpenIddict.EntityFrameworkCore.Tests.csproj +++ b/test/OpenIddict.EntityFrameworkCore.Tests/OpenIddict.EntityFrameworkCore.Tests.csproj @@ -3,8 +3,8 @@ - netcoreapp1.0;net452 - netcoreapp1.0 + netcoreapp2.0;net461 + netcoreapp2.0 diff --git a/test/OpenIddict.Mvc.Tests/OpenIddict.Mvc.Tests.csproj b/test/OpenIddict.Mvc.Tests/OpenIddict.Mvc.Tests.csproj index 616e6347..20d98d8c 100644 --- a/test/OpenIddict.Mvc.Tests/OpenIddict.Mvc.Tests.csproj +++ b/test/OpenIddict.Mvc.Tests/OpenIddict.Mvc.Tests.csproj @@ -3,8 +3,8 @@ - netcoreapp1.0;net452 - netcoreapp1.0 + netcoreapp2.0;net461 + netcoreapp2.0 diff --git a/test/OpenIddict.Tests/OpenIddict.Tests.csproj b/test/OpenIddict.Tests/OpenIddict.Tests.csproj index df06533e..4f54702f 100644 --- a/test/OpenIddict.Tests/OpenIddict.Tests.csproj +++ b/test/OpenIddict.Tests/OpenIddict.Tests.csproj @@ -3,8 +3,8 @@ - netcoreapp1.0;net452 - netcoreapp1.0 + netcoreapp2.0;net461 + netcoreapp2.0 @@ -34,7 +34,7 @@ - + $(DefineConstants);SUPPORTS_ECDSA diff --git a/test/OpenIddict.Tests/OpenIddictExtensionsTests.cs b/test/OpenIddict.Tests/OpenIddictExtensionsTests.cs index f949f52f..109b12df 100644 --- a/test/OpenIddict.Tests/OpenIddictExtensionsTests.cs +++ b/test/OpenIddict.Tests/OpenIddictExtensionsTests.cs @@ -2,11 +2,15 @@ using System.IdentityModel.Tokens.Jwt; using System.Reflection; using AspNet.Security.OpenIdConnect.Primitives; +using AspNet.Security.OpenIdConnect.Server; using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Builder.Internal; using Microsoft.AspNetCore.DataProtection; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Hosting.Internal; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Microsoft.IdentityModel.Tokens; using Moq; @@ -16,199 +20,36 @@ namespace OpenIddict.Tests { public class OpenIddictExtensionsTests { - [Fact] - public void UseOpenIddict_ThrowsAnExceptionWhenServicesAreNotRegistered() - { - // Arrange - var services = new ServiceCollection(); - var builder = new ApplicationBuilder(services.BuildServiceProvider()); - - // Act and assert - var exception = Assert.Throws(() => builder.UseOpenIddict()); - - Assert.Equal("The OpenIddict services cannot be resolved from the dependency injection container. " + - "Make sure 'services.AddOpenIddict()' is correctly called from 'ConfigureServices()'.", exception.Message); - } - - [Fact] - public void UseOpenIddict_ThrowsAnExceptionWhenNoDistributedCacheIsRegisteredIfRequestCachingIsEnabled() - { - // Arrange - var services = new ServiceCollection(); - - services.AddOpenIddict() - .EnableRequestCaching(); - - var builder = new ApplicationBuilder(services.BuildServiceProvider()); - - // Act and assert - var exception = Assert.Throws(() => builder.UseOpenIddict()); - - Assert.Equal("A distributed cache implementation must be registered in the OpenIddict options " + - "or in the dependency injection container when enabling request caching support.", exception.Message); - } - - [Fact] - public void UseOpenIddict_ThrowsAnExceptionWhenNoFlowIsEnabled() - { - // Arrange - var services = new ServiceCollection(); - services.AddOpenIddict(); - - var builder = new ApplicationBuilder(services.BuildServiceProvider()); - - // Act and assert - var exception = Assert.Throws(() => builder.UseOpenIddict()); - - Assert.Equal("At least one OAuth2/OpenID Connect flow must be enabled.", exception.Message); - } - - [Theory] - [InlineData(OpenIdConnectConstants.GrantTypes.AuthorizationCode)] - [InlineData(OpenIdConnectConstants.GrantTypes.Implicit)] - public void UseOpenIddict_ThrowsAnExceptionWhenAuthorizationEndpointIsDisabled(string flow) - { - // Arrange - var services = new ServiceCollection(); - - services.AddOpenIddict() - .Configure(options => options.GrantTypes.Add(flow)) - .Configure(options => options.AuthorizationEndpointPath = PathString.Empty); - - var builder = new ApplicationBuilder(services.BuildServiceProvider()); - - // Act and assert - var exception = Assert.Throws(() => builder.UseOpenIddict()); - - Assert.Equal("The authorization endpoint must be enabled to use " + - "the authorization code and implicit flows.", exception.Message); - } - - [Theory] - [InlineData(OpenIdConnectConstants.GrantTypes.AuthorizationCode)] - [InlineData(OpenIdConnectConstants.GrantTypes.ClientCredentials)] - [InlineData(OpenIdConnectConstants.GrantTypes.Password)] - [InlineData(OpenIdConnectConstants.GrantTypes.RefreshToken)] - public void UseOpenIddict_ThrowsAnExceptionWhenTokenEndpointIsDisabled(string flow) - { - // Arrange - var services = new ServiceCollection(); - - services.AddOpenIddict() - .EnableAuthorizationEndpoint("/connect/authorize") - .Configure(options => options.GrantTypes.Add(flow)) - .Configure(options => options.TokenEndpointPath = PathString.Empty); - - var builder = new ApplicationBuilder(services.BuildServiceProvider()); - - // Act and assert - var exception = Assert.Throws(() => builder.UseOpenIddict()); - - Assert.Equal("The token endpoint must be enabled to use the authorization code, " + - "client credentials, password and refresh token flows.", exception.Message); - } - - [Fact] - public void UseOpenIddict_ThrowsAnExceptionWhenTokenRevocationIsDisabled() - { - // Arrange - var services = new ServiceCollection(); - - services.AddOpenIddict() - .EnableAuthorizationEndpoint("/connect/authorize") - .EnableRevocationEndpoint("/connect/revocation") - .AllowImplicitFlow() - .DisableTokenRevocation(); - - var builder = new ApplicationBuilder(services.BuildServiceProvider()); - - // Act and assert - var exception = Assert.Throws(() => builder.UseOpenIddict()); - - Assert.Equal("The revocation endpoint cannot be enabled when token revocation is disabled.", exception.Message); - } - - [Fact] - public void UseOpenIddict_ThrowsAnExceptionWhenNoSigningKeyIsRegisteredIfTheImplicitFlowIsEnabled() - { - // Arrange - var services = new ServiceCollection(); - - services.AddOpenIddict() - .EnableAuthorizationEndpoint("/connect/authorize") - .AllowImplicitFlow(); - - var builder = new ApplicationBuilder(services.BuildServiceProvider()); - - // Act and assert - var exception = Assert.Throws(() => builder.UseOpenIddict()); - - Assert.Equal("At least one asymmetric signing key must be registered when enabling the implicit flow. " + - "Consider registering a X.509 certificate using 'services.AddOpenIddict().AddSigningCertificate()' " + - "or call 'services.AddOpenIddict().AddEphemeralSigningKey()' to use an ephemeral key.", exception.Message); - } - [Fact] public void Configure_OptionsAreCorrectlyAmended() { // Arrange - var services = new ServiceCollection(); - services.AddOptions(); - + var services = CreateServices(); var builder = new OpenIddictBuilder(services); // Act - builder.Configure(configuration => configuration.Description.DisplayName = "OpenIddict"); - - var provider = services.BuildServiceProvider(); - var options = provider.GetRequiredService>(); - - // Assert - Assert.Equal("OpenIddict", options.Value.Description.DisplayName); - } - - [Fact] - public void UseOpenIddict_OpenIdConnectServerMiddlewareIsRegistered() - { - // Arrange - var services = new ServiceCollection(); + builder.Configure(configuration => configuration.AccessTokenLifetime = TimeSpan.FromDays(1)); - services.AddOpenIddict() - .AddSigningCertificate( - assembly: typeof(OpenIddictProviderTests).GetTypeInfo().Assembly, - resource: "OpenIddict.Tests.Certificate.pfx", - password: "OpenIddict") - .AllowImplicitFlow() - .EnableAuthorizationEndpoint("/connect/authorize"); - - var builder = new Mock(); - builder.SetupGet(mock => mock.ApplicationServices) - .Returns(services.BuildServiceProvider()); - - // Act - builder.Object.UseOpenIddict(); + var options = GetOptions(services); // Assert - builder.Verify(mock => mock.Use(It.IsAny>()), Times.Once()); + Assert.Equal(TimeSpan.FromDays(1), options.AccessTokenLifetime); } [Fact] public void AddEphemeralSigningKey_SigningKeyIsCorrectlyAdded() { // Arrange - var services = new ServiceCollection(); - services.AddOptions(); - + var services = CreateServices(); var builder = new OpenIddictBuilder(services); // Act builder.AddEphemeralSigningKey(); - var provider = services.BuildServiceProvider(); - var options = provider.GetRequiredService>(); + var options = GetOptions(services); // Assert - Assert.Equal(1, options.Value.SigningCredentials.Count); + Assert.Equal(1, options.SigningCredentials.Count); } [Theory] @@ -223,17 +64,14 @@ namespace OpenIddict.Tests public void AddEphemeralSigningKey_SigningCredentialsUseSpecifiedAlgorithm(string algorithm) { // Arrange - var services = new ServiceCollection(); - services.AddOptions(); - + var services = CreateServices(); var builder = new OpenIddictBuilder(services); // Act builder.AddEphemeralSigningKey(algorithm); - var provider = services.BuildServiceProvider(); - var options = provider.GetRequiredService>(); - var credentials = options.Value.SigningCredentials[0]; + var options = GetOptions(services); + var credentials = options.SigningCredentials[0]; // Assert Assert.Equal(algorithm, credentials.Algorithm); @@ -250,9 +88,7 @@ namespace OpenIddict.Tests public void AddSigningKey_SigningKeyIsCorrectlyAdded(string algorithm) { // Arrange - var services = new ServiceCollection(); - services.AddOptions(); - + var services = CreateServices(); var builder = new OpenIddictBuilder(services); var factory = Mock.Of(mock => @@ -263,20 +99,17 @@ namespace OpenIddict.Tests // Act builder.AddSigningKey(key); - var provider = services.BuildServiceProvider(); - var options = provider.GetRequiredService>(); + var options = GetOptions(services); // Assert - Assert.Same(key, options.Value.SigningCredentials[0].Key); + Assert.Same(key, options.SigningCredentials[0].Key); } [Fact] public void AddSigningCertificate_SigningKeyIsCorrectlyAdded() { // Arrange - var services = new ServiceCollection(); - services.AddOptions(); - + var services = CreateServices(); var builder = new OpenIddictBuilder(services); // Act @@ -285,486 +118,432 @@ namespace OpenIddict.Tests resource: "OpenIddict.Tests.Certificate.pfx", password: "OpenIddict"); - var provider = services.BuildServiceProvider(); - var options = provider.GetRequiredService>(); + var options = GetOptions(services); // Assert - Assert.IsType(typeof(X509SecurityKey), options.Value.SigningCredentials[0].Key); + Assert.IsType(options.SigningCredentials[0].Key); } [Fact] public void AllowAuthorizationCodeFlow_CodeFlowIsAddedToGrantTypes() { // Arrange - var services = new ServiceCollection(); - services.AddOptions(); - + var services = CreateServices(); var builder = new OpenIddictBuilder(services); // Act builder.AllowAuthorizationCodeFlow(); - var provider = services.BuildServiceProvider(); - var options = provider.GetRequiredService>(); + var options = GetOptions(services); // Assert - Assert.Contains(OpenIdConnectConstants.GrantTypes.AuthorizationCode, options.Value.GrantTypes); + Assert.Contains(OpenIdConnectConstants.GrantTypes.AuthorizationCode, options.GrantTypes); } [Fact] public void AllowClientCredentialsFlow_ClientCredentialsFlowIsAddedToGrantTypes() { // Arrange - var services = new ServiceCollection(); - services.AddOptions(); - + var services = CreateServices(); var builder = new OpenIddictBuilder(services); // Act builder.AllowClientCredentialsFlow(); - var provider = services.BuildServiceProvider(); - var options = provider.GetRequiredService>(); + var options = GetOptions(services); // Assert - Assert.Contains(OpenIdConnectConstants.GrantTypes.ClientCredentials, options.Value.GrantTypes); + Assert.Contains(OpenIdConnectConstants.GrantTypes.ClientCredentials, options.GrantTypes); } [Fact] public void AllowCustomFlow_CustomFlowIsAddedToGrantTypes() { // Arrange - var services = new ServiceCollection(); - services.AddOptions(); - + var services = CreateServices(); var builder = new OpenIddictBuilder(services); // Act builder.AllowCustomFlow("urn:ietf:params:oauth:grant-type:custom_grant"); - var provider = services.BuildServiceProvider(); - var options = provider.GetRequiredService>(); + var options = GetOptions(services); // Assert - Assert.Contains("urn:ietf:params:oauth:grant-type:custom_grant", options.Value.GrantTypes); + Assert.Contains("urn:ietf:params:oauth:grant-type:custom_grant", options.GrantTypes); } [Fact] public void AllowImplicitFlow_ImplicitFlowIsAddedToGrantTypes() { // Arrange - var services = new ServiceCollection(); - services.AddOptions(); - + var services = CreateServices(); var builder = new OpenIddictBuilder(services); // Act builder.AllowImplicitFlow(); - var provider = services.BuildServiceProvider(); - var options = provider.GetRequiredService>(); + var options = GetOptions(services); // Assert - Assert.Contains(OpenIdConnectConstants.GrantTypes.Implicit, options.Value.GrantTypes); + Assert.Contains(OpenIdConnectConstants.GrantTypes.Implicit, options.GrantTypes); } [Fact] public void AllowPasswordFlow_PasswordFlowIsAddedToGrantTypes() { // Arrange - var services = new ServiceCollection(); - services.AddOptions(); - + var services = CreateServices(); var builder = new OpenIddictBuilder(services); // Act builder.AllowPasswordFlow(); - var provider = services.BuildServiceProvider(); - var options = provider.GetRequiredService>(); + var options = GetOptions(services); // Assert - Assert.Contains(OpenIdConnectConstants.GrantTypes.Password, options.Value.GrantTypes); + Assert.Contains(OpenIdConnectConstants.GrantTypes.Password, options.GrantTypes); } [Fact] public void AllowRefreshTokenFlow_RefreshTokenFlowIsAddedToGrantTypes() { // Arrange - var services = new ServiceCollection(); - services.AddOptions(); - + var services = CreateServices(); var builder = new OpenIddictBuilder(services); // Act builder.AllowRefreshTokenFlow(); - var provider = services.BuildServiceProvider(); - var options = provider.GetRequiredService>(); + var options = GetOptions(services); // Assert - Assert.Contains(OpenIdConnectConstants.GrantTypes.RefreshToken, options.Value.GrantTypes); + Assert.Contains(OpenIdConnectConstants.GrantTypes.RefreshToken, options.GrantTypes); } [Fact] public void DisableConfigurationEndpoint_ConfigurationEndpointIsDisabled() { // Arrange - var services = new ServiceCollection(); - services.AddOptions(); - + var services = CreateServices(); var builder = new OpenIddictBuilder(services); // Act builder.DisableConfigurationEndpoint(); - var provider = services.BuildServiceProvider(); - var options = provider.GetRequiredService>(); + var options = GetOptions(services); // Assert - Assert.Equal(PathString.Empty, options.Value.ConfigurationEndpointPath); + Assert.Equal(PathString.Empty, options.ConfigurationEndpointPath); } [Fact] public void DisableCryptographyEndpoint_CryptographyEndpointIsDisabled() { // Arrange - var services = new ServiceCollection(); - services.AddOptions(); - + var services = CreateServices(); var builder = new OpenIddictBuilder(services); // Act builder.DisableCryptographyEndpoint(); - var provider = services.BuildServiceProvider(); - var options = provider.GetRequiredService>(); + var options = GetOptions(services); // Assert - Assert.Equal(PathString.Empty, options.Value.CryptographyEndpointPath); + Assert.Equal(PathString.Empty, options.CryptographyEndpointPath); } [Fact] public void DisableSlidingExpiration_SlidingExpirationIsDisabled() { // Arrange - var services = new ServiceCollection(); - services.AddOptions(); - + var services = CreateServices(); var builder = new OpenIddictBuilder(services); // Act builder.DisableSlidingExpiration(); - var provider = services.BuildServiceProvider(); - var options = provider.GetRequiredService>(); + var options = GetOptions(services); // Assert - Assert.False(options.Value.UseSlidingExpiration); + Assert.False(options.UseSlidingExpiration); } [Fact] public void DisableTokenRevocation_TokenRevocationIsDisabled() { // Arrange - var services = new ServiceCollection(); - services.AddOptions(); - + var services = CreateServices(); var builder = new OpenIddictBuilder(services); // Act builder.DisableTokenRevocation(); - var provider = services.BuildServiceProvider(); - var options = provider.GetRequiredService>(); + var options = GetOptions(services); // Assert - Assert.True(options.Value.DisableTokenRevocation); + Assert.True(options.DisableTokenRevocation); } [Fact] public void EnableAuthorizationEndpoint_AuthorizationEndpointIsEnabled() { // Arrange - var services = new ServiceCollection(); - services.AddOptions(); - + var services = CreateServices(); var builder = new OpenIddictBuilder(services); // Act builder.EnableAuthorizationEndpoint("/endpoint-path"); - var provider = services.BuildServiceProvider(); - var options = provider.GetRequiredService>(); + var options = GetOptions(services); // Assert - Assert.Equal("/endpoint-path", options.Value.AuthorizationEndpointPath); + Assert.Equal("/endpoint-path", options.AuthorizationEndpointPath); } [Fact] public void EnableIntrospectionEndpoint_IntrospectionEndpointIsEnabled() { // Arrange - var services = new ServiceCollection(); - services.AddOptions(); - + var services = CreateServices(); var builder = new OpenIddictBuilder(services); // Act builder.EnableIntrospectionEndpoint("/endpoint-path"); - var provider = services.BuildServiceProvider(); - var options = provider.GetRequiredService>(); + var options = GetOptions(services); // Assert - Assert.Equal("/endpoint-path", options.Value.IntrospectionEndpointPath); + Assert.Equal("/endpoint-path", options.IntrospectionEndpointPath); } [Fact] public void EnableLogoutEndpoint_LogoutEndpointIsEnabled() { // Arrange - var services = new ServiceCollection(); - services.AddOptions(); - + var services = CreateServices(); var builder = new OpenIddictBuilder(services); // Act builder.EnableLogoutEndpoint("/endpoint-path"); - var provider = services.BuildServiceProvider(); - var options = provider.GetRequiredService>(); + var options = GetOptions(services); // Assert - Assert.Equal("/endpoint-path", options.Value.LogoutEndpointPath); + Assert.Equal("/endpoint-path", options.LogoutEndpointPath); } [Fact] public void EnableRequestCaching_RequestCachingIsEnabled() { // Arrange - var services = new ServiceCollection(); - services.AddOptions(); - + var services = CreateServices(); var builder = new OpenIddictBuilder(services); // Act builder.EnableRequestCaching(); - var provider = services.BuildServiceProvider(); - var options = provider.GetRequiredService>(); + var options = GetOptions(services); // Assert - Assert.True(options.Value.EnableRequestCaching); + Assert.True(options.EnableRequestCaching); } [Fact] public void EnableRevocationEndpoint_RevocationEndpointIsEnabled() { // Arrange - var services = new ServiceCollection(); - services.AddOptions(); - + var services = CreateServices(); var builder = new OpenIddictBuilder(services); // Act builder.EnableRevocationEndpoint("/endpoint-path"); - var provider = services.BuildServiceProvider(); - var options = provider.GetRequiredService>(); + var options = GetOptions(services); // Assert - Assert.Equal("/endpoint-path", options.Value.RevocationEndpointPath); + Assert.Equal("/endpoint-path", options.RevocationEndpointPath); } [Fact] public void EnableTokenEndpoint_TokenEndpointIsEnabled() { // Arrange - var services = new ServiceCollection(); - services.AddOptions(); - + var services = CreateServices(); var builder = new OpenIddictBuilder(services); // Act builder.EnableTokenEndpoint("/endpoint-path"); - var provider = services.BuildServiceProvider(); - var options = provider.GetRequiredService>(); + var options = GetOptions(services); // Assert - Assert.Equal("/endpoint-path", options.Value.TokenEndpointPath); + Assert.Equal("/endpoint-path", options.TokenEndpointPath); } [Fact] public void EnableUserinfoEndpoint_UserinfoEndpointIsEnabled() { // Arrange - var services = new ServiceCollection(); - services.AddOptions(); - + var services = CreateServices(); var builder = new OpenIddictBuilder(services); // Act builder.EnableUserinfoEndpoint("/endpoint-path"); - var provider = services.BuildServiceProvider(); - var options = provider.GetRequiredService>(); + var options = GetOptions(services); // Assert - Assert.Equal("/endpoint-path", options.Value.UserinfoEndpointPath); + Assert.Equal("/endpoint-path", options.UserinfoEndpointPath); } [Fact] public void RequireClientIdentification_ClientIdentificationIsEnforced() { // Arrange - var services = new ServiceCollection(); - services.AddOptions(); - + var services = CreateServices(); var builder = new OpenIddictBuilder(services); // Act builder.RequireClientIdentification(); - var provider = services.BuildServiceProvider(); - var options = provider.GetRequiredService>(); + var options = GetOptions(services); // Assert - Assert.True(options.Value.RequireClientIdentification); + Assert.True(options.RequireClientIdentification); } [Fact] public void SetAccessTokenLifetime_DefaultAccessTokenLifetimeIsReplaced() { // Arrange - var services = new ServiceCollection(); - services.AddOptions(); - + var services = CreateServices(); var builder = new OpenIddictBuilder(services); // Act builder.SetAccessTokenLifetime(TimeSpan.FromMinutes(42)); - var provider = services.BuildServiceProvider(); - var options = provider.GetRequiredService>(); + var options = GetOptions(services); // Assert - Assert.Equal(TimeSpan.FromMinutes(42), options.Value.AccessTokenLifetime); + Assert.Equal(TimeSpan.FromMinutes(42), options.AccessTokenLifetime); } [Fact] public void SetAuthorizationCodeLifetime_DefaultAuthorizationCodeLifetimeIsReplaced() { // Arrange - var services = new ServiceCollection(); - services.AddOptions(); - + var services = CreateServices(); var builder = new OpenIddictBuilder(services); // Act builder.SetAuthorizationCodeLifetime(TimeSpan.FromMinutes(42)); - var provider = services.BuildServiceProvider(); - var options = provider.GetRequiredService>(); + var options = GetOptions(services); // Assert - Assert.Equal(TimeSpan.FromMinutes(42), options.Value.AuthorizationCodeLifetime); + Assert.Equal(TimeSpan.FromMinutes(42), options.AuthorizationCodeLifetime); } [Fact] public void SetIdentityTokenLifetime_DefaultIdentityTokenLifetimeIsReplaced() { // Arrange - var services = new ServiceCollection(); - services.AddOptions(); - + var services = CreateServices(); var builder = new OpenIddictBuilder(services); // Act builder.SetIdentityTokenLifetime(TimeSpan.FromMinutes(42)); - var provider = services.BuildServiceProvider(); - var options = provider.GetRequiredService>(); + var options = GetOptions(services); // Assert - Assert.Equal(TimeSpan.FromMinutes(42), options.Value.IdentityTokenLifetime); + Assert.Equal(TimeSpan.FromMinutes(42), options.IdentityTokenLifetime); } [Fact] public void SetRefreshTokenLifetime_DefaultRefreshTokenLifetimeIsReplaced() { // Arrange - var services = new ServiceCollection(); - services.AddOptions(); - + var services = CreateServices(); var builder = new OpenIddictBuilder(services); // Act builder.SetRefreshTokenLifetime(TimeSpan.FromMinutes(42)); - var provider = services.BuildServiceProvider(); - var options = provider.GetRequiredService>(); + var options = GetOptions(services); // Assert - Assert.Equal(TimeSpan.FromMinutes(42), options.Value.RefreshTokenLifetime); + Assert.Equal(TimeSpan.FromMinutes(42), options.RefreshTokenLifetime); } [Fact] public void SetIssuer_AddressIsReplaced() { // Arrange - var services = new ServiceCollection(); - services.AddOptions(); - + var services = CreateServices(); var builder = new OpenIddictBuilder(services); // Act builder.SetIssuer(new Uri("http://www.fabrikam.com/")); - var provider = services.BuildServiceProvider(); - var options = provider.GetRequiredService>(); + var options = GetOptions(services); // Assert - Assert.Equal(new Uri("http://www.fabrikam.com/"), options.Value.Issuer); + Assert.Equal(new Uri("http://www.fabrikam.com/"), options.Issuer); } [Fact] public void UseDataProtectionProvider_DefaultProviderIsReplaced() { // Arrange - var services = new ServiceCollection(); - services.AddOptions(); - + var services = CreateServices(); var builder = new OpenIddictBuilder(services); // Act - builder.UseDataProtectionProvider(new EphemeralDataProtectionProvider()); + builder.UseDataProtectionProvider(new EphemeralDataProtectionProvider(new LoggerFactory())); - var provider = services.BuildServiceProvider(); - var options = provider.GetRequiredService>(); + var options = GetOptions(services); // Assert - Assert.IsType(typeof(EphemeralDataProtectionProvider), options.Value.DataProtectionProvider); + Assert.IsType(options.DataProtectionProvider); } [Fact] public void UseJsonWebTokens_AccessTokenHandlerIsCorrectlySet() { // Arrange - var services = new ServiceCollection(); - services.AddOptions(); - + var services = CreateServices(); var builder = new OpenIddictBuilder(services); // Act builder.UseJsonWebTokens(); - var provider = services.BuildServiceProvider(); - var options = provider.GetRequiredService>(); + var options = GetOptions(services); // Assert - Assert.IsType(typeof(JwtSecurityTokenHandler), options.Value.AccessTokenHandler); + Assert.IsType(options.AccessTokenHandler); + } + + private static IServiceCollection CreateServices() + { + var services = new ServiceCollection(); + services.AddAuthentication(); + services.AddDistributedMemoryCache(); + services.AddLogging(); + services.AddSingleton(); + + return services; + } + + private static OpenIddictOptions GetOptions(IServiceCollection services) + { + services.RemoveAll>(); + services.RemoveAll>(); + + var provider = services.BuildServiceProvider(); + + var options = provider.GetRequiredService>(); + return options.Get(OpenIdConnectServerDefaults.AuthenticationScheme); } } } diff --git a/test/OpenIddict.Tests/OpenIddictInitializerTests.cs b/test/OpenIddict.Tests/OpenIddictInitializerTests.cs new file mode 100644 index 00000000..6f527ae2 --- /dev/null +++ b/test/OpenIddict.Tests/OpenIddictInitializerTests.cs @@ -0,0 +1,164 @@ +using System; +using System.Threading.Tasks; +using AspNet.Security.OpenIdConnect.Client; +using AspNet.Security.OpenIdConnect.Primitives; +using AspNet.Security.OpenIdConnect.Server; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Xunit; + +namespace OpenIddict.Tests +{ + public class OpenIddictInitializerTests + { + [Fact] + public async Task PostConfigure_ThrowsAnExceptionWhenNoFlowIsEnabled() + { + // Arrange + var server = CreateAuthorizationServer(builder => + { + builder.Configure(options => { }); + }); + + var client = new OpenIdConnectClient(server.CreateClient()); + + // Act and assert + var exception = await Assert.ThrowsAsync(delegate + { + return client.GetAsync("/"); + }); + + // Assert + Assert.Equal("At least one OAuth2/OpenID Connect flow must be enabled.", exception.Message); + } + + [Theory] + [InlineData(OpenIdConnectConstants.GrantTypes.AuthorizationCode)] + [InlineData(OpenIdConnectConstants.GrantTypes.Implicit)] + public async Task PostConfigure_ThrowsAnExceptionWhenAuthorizationEndpointIsDisabled(string flow) + { + // Arrange + var server = CreateAuthorizationServer(builder => + { + builder.Configure(options => options.GrantTypes.Add(flow)) + .Configure(options => options.AuthorizationEndpointPath = PathString.Empty); + }); + + var client = new OpenIdConnectClient(server.CreateClient()); + + // Act and assert + var exception = await Assert.ThrowsAsync(delegate + { + return client.GetAsync("/"); + }); + + Assert.Equal("The authorization endpoint must be enabled to use " + + "the authorization code and implicit flows.", exception.Message); + } + + [Theory] + [InlineData(OpenIdConnectConstants.GrantTypes.AuthorizationCode)] + [InlineData(OpenIdConnectConstants.GrantTypes.ClientCredentials)] + [InlineData(OpenIdConnectConstants.GrantTypes.Password)] + [InlineData(OpenIdConnectConstants.GrantTypes.RefreshToken)] + public async Task PostConfigure_ThrowsAnExceptionWhenTokenEndpointIsDisabled(string flow) + { + // Arrange + var server = CreateAuthorizationServer(builder => + { + builder.EnableAuthorizationEndpoint("/connect/authorize") + .Configure(options => options.GrantTypes.Add(flow)) + .Configure(options => options.TokenEndpointPath = PathString.Empty); + }); + + var client = new OpenIdConnectClient(server.CreateClient()); + + // Act and assert + var exception = await Assert.ThrowsAsync(delegate + { + return client.GetAsync("/"); + }); + + Assert.Equal("The token endpoint must be enabled to use the authorization code, " + + "client credentials, password and refresh token flows.", exception.Message); + } + + [Fact] + public async Task PostConfigure_ThrowsAnExceptionWhenTokenRevocationIsDisabled() + { + // Arrange + var server = CreateAuthorizationServer(builder => + { + builder.EnableAuthorizationEndpoint("/connect/authorize") + .EnableRevocationEndpoint("/connect/revocation") + .AllowImplicitFlow() + .DisableTokenRevocation(); + }); + + var client = new OpenIdConnectClient(server.CreateClient()); + + // Act and assert + var exception = await Assert.ThrowsAsync(delegate + { + return client.GetAsync("/"); + }); + + Assert.Equal("The revocation endpoint cannot be enabled when token revocation is disabled.", exception.Message); + } + + [Fact] + public async Task PostConfigure_ThrowsAnExceptionWhenNoSigningKeyIsRegisteredIfTheImplicitFlowIsEnabled() + { + // Arrange + var server = CreateAuthorizationServer(builder => + { + builder.EnableAuthorizationEndpoint("/connect/authorize") + .AllowImplicitFlow(); + }); + + var client = new OpenIdConnectClient(server.CreateClient()); + + // Act and assert + var exception = await Assert.ThrowsAsync(delegate + { + return client.GetAsync("/"); + }); + + Assert.Equal("At least one asymmetric signing key must be registered when enabling the implicit flow. " + + "Consider registering a X.509 certificate using 'services.AddOpenIddict().AddSigningCertificate()' " + + "or call 'services.AddOpenIddict().AddEphemeralSigningKey()' to use an ephemeral key.", exception.Message); + } + + private static TestServer CreateAuthorizationServer(Action configuration = null) + { + var builder = new WebHostBuilder(); + + builder.UseEnvironment("Testing"); + + builder.ConfigureLogging(options => options.AddDebug()); + + builder.ConfigureServices(services => + { + services.AddAuthentication(); + services.AddOptions(); + services.AddDistributedMemoryCache(); + + services.AddOpenIddict(options => configuration?.Invoke(options)); + }); + + builder.Configure(app => + { + app.UseAuthentication(); + + app.Run(context => context.ChallengeAsync(OpenIdConnectServerDefaults.AuthenticationScheme)); + }); + + return new TestServer(builder); + } + } +} diff --git a/test/OpenIddict.Tests/OpenIddictProviderTests.Authentication.cs b/test/OpenIddict.Tests/OpenIddictProviderTests.Authentication.cs index c6c39607..567767c8 100644 --- a/test/OpenIddict.Tests/OpenIddictProviderTests.Authentication.cs +++ b/test/OpenIddict.Tests/OpenIddictProviderTests.Authentication.cs @@ -521,7 +521,8 @@ namespace OpenIddict.Tests cache.Verify(mock => mock.SetAsync( OpenIddictConstants.Environment.AuthorizationRequest + identifier, It.IsAny(), - It.IsAny()), Times.Once()); + It.IsAny(), + It.IsAny()), Times.Once()); } [Theory] @@ -596,7 +597,7 @@ namespace OpenIddict.Tests }; var stream = new MemoryStream(); - using (var writer = new BsonWriter(stream)) + using (var writer = new BsonDataWriter(stream)) { writer.CloseOutput = false; @@ -606,8 +607,9 @@ namespace OpenIddict.Tests var cache = new Mock(); - cache.Setup(mock => mock.GetAsync(OpenIddictConstants.Environment.AuthorizationRequest + - "b2ee7815-5579-4ff7-86b0-ba671b939d96")) + cache.Setup(mock => mock.GetAsync( + OpenIddictConstants.Environment.AuthorizationRequest + "b2ee7815-5579-4ff7-86b0-ba671b939d96", + It.IsAny())) .ReturnsAsync(stream.ToArray()); var server = CreateAuthorizationServer(builder => @@ -646,8 +648,8 @@ namespace OpenIddict.Tests Assert.NotNull(response.AccessToken); cache.Verify(mock => mock.RemoveAsync( - OpenIddictConstants.Environment.AuthorizationRequest + - "b2ee7815-5579-4ff7-86b0-ba671b939d96"), Times.Once()); + OpenIddictConstants.Environment.AuthorizationRequest + "b2ee7815-5579-4ff7-86b0-ba671b939d96", + It.IsAny()), Times.Once()); } [Fact] diff --git a/test/OpenIddict.Tests/OpenIddictProviderTests.Exchange.cs b/test/OpenIddict.Tests/OpenIddictProviderTests.Exchange.cs index 35cc7264..33fedc91 100644 --- a/test/OpenIddict.Tests/OpenIddictProviderTests.Exchange.cs +++ b/test/OpenIddict.Tests/OpenIddictProviderTests.Exchange.cs @@ -8,7 +8,6 @@ using AspNet.Security.OpenIdConnect.Server; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Authentication; using Microsoft.Extensions.DependencyInjection; using Moq; using OpenIddict.Core; diff --git a/test/OpenIddict.Tests/OpenIddictProviderTests.Introspection.cs b/test/OpenIddict.Tests/OpenIddictProviderTests.Introspection.cs index 0d050c08..2577952c 100644 --- a/test/OpenIddict.Tests/OpenIddictProviderTests.Introspection.cs +++ b/test/OpenIddict.Tests/OpenIddictProviderTests.Introspection.cs @@ -9,7 +9,6 @@ using AspNet.Security.OpenIdConnect.Server; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Authentication; using Microsoft.Extensions.DependencyInjection; using Moq; using OpenIddict.Core; diff --git a/test/OpenIddict.Tests/OpenIddictProviderTests.Revocation.cs b/test/OpenIddict.Tests/OpenIddictProviderTests.Revocation.cs index 33492f1f..740141ce 100644 --- a/test/OpenIddict.Tests/OpenIddictProviderTests.Revocation.cs +++ b/test/OpenIddict.Tests/OpenIddictProviderTests.Revocation.cs @@ -10,7 +10,6 @@ using AspNet.Security.OpenIdConnect.Primitives; using AspNet.Security.OpenIdConnect.Server; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Http.Authentication; using Microsoft.Extensions.DependencyInjection; using Microsoft.IdentityModel.Tokens; using Moq; diff --git a/test/OpenIddict.Tests/OpenIddictProviderTests.Session.cs b/test/OpenIddict.Tests/OpenIddictProviderTests.Session.cs index e13b94fb..aa272c0b 100644 --- a/test/OpenIddict.Tests/OpenIddictProviderTests.Session.cs +++ b/test/OpenIddict.Tests/OpenIddictProviderTests.Session.cs @@ -126,7 +126,8 @@ namespace OpenIddict.Tests cache.Verify(mock => mock.SetAsync( OpenIddictConstants.Environment.LogoutRequest + identifier, It.IsAny(), - It.IsAny()), Times.Once()); + It.IsAny(), + It.IsAny()), Times.Once()); } [Fact] diff --git a/test/OpenIddict.Tests/OpenIddictProviderTests.cs b/test/OpenIddict.Tests/OpenIddictProviderTests.cs index eb93fafc..ad04f052 100644 --- a/test/OpenIddict.Tests/OpenIddictProviderTests.cs +++ b/test/OpenIddict.Tests/OpenIddictProviderTests.cs @@ -12,7 +12,6 @@ using Microsoft.AspNetCore.DataProtection; using Microsoft.AspNetCore.Diagnostics; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Authentication; using Microsoft.AspNetCore.TestHost; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -44,46 +43,66 @@ namespace OpenIddict.Tests builder.ConfigureServices(services => { - services.AddAuthentication(); services.AddOptions(); + services.AddDistributedMemoryCache(); - var instance = services.AddOpenIddict() + // Note: the following client_id/client_secret are fake and are only + // used to test the metadata returned by the discovery endpoint. + services.AddAuthentication() + .AddFacebook(options => + { + options.ClientId = "16018790-E88E-4553-8036-BB342579FF19"; + options.ClientSecret = "3D6499AF-5607-489B-815A-F3ACF1617296"; + options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme; + }) + + .AddGoogle(options => + { + options.ClientId = "BAF437A5-87FA-4D06-8EFD-F9BA96CCEDC4"; + options.ClientSecret = "27DF07D3-6B03-4EE0-95CD-3AC16782216B"; + options.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme; + }); + + // Replace the default OpenIddict managers. + services.AddSingleton(CreateApplicationManager()); + services.AddSingleton(CreateAuthorizationManager()); + services.AddSingleton(CreateTokenManager()); + + services.AddOpenIddict(options => + { // Disable the transport security requirement during testing. - .DisableHttpsRequirement() + options.DisableHttpsRequirement(); // Enable the tested endpoints. - .EnableAuthorizationEndpoint(AuthorizationEndpoint) - .EnableIntrospectionEndpoint(IntrospectionEndpoint) - .EnableLogoutEndpoint(LogoutEndpoint) - .EnableRevocationEndpoint(RevocationEndpoint) - .EnableTokenEndpoint(TokenEndpoint) - .EnableUserinfoEndpoint(UserinfoEndpoint) + options.EnableAuthorizationEndpoint(AuthorizationEndpoint) + .EnableIntrospectionEndpoint(IntrospectionEndpoint) + .EnableLogoutEndpoint(LogoutEndpoint) + .EnableRevocationEndpoint(RevocationEndpoint) + .EnableTokenEndpoint(TokenEndpoint) + .EnableUserinfoEndpoint(UserinfoEndpoint); // Enable the tested flows. - .AllowAuthorizationCodeFlow() - .AllowClientCredentialsFlow() - .AllowImplicitFlow() - .AllowPasswordFlow() - .AllowRefreshTokenFlow() + options.AllowAuthorizationCodeFlow() + .AllowClientCredentialsFlow() + .AllowImplicitFlow() + .AllowPasswordFlow() + .AllowRefreshTokenFlow(); // Register the X.509 certificate used to sign the identity tokens. - .AddSigningCertificate( + options.AddSigningCertificate( assembly: typeof(OpenIddictProviderTests).GetTypeInfo().Assembly, resource: "OpenIddict.Tests.Certificate.pfx", - password: "OpenIddict") + password: "OpenIddict"); // Note: overriding the default data protection provider is not necessary for the tests to pass, // but is useful to ensure unnecessary keys are not persisted in testing environments, which also // helps make the unit tests run faster, as no registry or disk access is required in this case. - .UseDataProtectionProvider(new EphemeralDataProtectionProvider()); + options.UseDataProtectionProvider(new EphemeralDataProtectionProvider(new LoggerFactory())); - // Replace the default application/token managers. - services.AddSingleton(CreateApplicationManager()); - services.AddSingleton(CreateTokenManager()); - - // Run the configuration delegate - // registered by the unit tests. - configuration?.Invoke(instance); + // Run the configuration delegate + // registered by the unit tests. + configuration?.Invoke(options); + }); }); builder.Configure(app => @@ -110,25 +129,7 @@ namespace OpenIddict.Tests return next(context); }); - app.UseCookieAuthentication(); - - // Note: the following client_id/client_secret are fake and are only - // used to test the metadata returned by the discovery endpoint. - app.UseFacebookAuthentication(new FacebookOptions - { - ClientId = "16018790-E88E-4553-8036-BB342579FF19", - ClientSecret = "3D6499AF-5607-489B-815A-F3ACF1617296", - SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme - }); - - app.UseGoogleAuthentication(new GoogleOptions - { - ClientId = "BAF437A5-87FA-4D06-8EFD-F9BA96CCEDC4", - ClientSecret = "27DF07D3-6B03-4EE0-95CD-3AC16782216B", - SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme - }); - - app.UseOpenIddict(); + app.UseAuthentication(); app.Run(context => { @@ -147,12 +148,12 @@ namespace OpenIddict.Tests ticket.SetProperty(OpenIddictConstants.Properties.AuthorizationId, "1AF06AB2-A0FC-4E3D-86AF-E04DA8C7BE70"); - return context.Authentication.SignInAsync(ticket.AuthenticationScheme, ticket.Principal, ticket.Properties); + return context.SignInAsync(ticket.AuthenticationScheme, ticket.Principal, ticket.Properties); } else if (request.IsLogoutRequest()) { - return context.Authentication.SignOutAsync(OpenIdConnectServerDefaults.AuthenticationScheme); + return context.SignOutAsync(OpenIdConnectServerDefaults.AuthenticationScheme); } else if (request.IsUserinfoRequest()) From d39ebfe27d29a8976051e29f122daf74adf86fb3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Chalet?= Date: Mon, 17 Jul 2017 20:35:38 +0200 Subject: [PATCH 02/11] Introduce OpenIddictBuilder.AddEncryptingKey() --- src/OpenIddict.Core/OpenIddictBuilder.cs | 4 -- src/OpenIddict/OpenIddictExtensions.cs | 22 ++++++++++ .../OpenIddictExtensionsTests.cs | 43 ++++++++++++++----- 3 files changed, 54 insertions(+), 15 deletions(-) diff --git a/src/OpenIddict.Core/OpenIddictBuilder.cs b/src/OpenIddict.Core/OpenIddictBuilder.cs index 47b549a9..a1af2bc2 100644 --- a/src/OpenIddict.Core/OpenIddictBuilder.cs +++ b/src/OpenIddict.Core/OpenIddictBuilder.cs @@ -10,10 +10,6 @@ using JetBrains.Annotations; using OpenIddict.Core; using OpenIddict.Models; -#if NETSTANDARD1_3 -using System.Reflection; -#endif - namespace Microsoft.Extensions.DependencyInjection { /// diff --git a/src/OpenIddict/OpenIddictExtensions.cs b/src/OpenIddict/OpenIddictExtensions.cs index a4970cd0..9f8adb9d 100644 --- a/src/OpenIddict/OpenIddictExtensions.cs +++ b/src/OpenIddict/OpenIddictExtensions.cs @@ -132,6 +132,28 @@ namespace Microsoft.AspNetCore.Builder return builder.Configure(options => options.SigningCredentials.AddEphemeralKey(algorithm)); } + /// + /// Registers a used to encrypt the JWT access tokens issued by OpenIddict. + /// + /// The services builder used by OpenIddict to register new services. + /// The security key. + /// The . + public static OpenIddictBuilder AddEncryptingKey( + [NotNull] this OpenIddictBuilder builder, [NotNull] SecurityKey key) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + if (key == null) + { + throw new ArgumentNullException(nameof(key)); + } + + return builder.Configure(options => options.EncryptingCredentials.AddKey(key)); + } + /// /// Registers a that is used to sign the JWT tokens issued by OpenIddict. /// diff --git a/test/OpenIddict.Tests/OpenIddictExtensionsTests.cs b/test/OpenIddict.Tests/OpenIddictExtensionsTests.cs index 109b12df..9b7e4e1a 100644 --- a/test/OpenIddict.Tests/OpenIddictExtensionsTests.cs +++ b/test/OpenIddict.Tests/OpenIddictExtensionsTests.cs @@ -53,13 +53,13 @@ namespace OpenIddict.Tests } [Theory] - [InlineData(SecurityAlgorithms.RsaSha256Signature)] - [InlineData(SecurityAlgorithms.RsaSha384Signature)] - [InlineData(SecurityAlgorithms.RsaSha512Signature)] + [InlineData(SecurityAlgorithms.RsaSha256)] + [InlineData(SecurityAlgorithms.RsaSha384)] + [InlineData(SecurityAlgorithms.RsaSha512)] #if SUPPORTS_ECDSA - [InlineData(SecurityAlgorithms.EcdsaSha256Signature)] - [InlineData(SecurityAlgorithms.EcdsaSha384Signature)] - [InlineData(SecurityAlgorithms.EcdsaSha512Signature)] + [InlineData(SecurityAlgorithms.EcdsaSha256)] + [InlineData(SecurityAlgorithms.EcdsaSha384)] + [InlineData(SecurityAlgorithms.EcdsaSha512)] #endif public void AddEphemeralSigningKey_SigningCredentialsUseSpecifiedAlgorithm(string algorithm) { @@ -77,13 +77,34 @@ namespace OpenIddict.Tests Assert.Equal(algorithm, credentials.Algorithm); } + [Fact] + public void AddEncryptingKey_EncryptingKeyIsCorrectlyAdded() + { + // Arrange + var services = CreateServices(); + var builder = new OpenIddictBuilder(services); + + var factory = Mock.Of(mock => + mock.IsSupportedAlgorithm(SecurityAlgorithms.Aes256KW, It.IsAny())); + + var key = Mock.Of(mock => mock.CryptoProviderFactory == factory); + + // Act + builder.AddEncryptingKey(key); + + var options = GetOptions(services); + + // Assert + Assert.Same(key, options.EncryptingCredentials[0].Key); + } + [Theory] - [InlineData(SecurityAlgorithms.HmacSha256Signature)] - [InlineData(SecurityAlgorithms.RsaSha256Signature)] + [InlineData(SecurityAlgorithms.HmacSha256)] + [InlineData(SecurityAlgorithms.RsaSha256)] #if SUPPORTS_ECDSA - [InlineData(SecurityAlgorithms.EcdsaSha256Signature)] - [InlineData(SecurityAlgorithms.EcdsaSha384Signature)] - [InlineData(SecurityAlgorithms.EcdsaSha512Signature)] + [InlineData(SecurityAlgorithms.EcdsaSha256)] + [InlineData(SecurityAlgorithms.EcdsaSha384)] + [InlineData(SecurityAlgorithms.EcdsaSha512)] #endif public void AddSigningKey_SigningKeyIsCorrectlyAdded(string algorithm) { From 977366a3e5284c36c95f4e4ab1428a986ff32ac7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Chalet?= Date: Wed, 26 Jul 2017 03:10:06 +0200 Subject: [PATCH 03/11] Replace the aspnetcore-ci-dev feed by aspnetcore-dev --- NuGet.config | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NuGet.config b/NuGet.config index 8b9726ce..52e4e450 100644 --- a/NuGet.config +++ b/NuGet.config @@ -2,7 +2,7 @@ - + \ No newline at end of file From b7359dd93d6b81cb0268eb60c9d68d6340429b71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Chalet?= Date: Wed, 26 Jul 2017 04:14:13 +0200 Subject: [PATCH 04/11] Update OpenIddict.Tests.csproj to add a net47 target --- test/OpenIddict.Tests/OpenIddict.Tests.csproj | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/OpenIddict.Tests/OpenIddict.Tests.csproj b/test/OpenIddict.Tests/OpenIddict.Tests.csproj index 4f54702f..adca7c03 100644 --- a/test/OpenIddict.Tests/OpenIddict.Tests.csproj +++ b/test/OpenIddict.Tests/OpenIddict.Tests.csproj @@ -4,6 +4,7 @@ netcoreapp2.0;net461 + $(TargetFrameworks);net47 netcoreapp2.0 @@ -34,7 +35,7 @@ - + $(DefineConstants);SUPPORTS_ECDSA From 46dceca9390a1ae9de14b7a67cac3493b5394cb0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Chalet?= Date: Mon, 7 Aug 2017 21:37:55 +0200 Subject: [PATCH 05/11] Introduce OpenIddictBuilder.AddDevelopmentSigningCertificate() --- src/OpenIddict/OpenIddictExtensions.cs | 44 +++++++++++- src/OpenIddict/OpenIddictInitializer.cs | 17 ++++- test/OpenIddict.Tests/OpenIddict.Tests.csproj | 4 ++ .../OpenIddictExtensionsTests.cs | 69 +++++++++++++++++++ .../OpenIddictInitializerTests.cs | 35 +++++++++- 5 files changed, 161 insertions(+), 8 deletions(-) diff --git a/src/OpenIddict/OpenIddictExtensions.cs b/src/OpenIddict/OpenIddictExtensions.cs index 9f8adb9d..0df50a88 100644 --- a/src/OpenIddict/OpenIddictExtensions.cs +++ b/src/OpenIddict/OpenIddictExtensions.cs @@ -61,11 +61,11 @@ namespace Microsoft.AspNetCore.Builder // Note: TryAddEnumerable() is used here to ensure the initializers are only registered once. builder.Services.TryAddEnumerable( ServiceDescriptor.Singleton, - OpenIdConnectServerInitializer>()); + OpenIddictInitializer>()); builder.Services.TryAddEnumerable( ServiceDescriptor.Singleton, - OpenIddictInitializer>()); + OpenIdConnectServerInitializer>()); // Register the OpenID Connect server handler in the authentication options, // so it can be discovered by the default authentication handler provider. @@ -89,6 +89,46 @@ namespace Microsoft.AspNetCore.Builder return builder; } + /// + /// Registers (and generates if necessary) a user-specific development + /// certificate used to sign the JWT tokens issued by OpenIddict. + /// + /// The services builder used by OpenIddict to register new services. + /// The . + public static OpenIddictBuilder AddDevelopmentSigningCertificate( + [NotNull] this OpenIddictBuilder builder) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + return builder.Configure(options => options.SigningCredentials.AddDevelopmentCertificate()); + } + + /// + /// Registers (and generates if necessary) a user-specific development + /// certificate used to sign the JWT tokens issued by OpenIddict. + /// + /// The services builder used by OpenIddict to register new services. + /// The subject name associated with the certificate. + /// The . + public static OpenIddictBuilder AddDevelopmentSigningCertificate( + [NotNull] this OpenIddictBuilder builder, [NotNull] X500DistinguishedName subject) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + if (subject == null) + { + throw new ArgumentNullException(nameof(subject)); + } + + return builder.Configure(options => options.SigningCredentials.AddDevelopmentCertificate(subject)); + } + /// /// Registers a new ephemeral key used to sign the JWT tokens issued by OpenIddict: the key /// is discarded when the application shuts down and tokens signed using this key are diff --git a/src/OpenIddict/OpenIddictInitializer.cs b/src/OpenIddict/OpenIddictInitializer.cs index 3a713893..00e09f93 100644 --- a/src/OpenIddict/OpenIddictInitializer.cs +++ b/src/OpenIddict/OpenIddictInitializer.cs @@ -88,13 +88,24 @@ namespace OpenIddict throw new InvalidOperationException("The revocation endpoint cannot be enabled when token revocation is disabled."); } + if (options.AccessTokenHandler != null && options.SigningCredentials.Count == 0) + { + throw new InvalidOperationException( + "At least one signing key must be registered when using JWT as the access token format. " + + "Consider registering a X.509 certificate using 'services.AddOpenIddict().AddSigningCertificate()' " + + "or 'services.AddOpenIddict().AddDevelopmentSigningCertificate()' or call " + + "'services.AddOpenIddict().AddEphemeralSigningKey()' to use an ephemeral key."); + } + // Ensure at least one asymmetric signing certificate/key was registered if the implicit flow was enabled. if (!options.SigningCredentials.Any(credentials => credentials.Key is AsymmetricSecurityKey) && options.GrantTypes.Contains(OpenIdConnectConstants.GrantTypes.Implicit)) { - throw new InvalidOperationException("At least one asymmetric signing key must be registered when enabling the implicit flow. " + - "Consider registering a X.509 certificate using 'services.AddOpenIddict().AddSigningCertificate()' " + - "or call 'services.AddOpenIddict().AddEphemeralSigningKey()' to use an ephemeral key."); + throw new InvalidOperationException( + "At least one asymmetric signing key must be registered when enabling the implicit flow. " + + "Consider registering a X.509 certificate using 'services.AddOpenIddict().AddSigningCertificate()' " + + "or 'services.AddOpenIddict().AddDevelopmentSigningCertificate()' or call " + + "'services.AddOpenIddict().AddEphemeralSigningKey()' to use an ephemeral key."); } } } diff --git a/test/OpenIddict.Tests/OpenIddict.Tests.csproj b/test/OpenIddict.Tests/OpenIddict.Tests.csproj index adca7c03..def91b32 100644 --- a/test/OpenIddict.Tests/OpenIddict.Tests.csproj +++ b/test/OpenIddict.Tests/OpenIddict.Tests.csproj @@ -39,4 +39,8 @@ $(DefineConstants);SUPPORTS_ECDSA + + $(DefineConstants);SUPPORTS_CERTIFICATE_GENERATION + + diff --git a/test/OpenIddict.Tests/OpenIddictExtensionsTests.cs b/test/OpenIddict.Tests/OpenIddictExtensionsTests.cs index 9b7e4e1a..6b1e416e 100644 --- a/test/OpenIddict.Tests/OpenIddictExtensionsTests.cs +++ b/test/OpenIddict.Tests/OpenIddictExtensionsTests.cs @@ -36,6 +36,75 @@ namespace OpenIddict.Tests Assert.Equal(TimeSpan.FromDays(1), options.AccessTokenLifetime); } + [Fact] + public void AddDevelopmentSigningCertificate_ThrowsAnExceptionForNullBuilder() + { + // Arrange + var builder = (OpenIddictBuilder) null; + + // Act and assert + var exception = Assert.Throws(delegate + { + builder.AddDevelopmentSigningCertificate(); + }); + + Assert.Equal("builder", exception.ParamName); + } + + [Fact] + public void AddDevelopmentSigningCertificate_ThrowsAnExceptionForNullSubject() + { + // Arrange + var services = CreateServices(); + var builder = new OpenIddictBuilder(services); + + // Act and assert + var exception = Assert.Throws(delegate + { + builder.AddDevelopmentSigningCertificate(subject: null); + }); + + Assert.Equal("subject", exception.ParamName); + } + +#if SUPPORTS_CERTIFICATE_GENERATION + [Fact] + public void AddDevelopmentSigningCertificate_CanGenerateCertificate() + { + // Arrange + var services = CreateServices(); + var builder = new OpenIddictBuilder(services); + + // Act + builder.AddDevelopmentSigningCertificate(); + + var options = GetOptions(services); + + // Assert + Assert.Equal(1, options.SigningCredentials.Count); + Assert.Equal(SecurityAlgorithms.RsaSha256, options.SigningCredentials[0].Algorithm); + Assert.NotNull(options.SigningCredentials[0].Kid); + } +#else + [Fact] + public void AddDevelopmentSigningCertificate_ThrowsAnExceptionOnUnsupportedPlatforms() + { + // Arrange + var services = CreateServices(); + var builder = new OpenIddictBuilder(services); + + builder.AddDevelopmentSigningCertificate(); + + // Act and assert + var exception = Assert.Throws(delegate + { + return GetOptions(services); + }); + + Assert.Equal("X.509 certificate generation is not supported on this platform.", exception.Message); + } +#endif + [Fact] public void AddEphemeralSigningKey_SigningKeyIsCorrectlyAdded() { diff --git a/test/OpenIddict.Tests/OpenIddictInitializerTests.cs b/test/OpenIddict.Tests/OpenIddictInitializerTests.cs index 6f527ae2..6ce41c53 100644 --- a/test/OpenIddict.Tests/OpenIddictInitializerTests.cs +++ b/test/OpenIddict.Tests/OpenIddictInitializerTests.cs @@ -111,6 +111,33 @@ namespace OpenIddict.Tests Assert.Equal("The revocation endpoint cannot be enabled when token revocation is disabled.", exception.Message); } + [Fact] + public async Task PostConfigure_ThrowsAnExceptionWhenNoSigningKeyIsRegisteredIfAnAccessTokenHandlerIsSet() + { + // Arrange + var server = CreateAuthorizationServer(builder => + { + builder.EnableAuthorizationEndpoint("/connect/authorize") + .EnableTokenEndpoint("/connect/token") + .AllowAuthorizationCodeFlow() + .UseJsonWebTokens(); + }); + + var client = new OpenIdConnectClient(server.CreateClient()); + + // Act and assert + var exception = await Assert.ThrowsAsync(delegate + { + return client.GetAsync("/"); + }); + + Assert.Equal( + "At least one signing key must be registered when using JWT as the access token format. " + + "Consider registering a X.509 certificate using 'services.AddOpenIddict().AddSigningCertificate()' " + + "or 'services.AddOpenIddict().AddDevelopmentSigningCertificate()' or call " + + "'services.AddOpenIddict().AddEphemeralSigningKey()' to use an ephemeral key.", exception.Message); + } + [Fact] public async Task PostConfigure_ThrowsAnExceptionWhenNoSigningKeyIsRegisteredIfTheImplicitFlowIsEnabled() { @@ -129,9 +156,11 @@ namespace OpenIddict.Tests return client.GetAsync("/"); }); - Assert.Equal("At least one asymmetric signing key must be registered when enabling the implicit flow. " + - "Consider registering a X.509 certificate using 'services.AddOpenIddict().AddSigningCertificate()' " + - "or call 'services.AddOpenIddict().AddEphemeralSigningKey()' to use an ephemeral key.", exception.Message); + Assert.Equal( + "At least one asymmetric signing key must be registered when enabling the implicit flow. " + + "Consider registering a X.509 certificate using 'services.AddOpenIddict().AddSigningCertificate()' " + + "or 'services.AddOpenIddict().AddDevelopmentSigningCertificate()' or call " + + "'services.AddOpenIddict().AddEphemeralSigningKey()' to use an ephemeral key.", exception.Message); } private static TestServer CreateAuthorizationServer(Action configuration = null) From 42831a3a7adda202f08270d73b9b139cc3b5d955 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Chalet?= Date: Mon, 14 Aug 2017 02:49:10 +0200 Subject: [PATCH 06/11] Update dependencies.props to target the official ASP.NET Core 2.0 packages --- build/dependencies.props | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/build/dependencies.props b/build/dependencies.props index 8377df10..62c93864 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -3,16 +3,16 @@ 2.0.0-* 2.0.0-* - 2.0.0-* - 4.4.0-* + 2.0.0 + 4.4.0 2.0.0 10.3.0 1.0.1 4.7.63 - 2.0.0-* - 2.0.0-* - 2.0.0-* - 15.3.0-* + 2.0.0 + 2.0.0 + 2.0.0 + 15.3.0 2.3.0-* From e9d78fcc6165a86c507df4a5c098738b4816fd88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Chalet?= Date: Mon, 14 Aug 2017 02:49:38 +0200 Subject: [PATCH 07/11] Update NuGet.config to remove the aspnetcore-dev feed --- NuGet.config | 1 - 1 file changed, 1 deletion(-) diff --git a/NuGet.config b/NuGet.config index 52e4e450..ae8aea9e 100644 --- a/NuGet.config +++ b/NuGet.config @@ -2,7 +2,6 @@ - \ No newline at end of file From f68fad8f01ac92cc54e6dce948b98016cd977d57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Chalet?= Date: Tue, 15 Aug 2017 01:06:30 +0200 Subject: [PATCH 08/11] Use Assert.Single()/Empty() instead of Assert.Equal() --- .../OpenIddictProviderTests.Authentication.cs | 2 +- test/OpenIddict.Tests/OpenIddictProviderTests.Discovery.cs | 2 +- .../OpenIddictProviderTests.Introspection.cs | 6 +++--- test/OpenIddict.Tests/OpenIddictProviderTests.Revocation.cs | 4 ++-- test/OpenIddict.Tests/OpenIddictProviderTests.Session.cs | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/test/OpenIddict.Tests/OpenIddictProviderTests.Authentication.cs b/test/OpenIddict.Tests/OpenIddictProviderTests.Authentication.cs index 567767c8..30e8e548 100644 --- a/test/OpenIddict.Tests/OpenIddictProviderTests.Authentication.cs +++ b/test/OpenIddict.Tests/OpenIddictProviderTests.Authentication.cs @@ -515,7 +515,7 @@ namespace OpenIddict.Tests var identifier = (string) response[OpenIdConnectConstants.Parameters.RequestId]; // Assert - Assert.Equal(1, response.GetParameters().Count()); + Assert.Single(response.GetParameters()); Assert.NotNull(identifier); cache.Verify(mock => mock.SetAsync( diff --git a/test/OpenIddict.Tests/OpenIddictProviderTests.Discovery.cs b/test/OpenIddict.Tests/OpenIddictProviderTests.Discovery.cs index e354324f..79740b26 100644 --- a/test/OpenIddict.Tests/OpenIddictProviderTests.Discovery.cs +++ b/test/OpenIddict.Tests/OpenIddictProviderTests.Discovery.cs @@ -56,7 +56,7 @@ namespace OpenIddict.Tests var types = ((JArray) response[OpenIdConnectConstants.Metadata.GrantTypesSupported]).Values(); // Assert - Assert.Equal(1, types.Count()); + Assert.Single(types); Assert.Contains(flow, types); } diff --git a/test/OpenIddict.Tests/OpenIddictProviderTests.Introspection.cs b/test/OpenIddict.Tests/OpenIddictProviderTests.Introspection.cs index 2577952c..75d15783 100644 --- a/test/OpenIddict.Tests/OpenIddictProviderTests.Introspection.cs +++ b/test/OpenIddict.Tests/OpenIddictProviderTests.Introspection.cs @@ -224,7 +224,7 @@ namespace OpenIddict.Tests }); // Assert - Assert.Equal(1, response.GetParameters().Count()); + Assert.Single(response.GetParameters()); Assert.False((bool) response[OpenIdConnectConstants.Claims.Active]); } @@ -398,7 +398,7 @@ namespace OpenIddict.Tests }); // Assert - Assert.Equal(1, response.GetParameters().Count()); + Assert.Single(response.GetParameters()); Assert.False((bool) response[OpenIdConnectConstants.Claims.Active]); Mock.Get(manager).Verify(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny()), Times.Once()); @@ -462,7 +462,7 @@ namespace OpenIddict.Tests }); // Assert - Assert.Equal(1, response.GetParameters().Count()); + Assert.Single(response.GetParameters()); Assert.False((bool) response[OpenIdConnectConstants.Claims.Active]); Mock.Get(manager).Verify(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny()), Times.Once()); diff --git a/test/OpenIddict.Tests/OpenIddictProviderTests.Revocation.cs b/test/OpenIddict.Tests/OpenIddictProviderTests.Revocation.cs index 740141ce..557bdacb 100644 --- a/test/OpenIddict.Tests/OpenIddictProviderTests.Revocation.cs +++ b/test/OpenIddict.Tests/OpenIddictProviderTests.Revocation.cs @@ -336,7 +336,7 @@ namespace OpenIddict.Tests }); // Assert - Assert.Equal(0, response.GetParameters().Count()); + Assert.Empty(response.GetParameters()); Mock.Get(manager).Verify(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny()), Times.Once()); Mock.Get(manager).Verify(mock => mock.RevokeAsync(It.IsAny(), It.IsAny()), Times.Never()); @@ -382,7 +382,7 @@ namespace OpenIddict.Tests }); // Assert - Assert.Equal(0, response.GetParameters().Count()); + Assert.Empty(response.GetParameters()); Mock.Get(manager).Verify(mock => mock.FindByIdAsync("3E228451-1555-46F7-A471-951EFBA23A56", It.IsAny()), Times.Once()); Mock.Get(manager).Verify(mock => mock.RevokeAsync(token, It.IsAny()), Times.Once()); diff --git a/test/OpenIddict.Tests/OpenIddictProviderTests.Session.cs b/test/OpenIddict.Tests/OpenIddictProviderTests.Session.cs index aa272c0b..2e692099 100644 --- a/test/OpenIddict.Tests/OpenIddictProviderTests.Session.cs +++ b/test/OpenIddict.Tests/OpenIddictProviderTests.Session.cs @@ -120,7 +120,7 @@ namespace OpenIddict.Tests var identifier = (string) response[OpenIdConnectConstants.Parameters.RequestId]; // Assert - Assert.Equal(1, response.GetParameters().Count()); + Assert.Single(response.GetParameters()); Assert.NotNull(identifier); cache.Verify(mock => mock.SetAsync( From f8a559d3326ea9b9c008e174b670584da24bd062 Mon Sep 17 00:00:00 2001 From: Henk Mollema Date: Fri, 18 Aug 2017 10:48:26 +0200 Subject: [PATCH 09/11] Update to CryptoHelper 3.0.0 --- build/dependencies.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/dependencies.props b/build/dependencies.props index 62c93864..749fe6d6 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -5,7 +5,7 @@ 2.0.0-* 2.0.0 4.4.0 - 2.0.0 + 3.0.0 10.3.0 1.0.1 4.7.63 From 69ca17cb264d0b8eb93b3a63ed01fb935c99a3f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Chalet?= Date: Sat, 19 Aug 2017 02:31:11 +0200 Subject: [PATCH 10/11] Update OpenIddictCustomizer to inherit from RelationalModelCustomizer instead of ModelCustomizer --- src/OpenIddict.EntityFrameworkCore/OpenIddictCustomizer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/OpenIddict.EntityFrameworkCore/OpenIddictCustomizer.cs b/src/OpenIddict.EntityFrameworkCore/OpenIddictCustomizer.cs index 87c418d2..77459060 100644 --- a/src/OpenIddict.EntityFrameworkCore/OpenIddictCustomizer.cs +++ b/src/OpenIddict.EntityFrameworkCore/OpenIddictCustomizer.cs @@ -17,7 +17,7 @@ namespace OpenIddict.EntityFrameworkCore /// Represents a model customizer able to register the entity sets /// required by the OpenIddict stack in an Entity Framework context. /// - public class OpenIddictCustomizer : ModelCustomizer + public class OpenIddictCustomizer : RelationalModelCustomizer where TApplication : OpenIddictApplication, new() where TAuthorization : OpenIddictAuthorization, new() where TScope : OpenIddictScope, new() From 6b7a1752894e23012c125621f2abdeb5daa21c38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Chalet?= Date: Sat, 26 Aug 2017 16:08:44 +0200 Subject: [PATCH 11/11] Update README.md --- README.md | 45 ++++++++++++++++++++++++--------------------- 1 file changed, 24 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 68593a1a..d0db6382 100644 --- a/README.md +++ b/README.md @@ -4,10 +4,9 @@ [![Build status](https://ci.appveyor.com/api/projects/status/46ofo2eusje0hcw2?svg=true)](https://ci.appveyor.com/project/openiddict/openiddict-core) [![Build status](https://travis-ci.org/openiddict/openiddict-core.svg)](https://travis-ci.org/openiddict/openiddict-core) - ### What's OpenIddict? -OpenIddict aims at providing a **simple and easy-to-use solution** to implement an **OpenID Connect server in any ASP.NET Core application**. +OpenIddict aims at providing a **simple and easy-to-use solution** to implement an **OpenID Connect server in any ASP.NET Core 1.x or 2.x application**. OpenIddict is based on **[AspNet.Security.OpenIdConnect.Server (codenamed ASOS)](https://github.com/aspnet-contrib/AspNet.Security.OpenIdConnect.Server)** to control the OpenID Connect authentication flow and can be used with any membership stack, **including [ASP.NET Core Identity](https://github.com/aspnet/Identity)**. @@ -16,6 +15,9 @@ OpenIddict fully supports the **[code/implicit/hybrid flows](http://openid.net/s Note: OpenIddict uses **[Entity Framework Core](https://github.com/aspnet/EntityFramework)** by default, but you can also provide your own store. +> Note: **the OpenIddict 2.x packages are only compatible with ASP.NET Core 2.x**. +> If your application targets ASP.NET Core 1.x, use the OpenIddict 1.x packages. + ### Why an OpenID Connect server? Adding an OpenID Connect server to your application **allows you to support token authentication**. @@ -27,11 +29,13 @@ with the power to control who can access your API and the information that is ex **[Specialized samples can be found in the samples repository](https://github.com/openiddict/openiddict-samples):** - - [Authorization code flow sample](https://github.com/openiddict/openiddict-samples/tree/master/samples/CodeFlow) - - [Implicit flow sample](https://github.com/openiddict/openiddict-samples/tree/master/samples/ImplicitFlow) - - [Password flow sample](https://github.com/openiddict/openiddict-samples/tree/master/samples/PasswordFlow) - - [Client credentials flow sample](https://github.com/openiddict/openiddict-samples/tree/master/samples/ClientCredentialsFlow) - - [Refresh flow sample](https://github.com/openiddict/openiddict-samples/tree/master/samples/RefreshFlow) + - [Authorization code flow sample](https://github.com/openiddict/openiddict-samples/tree/dev/samples/CodeFlow) + - [Implicit flow sample](https://github.com/openiddict/openiddict-samples/tree/dev/samples/ImplicitFlow) + - [Password flow sample](https://github.com/openiddict/openiddict-samples/tree/dev/samples/PasswordFlow) + - [Client credentials flow sample](https://github.com/openiddict/openiddict-samples/tree/dev/samples/ClientCredentialsFlow) + - [Refresh flow sample](https://github.com/openiddict/openiddict-samples/tree/dev/samples/RefreshFlow) + +> **Samples for ASP.NET Core 1.x can be found [in the master branch of the samples repository](https://github.com/openiddict/openiddict-samples/tree/master)**. -------------- @@ -39,7 +43,7 @@ with the power to control who can access your API and the information that is ex To use OpenIddict, you need to: - - **Install the latest [.NET Core tooling](https://www.microsoft.com/net/download) and update your packages to reference the ASP.NET Core RTM packages**. + - **Install the latest [.NET Core 2.x tooling](https://www.microsoft.com/net/download) and update your packages to reference the ASP.NET Core 2.x packages**. - **Have an existing project or create a new one**: when creating a new project using Visual Studio's default ASP.NET Core template, using **individual user accounts authentication** is strongly recommended. When updating an existing project, you must provide your own `AccountController` to handle the registration process and the authentication flow. @@ -58,10 +62,10 @@ To use OpenIddict, you need to: - **Update your `.csproj` file** to reference `AspNet.Security.OAuth.Validation` and the `OpenIddict` packages: ```xml - - - - + + + + ``` - **Configure the OpenIddict services** in `Startup.ConfigureServices`: @@ -87,6 +91,10 @@ public void ConfigureServices(IServiceCollection services) .AddEntityFrameworkStores() .AddDefaultTokenProviders(); + // Register the OAuth2 validation handler. + services.AddAuthentication() + .AddOAuthValidation(); + // Register the OpenIddict services. // Note: use the generic overload if you need // to replace the default OpenIddict entities. @@ -116,22 +124,17 @@ public void ConfigureServices(IServiceCollection services) [Configuration and options](https://github.com/openiddict/core/wiki/Configuration-and-options) in the project wiki. - - **Add OpenIddict and the OAuth2 token validation middleware in your ASP.NET Core pipeline** by calling `app.UseOAuthValidation()` and `app.UseOpenIddict()` after `app.UseIdentity()` and before `app.UseMvc()`: + - **Make sure the authentication middleware is registered before all the other middleware, including `app.UseMvc()`: ```csharp -public void Configure(IApplicationBuilder app) { - app.UseIdentity(); - - app.UseOAuthValidation(); - - app.UseOpenIddict(); +public void Configure(IApplicationBuilder app) +{ + app.UseAuthentication(); app.UseMvc(); } ``` -> **Note:** `UseOpenIddict()` must be registered ***after*** `app.UseIdentity()` and the external social providers. - - **Update your Entity Framework context registration to register the OpenIddict entities**: ```csharp