diff --git a/Directory.Packages.props b/Directory.Packages.props
index 829b71fd7f1c..0221d7c1b50e 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -54,6 +54,8 @@
+
+
diff --git a/src/OrchardCore.Modules/OrchardCore.OpenId/Configuration/OpenIdClientConfiguration.cs b/src/OrchardCore.Modules/OrchardCore.OpenId/Configuration/OpenIdClientConfiguration.cs
index 64fb49971c72..c2ff9d1a73a3 100644
--- a/src/OrchardCore.Modules/OrchardCore.OpenId/Configuration/OpenIdClientConfiguration.cs
+++ b/src/OrchardCore.Modules/OrchardCore.OpenId/Configuration/OpenIdClientConfiguration.cs
@@ -1,10 +1,14 @@
using System.ComponentModel.DataAnnotations;
using System.Diagnostics;
+using System.Security.Cryptography;
using Microsoft.AspNetCore.Authentication;
-using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.AspNetCore.DataProtection;
+using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
+using Microsoft.IdentityModel.Tokens;
+using OpenIddict.Client;
+using OpenIddict.Client.AspNetCore;
using OrchardCore.Environment.Shell;
using OrchardCore.OpenId.Services;
using OrchardCore.OpenId.Settings;
@@ -13,21 +17,25 @@ namespace OrchardCore.OpenId.Configuration;
public sealed class OpenIdClientConfiguration :
IConfigureOptions,
- IConfigureNamedOptions
+ IConfigureOptions,
+ IConfigureNamedOptions
{
private readonly IOpenIdClientService _clientService;
private readonly IDataProtectionProvider _dataProtectionProvider;
+ private readonly IServiceProvider _serviceProvider;
private readonly ShellSettings _shellSettings;
private readonly ILogger _logger;
public OpenIdClientConfiguration(
IOpenIdClientService clientService,
IDataProtectionProvider dataProtectionProvider,
+ IServiceProvider serviceProvider,
ShellSettings shellSettings,
ILogger logger)
{
_clientService = clientService;
_dataProtectionProvider = dataProtectionProvider;
+ _serviceProvider = serviceProvider;
_shellSettings = shellSettings;
_logger = logger;
}
@@ -40,42 +48,53 @@ public void Configure(AuthenticationOptions options)
return;
}
- // Register the OpenID Connect client handler in the authentication handlers collection.
- options.AddScheme(OpenIdConnectDefaults.AuthenticationScheme, settings.DisplayName);
- }
+ options.AddScheme(
+ OpenIddictClientAspNetCoreDefaults.AuthenticationScheme, displayName: null);
- public void Configure(string name, OpenIdConnectOptions options)
- {
- // Ignore OpenID Connect client handler instances that don't correspond to the instance managed by the OpenID module.
- if (!string.Equals(name, OpenIdConnectDefaults.AuthenticationScheme, StringComparison.Ordinal))
+ foreach (var scheme in _serviceProvider.GetRequiredService>()
+ .CurrentValue.ForwardedAuthenticationSchemes)
{
- return;
+ options.AddScheme(scheme.Name, scheme.DisplayName);
}
+ }
+ public void Configure(OpenIddictClientOptions options)
+ {
var settings = GetClientSettingsAsync().GetAwaiter().GetResult();
if (settings == null)
{
return;
}
- options.Authority = settings.Authority.AbsoluteUri;
- options.ClientId = settings.ClientId;
- options.SignedOutRedirectUri = settings.SignedOutRedirectUri ?? options.SignedOutRedirectUri;
- options.SignedOutCallbackPath = settings.SignedOutCallbackPath ?? options.SignedOutCallbackPath;
- options.RequireHttpsMetadata = string.Equals(settings.Authority.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase);
- options.GetClaimsFromUserInfoEndpoint = true;
- options.ResponseMode = settings.ResponseMode;
- options.ResponseType = settings.ResponseType;
- options.SaveTokens = settings.StoreExternalTokens;
+ // Note: the provider name, redirect URI and post-logout redirect URI use the same default
+ // values as the Microsoft ASP.NET Core OpenID Connect handler, for compatibility reasons.
+ var registration = new OpenIddictClientRegistration
+ {
+ Issuer = settings.Authority,
+ ClientId = settings.ClientId,
+ RedirectUri = new Uri(settings.CallbackPath ?? "signin-oidc", UriKind.RelativeOrAbsolute),
+ PostLogoutRedirectUri = new Uri(settings.SignedOutCallbackPath ?? "signout-callback-oidc", UriKind.RelativeOrAbsolute),
+ ProviderName = "OpenIdConnect",
+ ProviderDisplayName = settings.DisplayName,
+ Properties =
+ {
+ [nameof(OpenIdClientSettings)] = settings
+ }
+ };
+
+ if (!string.IsNullOrEmpty(settings.ResponseMode))
+ {
+ registration.ResponseModes.Add(settings.ResponseMode);
+ }
- options.CallbackPath = settings.CallbackPath ?? options.CallbackPath;
+ if (!string.IsNullOrEmpty(settings.ResponseType))
+ {
+ registration.ResponseTypes.Add(settings.ResponseType);
+ }
if (settings.Scopes != null)
{
- foreach (var scope in settings.Scopes)
- {
- options.Scope.Add(scope);
- }
+ registration.Scopes.UnionWith(settings.Scopes);
}
if (!string.IsNullOrEmpty(settings.ClientSecret))
@@ -84,7 +103,7 @@ public void Configure(string name, OpenIdConnectOptions options)
try
{
- options.ClientSecret = protector.Unprotect(settings.ClientSecret);
+ registration.ClientSecret = protector.Unprotect(settings.ClientSecret);
}
catch
{
@@ -92,22 +111,38 @@ public void Configure(string name, OpenIdConnectOptions options)
}
}
- if (settings.Parameters != null && settings.Parameters.Length > 0)
- {
- var parameters = settings.Parameters;
- options.Events.OnRedirectToIdentityProvider = (context) =>
- {
- foreach (var parameter in parameters)
- {
- context.ProtocolMessage.SetParameter(parameter.Name, parameter.Value);
- }
+ options.Registrations.Add(registration);
- return Task.CompletedTask;
- };
- }
+ // Note: claims are mapped by CallbackController, so the built-in mapping feature is unnecessary.
+ options.DisableWebServicesFederationClaimMapping = true;
+
+ // TODO: use proper encryption/signing credentials, similar to what's used for the server feature.
+ options.EncryptionCredentials.Add(new EncryptingCredentials(new SymmetricSecurityKey(
+ RandomNumberGenerator.GetBytes(256 / 8)), SecurityAlgorithms.Aes256KW, SecurityAlgorithms.Aes256CbcHmacSha512));
+
+ options.SigningCredentials.Add(new SigningCredentials(new SymmetricSecurityKey(
+ RandomNumberGenerator.GetBytes(256 / 8)), SecurityAlgorithms.HmacSha256));
+ }
+
+ public void Configure(string name, OpenIddictClientAspNetCoreOptions options)
+ {
+ // Note: the OpenID module handles the redirection requests in its dedicated
+ // ASP.NET Core MVC controller, which requires enabling the pass-through mode.
+ options.EnableRedirectionEndpointPassthrough = true;
+ options.EnablePostLogoutRedirectionEndpointPassthrough = true;
+
+ // Note: error pass-through is enabled to allow the actions of the MVC callback controller
+ // to handle the errors returned by the interactive endpoints without relying on the generic
+ // status code pages middleware to rewrite the response later in the request processing.
+ options.EnableErrorPassthrough = true;
+
+ // Note: in Orchard, transport security is usually configured via the dedicated HTTPS module.
+ // To make configuration easier and avoid having to configure it in two different features,
+ // the transport security requirement enforced by OpenIddict by default is always turned off.
+ options.DisableTransportSecurityRequirement = true;
}
- public void Configure(OpenIdConnectOptions options) => Debug.Fail("This infrastructure method shouldn't be called.");
+ public void Configure(OpenIddictClientAspNetCoreOptions options) => Debug.Fail("This infrastructure method shouldn't be called.");
private async Task GetClientSettingsAsync()
{
diff --git a/src/OrchardCore.Modules/OrchardCore.OpenId/Controllers/CallbackController.cs b/src/OrchardCore.Modules/OrchardCore.OpenId/Controllers/CallbackController.cs
new file mode 100644
index 000000000000..5e6a5b6815ac
--- /dev/null
+++ b/src/OrchardCore.Modules/OrchardCore.OpenId/Controllers/CallbackController.cs
@@ -0,0 +1,171 @@
+using System;
+using System.Collections.Generic;
+using System.IdentityModel.Tokens.Jwt;
+using System.Linq;
+using System.Security.Claims;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore;
+using Microsoft.AspNetCore.Authentication;
+using Microsoft.AspNetCore.Authentication.Cookies;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Mvc;
+using OpenIddict.Client;
+using OpenIddict.Client.AspNetCore;
+using OrchardCore.Modules;
+using OrchardCore.OpenId.Settings;
+using OrchardCore.OpenId.ViewModels;
+using static OpenIddict.Abstractions.OpenIddictConstants;
+using static OpenIddict.Client.AspNetCore.OpenIddictClientAspNetCoreConstants;
+
+namespace OrchardCore.OpenId.Controllers;
+
+[AllowAnonymous, Feature(OpenIdConstants.Features.Client)]
+public class CallbackController : Controller
+{
+ private readonly OpenIddictClientService _service;
+
+ public CallbackController(OpenIddictClientService service)
+ => _service = service;
+
+ [IgnoreAntiforgeryToken]
+ public async Task LogInCallback()
+ {
+ var response = HttpContext.GetOpenIddictClientResponse();
+ if (response != null)
+ {
+ return View("Error", new ErrorViewModel
+ {
+ Error = response.Error,
+ ErrorDescription = response.ErrorDescription
+ });
+ }
+
+ var request = HttpContext.GetOpenIddictClientRequest();
+ if (request == null)
+ {
+ return NotFound();
+ }
+
+ // Retrieve the authorization data validated by OpenIddict as part of the callback handling.
+ var result = await HttpContext.AuthenticateAsync(OpenIddictClientAspNetCoreDefaults.AuthenticationScheme);
+
+ // Important: if the remote server doesn't support OpenID Connect and doesn't expose a userinfo endpoint,
+ // result.Principal.Identity will represent an unauthenticated identity and won't contain any claim.
+ //
+ // Such identities cannot be used as-is to build an authentication cookie in ASP.NET Core, as the
+ // antiforgery stack requires at least a name claim to bind CSRF cookies to the user's identity.
+ if (result.Principal.Identity is not ClaimsIdentity { IsAuthenticated: true })
+ {
+ throw new InvalidOperationException("The external authorization data cannot be used for authentication.");
+ }
+
+ // Build an identity based on the external claims and that will be used to create the authentication cookie.
+ //
+ // Note: for compatibility reasons, the claims are mapped to their WS-Federation equivalent
+ // using the default mapping provided by JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.
+ var claims = result.Principal.Claims.Select(claim =>
+ JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.TryGetValue(claim.Type, out var type) ?
+ new Claim(type, claim.Value, claim.ValueType, claim.Issuer, claim.OriginalIssuer, claim.Subject) : claim);
+
+ var identity = new ClaimsIdentity(claims,
+ authenticationType: CookieAuthenticationDefaults.AuthenticationScheme,
+ nameType: ClaimTypes.Name,
+ roleType: ClaimTypes.Role);
+
+ // Build the authentication properties based on the properties that were added when the challenge was triggered.
+ var properties = new AuthenticationProperties(result.Properties.Items)
+ {
+ RedirectUri = result.Properties.RedirectUri ?? "/"
+ };
+
+ // If enabled, preserve the received tokens in the authentication cookie.
+ //
+ // Note: for compatibility reasons, the tokens are stored using the same
+ // names as the Microsoft ASP.NET Core OIDC client: when both a frontchannel
+ // and a backchannel token exist, the backchannel one is always preferred.
+ var registration = await _service.GetClientRegistrationByIdAsync(result.Principal.FindFirstValue(Claims.Private.RegistrationId));
+ if (registration.Properties.TryGetValue(nameof(OpenIdClientSettings), out var settings) &&
+ settings is OpenIdClientSettings { StoreExternalTokens: true })
+ {
+ var tokens = new List();
+
+ if (!string.IsNullOrEmpty(result.Properties.GetTokenValue(Tokens.BackchannelAccessToken)) ||
+ !string.IsNullOrEmpty(result.Properties.GetTokenValue(Tokens.FrontchannelAccessToken)))
+ {
+ tokens.Add(new AuthenticationToken
+ {
+ Name = Parameters.AccessToken,
+ Value = result.Properties.GetTokenValue(Tokens.BackchannelAccessToken) ??
+ result.Properties.GetTokenValue(Tokens.FrontchannelAccessToken)
+ });
+ }
+
+ if (!string.IsNullOrEmpty(result.Properties.GetTokenValue(Tokens.BackchannelAccessTokenExpirationDate)) ||
+ !string.IsNullOrEmpty(result.Properties.GetTokenValue(Tokens.FrontchannelAccessTokenExpirationDate)))
+ {
+ tokens.Add(new AuthenticationToken
+ {
+ Name = "expires_at",
+ Value = result.Properties.GetTokenValue(Tokens.BackchannelAccessTokenExpirationDate) ??
+ result.Properties.GetTokenValue(Tokens.FrontchannelAccessTokenExpirationDate)
+ });
+ }
+
+ if (!string.IsNullOrEmpty(result.Properties.GetTokenValue(Tokens.BackchannelIdentityToken)) ||
+ !string.IsNullOrEmpty(result.Properties.GetTokenValue(Tokens.FrontchannelIdentityToken)))
+ {
+ tokens.Add(new AuthenticationToken
+ {
+ Name = Parameters.IdToken,
+ Value = result.Properties.GetTokenValue(Tokens.BackchannelIdentityToken) ??
+ result.Properties.GetTokenValue(Tokens.FrontchannelIdentityToken)
+ });
+ }
+
+ if (!string.IsNullOrEmpty(result.Properties.GetTokenValue(Tokens.RefreshToken)))
+ {
+ tokens.Add(new AuthenticationToken
+ {
+ Name = Parameters.RefreshToken,
+ Value = result.Properties.GetTokenValue(Tokens.RefreshToken)
+ });
+ }
+
+ properties.StoreTokens(tokens);
+ }
+
+ else
+ {
+ properties.StoreTokens(Enumerable.Empty());
+ }
+
+ // Ask the cookie authentication handler to return a new cookie and redirect
+ // the user agent to the return URL stored in the authentication properties.
+ return SignIn(new ClaimsPrincipal(identity), properties);
+ }
+
+ [IgnoreAntiforgeryToken]
+ public async Task LogOutCallback()
+ {
+ var response = HttpContext.GetOpenIddictClientResponse();
+ if (response != null)
+ {
+ return View("Error", new ErrorViewModel
+ {
+ Error = response.Error,
+ ErrorDescription = response.ErrorDescription
+ });
+ }
+
+ var request = HttpContext.GetOpenIddictClientRequest();
+ if (request == null)
+ {
+ return NotFound();
+ }
+
+ // Retrieve the data stored by OpenIddict in the state token created when the logout was triggered
+ // and redirect the user agent to the specified return URL (or to the home page if none was set).
+ var result = await HttpContext.AuthenticateAsync(OpenIddictClientAspNetCoreDefaults.AuthenticationScheme);
+ return Redirect(result!.Properties!.RedirectUri ?? "/");
+ }
+}
diff --git a/src/OrchardCore.Modules/OrchardCore.OpenId/OrchardCore.OpenId.csproj b/src/OrchardCore.Modules/OrchardCore.OpenId/OrchardCore.OpenId.csproj
index e6974b9d4dd3..91da25b6ebe6 100644
--- a/src/OrchardCore.Modules/OrchardCore.OpenId/OrchardCore.OpenId.csproj
+++ b/src/OrchardCore.Modules/OrchardCore.OpenId/OrchardCore.OpenId.csproj
@@ -30,8 +30,9 @@
-
+
+
diff --git a/src/OrchardCore.Modules/OrchardCore.OpenId/Startup.cs b/src/OrchardCore.Modules/OrchardCore.OpenId/Startup.cs
index 0a3e41050d51..e524d6271c4b 100644
--- a/src/OrchardCore.Modules/OrchardCore.OpenId/Startup.cs
+++ b/src/OrchardCore.Modules/OrchardCore.OpenId/Startup.cs
@@ -1,12 +1,13 @@
using System.ComponentModel.DataAnnotations;
using System.Diagnostics;
using Microsoft.AspNetCore.Authentication;
-using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Options;
+using OpenIddict.Client;
+using OpenIddict.Client.AspNetCore;
using OpenIddict.Server;
using OpenIddict.Server.AspNetCore;
using OpenIddict.Server.DataProtection;
@@ -61,25 +62,95 @@ public sealed class ClientStartup : StartupBase
{
public override void ConfigureServices(IServiceCollection services)
{
+ services.AddOpenIddict()
+ .AddClient(options =>
+ {
+ options.UseAspNetCore();
+ options.UseSystemNetHttp();
+
+ // TODO: determine what flows we want to enable and whether this
+ // should be configurable by the user (like the server feature).
+ options.AllowAuthorizationCodeFlow()
+ .AllowHybridFlow()
+ .AllowImplicitFlow();
+
+ options.AddEventHandler(builder =>
+ {
+ builder.UseInlineHandler(static context =>
+ {
+ // If the client registration is managed by Orchard, attach the custom parameters set by the user.
+ if (context.Registration.Properties.TryGetValue(nameof(OpenIdClientSettings), out var value) &&
+ value is OpenIdClientSettings settings && settings.Parameters is { Length: > 0 } parameters)
+ {
+ foreach (var parameter in parameters)
+ {
+ context.Parameters[parameter.Name] = parameter.Value;
+ }
+ }
+
+ return default;
+ });
+
+ builder.SetOrder(OpenIddictClientHandlers.AttachCustomChallengeParameters.Descriptor.Order - 1);
+ });
+ });
+
services.TryAddSingleton();
// Note: the following services are registered using TryAddEnumerable to prevent duplicate registrations.
- services.TryAddEnumerable(new[]
+ services.TryAddEnumerable(ServiceDescriptor.Scoped, OpenIdClientSettingsDisplayDriver>());
+ services.AddRecipeExecutionStep();
+
+ // Note: the OpenIddict ASP.NET host adds an authentication options initializer that takes care of
+ // registering the client ASP.NET Core handler. Yet, it MUST NOT be registered at this stage
+ // as it is lazily registered by OpenIdClientConfiguration only after checking the OpenID client
+ // settings are valid and can be safely used in this tenant without causing runtime exceptions.
+ // To prevent that, the initializer is manually removed from the services collection of the tenant.
+ services.RemoveAll, OpenIddictClientAspNetCoreConfiguration>();
+
+ services.TryAddEnumerable(ServiceDescriptor.Singleton, OpenIdClientConfiguration>());
+ services.TryAddEnumerable(ServiceDescriptor.Singleton, OpenIdClientConfiguration>());
+ services.TryAddEnumerable(ServiceDescriptor.Singleton, OpenIdClientConfiguration>());
+ }
+
+ public override void Configure(IApplicationBuilder app, IEndpointRouteBuilder routes, IServiceProvider serviceProvider)
+ {
+ var settings = GetClientSettingsAsync().GetAwaiter().GetResult();
+ if (settings == null)
{
- ServiceDescriptor.Scoped, OpenIdClientSettingsDisplayDriver>(),
- });
+ return;
+ }
- services.AddRecipeExecutionStep();
- // Register the options initializers required by the OpenID Connect client handler.
- services.TryAddEnumerable(new[]
+ // Note: the redirection and post-logout redirection endpoints use the same default values
+ // as the Microsoft ASP.NET Core OpenID Connect handler, for compatibility reasons.
+ routes.MapAreaControllerRoute(
+ name: "Callback.LogInCallback",
+ areaName: typeof(Startup).Namespace,
+ pattern: settings.CallbackPath ?? "signin-oidc",
+ defaults: new { controller = "Callback", action = "LogInCallback" }
+ );
+
+ routes.MapAreaControllerRoute(
+ name: "Callback.LogOutCallback",
+ areaName: typeof(Startup).Namespace,
+ pattern: settings.SignedOutCallbackPath ?? "signout-callback-oidc",
+ defaults: new { controller = "Callback", action = "LogOutCallback" }
+ );
+
+ async Task GetClientSettingsAsync()
{
- // Orchard-specific initializers:
- ServiceDescriptor.Singleton, OpenIdClientConfiguration>(),
- ServiceDescriptor.Singleton, OpenIdClientConfiguration>(),
+ // Note: the OpenID client service is registered as a singleton service and thus can be
+ // safely used with the non-scoped/root service provider available at this stage.
+ var service = serviceProvider.GetRequiredService();
+
+ var configuration = await service.GetSettingsAsync();
+ if ((await service.ValidateSettingsAsync(configuration)).Any(result => result != ValidationResult.Success))
+ {
+ return null;
+ }
- // Built-in initializers:
- ServiceDescriptor.Singleton, OpenIdConnectPostConfigureOptions>()
- });
+ return configuration;
+ }
}
}
@@ -98,14 +169,11 @@ public override void ConfigureServices(IServiceCollection services)
services.TryAddSingleton();
services.AddDataMigration();
- // Note: the following services are registered using TryAddEnumerable to prevent duplicate registrations.
- services.TryAddEnumerable(new[]
- {
- ServiceDescriptor.Scoped(),
- ServiceDescriptor.Scoped, OpenIdServerSettingsDisplayDriver>(),
- ServiceDescriptor.Singleton()
- });
+ // Note: the following services are registered using TryAddEnumerable to prevent duplicate registrations.
+ services.TryAddEnumerable(ServiceDescriptor.Scoped());
+ services.TryAddEnumerable(ServiceDescriptor.Scoped, OpenIdServerSettingsDisplayDriver>());
+ services.TryAddEnumerable(ServiceDescriptor.Singleton());
services.AddRecipeExecutionStep()
.AddRecipeExecutionStep()
@@ -118,13 +186,10 @@ public override void ConfigureServices(IServiceCollection services)
// To prevent that, the initializer is manually removed from the services collection of the tenant.
services.RemoveAll, OpenIddictServerAspNetCoreConfiguration>();
- services.TryAddEnumerable(new[]
- {
- ServiceDescriptor.Singleton, OpenIdServerConfiguration>(),
- ServiceDescriptor.Singleton, OpenIdServerConfiguration>(),
- ServiceDescriptor.Singleton, OpenIdServerConfiguration>(),
- ServiceDescriptor.Singleton, OpenIdServerConfiguration>()
- });
+ services.TryAddEnumerable(ServiceDescriptor.Singleton, OpenIdServerConfiguration>());
+ services.TryAddEnumerable(ServiceDescriptor.Singleton, OpenIdServerConfiguration>());
+ services.TryAddEnumerable(ServiceDescriptor.Singleton, OpenIdServerConfiguration>());
+ services.TryAddEnumerable(ServiceDescriptor.Singleton, OpenIdServerConfiguration>());
}
public override async ValueTask ConfigureAsync(IApplicationBuilder app, IEndpointRouteBuilder routes, IServiceProvider serviceProvider)
@@ -217,10 +282,7 @@ public override void ConfigureServices(IServiceCollection services)
services.TryAddSingleton();
// Note: the following services are registered using TryAddEnumerable to prevent duplicate registrations.
- services.TryAddEnumerable(new[]
- {
- ServiceDescriptor.Scoped, OpenIdValidationSettingsDisplayDriver>(),
- });
+ services.TryAddEnumerable(ServiceDescriptor.Scoped, OpenIdValidationSettingsDisplayDriver>());
services.AddRecipeExecutionStep();
@@ -231,13 +293,10 @@ public override void ConfigureServices(IServiceCollection services)
// To prevent that, the initializer is manually removed from the services collection of the tenant.
services.RemoveAll, OpenIddictValidationAspNetCoreConfiguration>();
- services.TryAddEnumerable(new[]
- {
- ServiceDescriptor.Singleton, OpenIdValidationConfiguration>(),
- ServiceDescriptor.Singleton, OpenIdValidationConfiguration>(),
- ServiceDescriptor.Singleton, OpenIdValidationConfiguration>(),
- ServiceDescriptor.Singleton, OpenIdValidationConfiguration>()
- });
+ services.TryAddEnumerable(ServiceDescriptor.Singleton, OpenIdValidationConfiguration>());
+ services.TryAddEnumerable(ServiceDescriptor.Singleton, OpenIdValidationConfiguration>());
+ services.TryAddEnumerable(ServiceDescriptor.Singleton, OpenIdValidationConfiguration>());
+ services.TryAddEnumerable(ServiceDescriptor.Singleton, OpenIdValidationConfiguration>());
}
}
diff --git a/src/OrchardCore.Modules/OrchardCore.OpenId/Views/Access/Error.cshtml b/src/OrchardCore.Modules/OrchardCore.OpenId/Views/Shared/Error.cshtml
similarity index 100%
rename from src/OrchardCore.Modules/OrchardCore.OpenId/Views/Access/Error.cshtml
rename to src/OrchardCore.Modules/OrchardCore.OpenId/Views/Shared/Error.cshtml