More improvements to OIDC support
This commit is contained in:
@ -8,6 +8,9 @@ using Oqtane.Enums;
|
||||
using Oqtane.Infrastructure;
|
||||
using Oqtane.Repository;
|
||||
using System.Net;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
|
||||
namespace Oqtane.Controllers
|
||||
{
|
||||
@ -20,14 +23,16 @@ namespace Oqtane.Controllers
|
||||
private readonly ISyncManager _syncManager;
|
||||
private readonly ILogManager _logger;
|
||||
private readonly Alias _alias;
|
||||
private readonly IOptionsMonitorCache<OpenIdConnectOptions> _optionsMonitorCache;
|
||||
private readonly string _visitorCookie;
|
||||
|
||||
public SettingController(ISettingRepository settings, IPageModuleRepository pageModules, IUserPermissions userPermissions, ITenantManager tenantManager, ISyncManager syncManager, ILogManager logger)
|
||||
public SettingController(ISettingRepository settings, IPageModuleRepository pageModules, IUserPermissions userPermissions, ITenantManager tenantManager, ISyncManager syncManager, IOptionsMonitorCache<OpenIdConnectOptions> optionsMonitorCache, ILogManager logger)
|
||||
{
|
||||
_settings = settings;
|
||||
_pageModules = pageModules;
|
||||
_userPermissions = userPermissions;
|
||||
_syncManager = syncManager;
|
||||
_optionsMonitorCache = optionsMonitorCache;
|
||||
_logger = logger;
|
||||
_alias = tenantManager.GetAlias();
|
||||
_visitorCookie = "APP_VISITOR_" + _alias.SiteId.ToString();
|
||||
@ -131,6 +136,15 @@ namespace Oqtane.Controllers
|
||||
}
|
||||
}
|
||||
|
||||
// DELETE api/<controller>/clear
|
||||
[HttpDelete("clear/{id}")]
|
||||
[Authorize(Roles = RoleNames.Admin)]
|
||||
public void Clear(int id)
|
||||
{
|
||||
_optionsMonitorCache.Clear();
|
||||
_logger.Log(LogLevel.Information, this, LogFunction.Other, "Site Options Cache Cleared");
|
||||
}
|
||||
|
||||
private bool IsAuthorized(string entityName, int entityId, string permissionName)
|
||||
{
|
||||
bool authorized = false;
|
||||
|
19
Oqtane.Server/Extensions/DictionaryExtensions.cs
Normal file
19
Oqtane.Server/Extensions/DictionaryExtensions.cs
Normal file
@ -0,0 +1,19 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Oqtane.Extensions
|
||||
{
|
||||
public static class DictionaryExtensions
|
||||
{
|
||||
public static TValue GetValue<TKey, TValue>(this Dictionary<TKey, TValue> dictionary, TKey key, TValue defaultValue, bool nullOrEmptyValueIsValid = false)
|
||||
{
|
||||
if (dictionary != null && key != null && dictionary.ContainsKey(key))
|
||||
{
|
||||
if (nullOrEmptyValueIsValid || (dictionary[key] != null && !string.IsNullOrEmpty(dictionary[key].ToString())))
|
||||
{
|
||||
return dictionary[key];
|
||||
}
|
||||
}
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
}
|
@ -15,6 +15,8 @@ using Oqtane.Repository;
|
||||
using System.IO;
|
||||
using System.Collections.Generic;
|
||||
using Oqtane.Security;
|
||||
using System.Net;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace Oqtane.Extensions
|
||||
{
|
||||
@ -47,38 +49,41 @@ namespace Oqtane.Extensions
|
||||
// site OpenIdConnect options
|
||||
builder.AddSiteOptions<OpenIdConnectOptions>((options, alias) =>
|
||||
{
|
||||
if (alias.SiteSettings.ContainsKey("OpenIdConnectOptions:Authority"))
|
||||
{
|
||||
options.Authority = alias.SiteSettings["OpenIdConnectOptions:Authority"];
|
||||
}
|
||||
if (alias.SiteSettings.ContainsKey("OpenIdConnectOptions:ClientId"))
|
||||
{
|
||||
options.ClientId = alias.SiteSettings["OpenIdConnectOptions:ClientId"];
|
||||
}
|
||||
if (alias.SiteSettings.ContainsKey("OpenIdConnectOptions:ClientSecret"))
|
||||
{
|
||||
options.ClientSecret = alias.SiteSettings["OpenIdConnectOptions:ClientSecret"];
|
||||
}
|
||||
|
||||
// default options
|
||||
options.SignInScheme = Constants.AuthenticationScheme; // identity cookie
|
||||
options.RequireHttpsMetadata = true;
|
||||
options.SaveTokens = true;
|
||||
options.GetClaimsFromUserInfoEndpoint = true;
|
||||
options.CallbackPath = string.IsNullOrEmpty(alias.Path) ? "/signin-oidc" : "/" + alias.Path + "/signin-oidc";
|
||||
options.ResponseType = OpenIdConnectResponseType.Code; // authorization code flow
|
||||
options.ResponseMode = OpenIdConnectResponseMode.FormPost; // recommended as most secure
|
||||
options.UsePkce = true;
|
||||
options.Scope.Add("openid"); // core claims
|
||||
options.Scope.Add("profile"); // name claims
|
||||
options.Scope.Add("email"); // email claim
|
||||
//options.Scope.Add("offline_access"); // get refresh token
|
||||
options.SaveTokens = true;
|
||||
options.GetClaimsFromUserInfoEndpoint = true;
|
||||
options.CallbackPath = string.IsNullOrEmpty(alias.Path) ? "/signin-oidc" : "/" + alias.Path + "/signin-oidc";
|
||||
options.ResponseType = OpenIdConnectResponseType.Code;
|
||||
//options.Scope.Add("offline_access"); // refresh token
|
||||
|
||||
// cookie config is required to avoid Correlation Failed errors
|
||||
options.NonceCookie.SameSite = SameSiteMode.Unspecified;
|
||||
options.CorrelationCookie.SameSite = SameSiteMode.Unspecified;
|
||||
|
||||
// site options
|
||||
options.Authority = alias.SiteSettings.GetValue("OpenIdConnectOptions:Authority", options.Authority);
|
||||
options.ClientId = alias.SiteSettings.GetValue("OpenIdConnectOptions:ClientId", options.ClientId);
|
||||
options.ClientSecret = alias.SiteSettings.GetValue("OpenIdConnectOptions:ClientSecret", options.ClientSecret);
|
||||
options.MetadataAddress = alias.SiteSettings.GetValue("OpenIdConnectOptions:MetadataAddress", options.MetadataAddress);
|
||||
|
||||
// openid connect events
|
||||
options.Events.OnTokenValidated = OnTokenValidated;
|
||||
options.Events.OnRedirectToIdentityProvider = OnRedirectToIdentityProvider;
|
||||
options.Events.OnRedirectToIdentityProviderForSignOut = OnRedirectToIdentityProviderForSignOut;
|
||||
options.Events.OnRemoteFailure = OnRemoteFailure;
|
||||
});
|
||||
|
||||
// site ChallengeScheme options
|
||||
builder.AddSiteOptions<AuthenticationOptions>((options, alias) =>
|
||||
{
|
||||
if (alias.SiteSettings.ContainsKey("OpenIdConnectOptions:Authority") && !string.IsNullOrEmpty(alias.SiteSettings["OpenIdConnectOptions:Authority"]))
|
||||
if (alias.SiteSettings.GetValue("OpenIdConnectOptions:Authority", "") != "")
|
||||
{
|
||||
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
|
||||
}
|
||||
@ -175,7 +180,7 @@ namespace Oqtane.Extensions
|
||||
else
|
||||
{
|
||||
// provider keys do not match
|
||||
_logger.Log(LogLevel.Error, nameof(OqtaneSiteAuthenticationBuilderExtensions), Enums.LogFunction.Security, "OpenId Connect Server Provider Key Does Not Match For User {Email}", email);
|
||||
_logger.Log(LogLevel.Error, nameof(OqtaneSiteAuthenticationBuilderExtensions), Enums.LogFunction.Security, "OpenId Connect Provider Key Does Not Match For User {Email}", email);
|
||||
}
|
||||
}
|
||||
else
|
||||
@ -208,14 +213,53 @@ namespace Oqtane.Extensions
|
||||
List<UserRole> userroles = _userRoles.GetUserRoles(user.UserId, context.HttpContext.GetAlias().SiteId).ToList();
|
||||
var identity = UserSecurity.CreateClaimsIdentity(context.HttpContext.GetAlias(), user, userroles);
|
||||
principal.AddClaims(identity.Claims);
|
||||
|
||||
// add provider
|
||||
principal.AddClaim(new Claim("Provider", context.HttpContext.GetAlias().SiteSettings["OpenIdConnectOptions:Authority"]));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.Log(LogLevel.Information, nameof(OqtaneSiteAuthenticationBuilderExtensions), Enums.LogFunction.Security, "OpenId Connect Server Did Not Return An Email Claim");
|
||||
_logger.Log(LogLevel.Information, nameof(OqtaneSiteAuthenticationBuilderExtensions), Enums.LogFunction.Security, "OpenId Connect Provider Did Not Return An Email Claim");
|
||||
}
|
||||
}
|
||||
|
||||
private static Task OnRedirectToIdentityProvider(RedirectContext context)
|
||||
{
|
||||
//context.ProtocolMessage.SetParameter("key", "value");
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private static Task OnRedirectToIdentityProviderForSignOut(RedirectContext context)
|
||||
{
|
||||
var logoutUrl = context.HttpContext.GetAlias().SiteSettings.GetValue("OpenIdConnectOptions:LogoutUrl", "");
|
||||
if (logoutUrl != "")
|
||||
{
|
||||
var postLogoutUri = context.Properties.RedirectUri;
|
||||
if (!string.IsNullOrEmpty(postLogoutUri))
|
||||
{
|
||||
if (postLogoutUri.StartsWith("/"))
|
||||
{
|
||||
var request = context.Request;
|
||||
postLogoutUri = request.Scheme + "://" + request.Host + request.PathBase + postLogoutUri;
|
||||
}
|
||||
logoutUrl += $"&returnTo={Uri.EscapeDataString(postLogoutUri)}";
|
||||
}
|
||||
context.Response.Redirect(logoutUrl);
|
||||
context.HandleResponse();
|
||||
}
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private static Task OnRemoteFailure(RemoteFailureContext context)
|
||||
{
|
||||
var _logger = context.HttpContext.RequestServices.GetRequiredService<ILogManager>();
|
||||
_logger.Log(LogLevel.Error, nameof(OqtaneSiteAuthenticationBuilderExtensions), Enums.LogFunction.Security, "OpenId Connect Remote Failure {Error}", context.Failure.Message);
|
||||
context.Response.Redirect(context.Properties.RedirectUri);
|
||||
context.HandleResponse();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public static bool DecorateService<TService, TImpl>(this IServiceCollection services, params object[] parameters)
|
||||
{
|
||||
var existingService = services.SingleOrDefault(s => s.ServiceType == typeof(TService));
|
||||
|
@ -15,40 +15,16 @@ namespace Oqtane.Extensions
|
||||
builder.AddSiteOptions<IdentityOptions>((options, alias) =>
|
||||
{
|
||||
// password options
|
||||
if (alias.SiteSettings.ContainsKey("IdentityOptions:Password:RequiredLength"))
|
||||
{
|
||||
options.Password.RequiredLength = int.Parse(alias.SiteSettings["IdentityOptions:Password:RequiredLength"]);
|
||||
}
|
||||
if (alias.SiteSettings.ContainsKey("IdentityOptions:Password:RequiredUniqueChars"))
|
||||
{
|
||||
options.Password.RequiredUniqueChars = int.Parse(alias.SiteSettings["IdentityOptions:Password:RequiredUniqueChars"]);
|
||||
}
|
||||
if (alias.SiteSettings.ContainsKey("IdentityOptions:Password:RequireDigit"))
|
||||
{
|
||||
options.Password.RequireDigit = bool.Parse(alias.SiteSettings["IdentityOptions:Password:RequireDigit"]);
|
||||
}
|
||||
if (alias.SiteSettings.ContainsKey("IdentityOptions:Password:RequireUppercase"))
|
||||
{
|
||||
options.Password.RequireUppercase = bool.Parse(alias.SiteSettings["IdentityOptions:Password:RequireUppercase"]);
|
||||
}
|
||||
if (alias.SiteSettings.ContainsKey("IdentityOptions:Password:RequireLowercase"))
|
||||
{
|
||||
options.Password.RequireLowercase = bool.Parse(alias.SiteSettings["IdentityOptions:Password:RequireLowercase"]);
|
||||
}
|
||||
if (alias.SiteSettings.ContainsKey("IdentityOptions:Password:RequireNonAlphanumeric"))
|
||||
{
|
||||
options.Password.RequireNonAlphanumeric = bool.Parse(alias.SiteSettings["IdentityOptions:Password:RequireNonAlphanumeric"]);
|
||||
}
|
||||
options.Password.RequiredLength = int.Parse(alias.SiteSettings.GetValue("IdentityOptions:Password:RequiredLength", options.Password.RequiredLength.ToString()));
|
||||
options.Password.RequiredUniqueChars = int.Parse(alias.SiteSettings.GetValue("IdentityOptions:Password:RequiredUniqueChars", options.Password.RequiredUniqueChars.ToString()));
|
||||
options.Password.RequireDigit = bool.Parse(alias.SiteSettings.GetValue("IdentityOptions:Password:RequireDigit", options.Password.RequireDigit.ToString()));
|
||||
options.Password.RequireUppercase = bool.Parse(alias.SiteSettings.GetValue("IdentityOptions:Password:RequireUppercase", options.Password.RequireUppercase.ToString()));
|
||||
options.Password.RequireLowercase = bool.Parse(alias.SiteSettings.GetValue("IdentityOptions:Password:RequireLowercase", options.Password.RequireLowercase.ToString()));
|
||||
options.Password.RequireNonAlphanumeric = bool.Parse(alias.SiteSettings.GetValue("IdentityOptions:Password:RequireNonAlphanumeric", options.Password.RequireNonAlphanumeric.ToString()));
|
||||
|
||||
// lockout options
|
||||
if (alias.SiteSettings.ContainsKey("IdentityOptions:Password:MaxFailedAccessAttempts"))
|
||||
{
|
||||
options.Lockout.MaxFailedAccessAttempts = int.Parse(alias.SiteSettings["IdentityOptions:Password:MaxFailedAccessAttempts"]);
|
||||
}
|
||||
if (alias.SiteSettings.ContainsKey("IdentityOptions:Password:DefaultLockoutTimeSpan"))
|
||||
{
|
||||
options.Lockout.DefaultLockoutTimeSpan = TimeSpan.Parse(alias.SiteSettings["IdentityOptions:Password:DefaultLockoutTimeSpan"]);
|
||||
}
|
||||
options.Lockout.MaxFailedAccessAttempts = int.Parse(alias.SiteSettings.GetValue("IdentityOptions:Password:MaxFailedAccessAttempts", options.Lockout.MaxFailedAccessAttempts.ToString()));
|
||||
options.Lockout.DefaultLockoutTimeSpan = TimeSpan.Parse(alias.SiteSettings.GetValue("IdentityOptions:Password:DefaultLockoutTimeSpan", options.Lockout.DefaultLockoutTimeSpan.ToString()));
|
||||
});
|
||||
|
||||
return builder;
|
||||
|
@ -1,6 +1,7 @@
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Oqtane.Repository;
|
||||
using Oqtane.Shared;
|
||||
|
||||
@ -27,10 +28,15 @@ namespace Oqtane.Infrastructure
|
||||
|
||||
if (alias != null)
|
||||
{
|
||||
// get site settings and store alias in HttpContext
|
||||
var settingRepository = context.RequestServices.GetService(typeof(ISettingRepository)) as ISettingRepository;
|
||||
alias.SiteSettings = settingRepository.GetSettings(EntityNames.Site)
|
||||
.ToDictionary(setting => setting.SettingName, setting => setting.SettingValue);
|
||||
// get site settings
|
||||
var cache = context.RequestServices.GetService(typeof(IMemoryCache)) as IMemoryCache;
|
||||
alias.SiteSettings = cache.GetOrCreate("sitesettings:" + alias.SiteKey, entry =>
|
||||
{
|
||||
var settingRepository = context.RequestServices.GetService(typeof(ISettingRepository)) as ISettingRepository;
|
||||
return settingRepository.GetSettings(EntityNames.Site)
|
||||
.ToDictionary(setting => setting.SettingName, setting => setting.SettingValue);
|
||||
});
|
||||
// save alias in HttpContext
|
||||
context.Items.Add(Constants.HttpContextAliasKey, alias);
|
||||
|
||||
// rewrite path by removing alias path prefix from api and pages requests (for consistent routing)
|
||||
@ -42,9 +48,7 @@ namespace Oqtane.Infrastructure
|
||||
context.Request.Path = path.Replace("/" + alias.Path, "");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// continue processing
|
||||
|
@ -1,32 +1,37 @@
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
using Oqtane.Extensions;
|
||||
using Oqtane.Shared;
|
||||
|
||||
namespace Oqtane.Pages
|
||||
{
|
||||
[AllowAnonymous]
|
||||
[Authorize]
|
||||
public class LogoutModel : PageModel
|
||||
{
|
||||
public async Task<IActionResult> OnGetAsync(string returnurl)
|
||||
{
|
||||
if (HttpContext.User.Identity.IsAuthenticated)
|
||||
{
|
||||
await HttpContext.SignOutAsync(Constants.AuthenticationScheme);
|
||||
}
|
||||
await HttpContext.SignOutAsync(Constants.AuthenticationScheme);
|
||||
|
||||
if (returnurl == null)
|
||||
{
|
||||
returnurl = "";
|
||||
}
|
||||
if (!returnurl.StartsWith("/"))
|
||||
{
|
||||
returnurl = "/" + returnurl;
|
||||
}
|
||||
returnurl = (returnurl == null) ? "/" : returnurl;
|
||||
returnurl = (!returnurl.StartsWith("/")) ? "/" + returnurl : returnurl;
|
||||
|
||||
return LocalRedirect(Url.Content("~" + returnurl));
|
||||
var provider = HttpContext.User.Claims.FirstOrDefault(item => item.Type == "Provider");
|
||||
var authority = HttpContext.GetAlias().SiteSettings.GetValue("OpenIdConnectOptions:Authority", "");
|
||||
var logoutUrl = HttpContext.GetAlias().SiteSettings.GetValue("OpenIdConnectOptions:LogoutUrl", "");
|
||||
if (provider != null && provider.Value == authority && logoutUrl != "")
|
||||
{
|
||||
return new SignOutResult(OpenIdConnectDefaults.AuthenticationScheme,
|
||||
new AuthenticationProperties { RedirectUri = returnurl });
|
||||
}
|
||||
else
|
||||
{
|
||||
return LocalRedirect(Url.Content("~" + returnurl));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -9,7 +9,11 @@ namespace Oqtane.Pages
|
||||
{
|
||||
public IActionResult OnGetAsync(string returnurl)
|
||||
{
|
||||
return new ChallengeResult(OpenIdConnectDefaults.AuthenticationScheme, new AuthenticationProperties { RedirectUri = !string.IsNullOrEmpty(returnurl) ? returnurl : "/" });
|
||||
returnurl = (returnurl == null) ? "/" : returnurl;
|
||||
returnurl = (!returnurl.StartsWith("/")) ? "/" + returnurl : returnurl;
|
||||
|
||||
return new ChallengeResult(OpenIdConnectDefaults.AuthenticationScheme,
|
||||
new AuthenticationProperties { RedirectUri = returnurl });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Oqtane.Models;
|
||||
using Oqtane.Shared;
|
||||
|
||||
@ -10,11 +11,15 @@ namespace Oqtane.Repository
|
||||
{
|
||||
private TenantDBContext _tenant;
|
||||
private MasterDBContext _master;
|
||||
private readonly SiteState _siteState;
|
||||
private readonly IMemoryCache _cache;
|
||||
|
||||
public SettingRepository(TenantDBContext tenant, MasterDBContext master)
|
||||
public SettingRepository(TenantDBContext tenant, MasterDBContext master, SiteState siteState, IMemoryCache cache)
|
||||
{
|
||||
_tenant = tenant;
|
||||
_master = master;
|
||||
_siteState = siteState;
|
||||
_cache = cache;
|
||||
}
|
||||
|
||||
public IEnumerable<Setting> GetSettings(string entityName)
|
||||
@ -47,6 +52,7 @@ namespace Oqtane.Repository
|
||||
_tenant.Setting.Add(setting);
|
||||
_tenant.SaveChanges();
|
||||
}
|
||||
ManageCache(setting.EntityName);
|
||||
return setting;
|
||||
}
|
||||
|
||||
@ -62,6 +68,7 @@ namespace Oqtane.Repository
|
||||
_tenant.Entry(setting).State = EntityState.Modified;
|
||||
_tenant.SaveChanges();
|
||||
}
|
||||
ManageCache(setting.EntityName);
|
||||
return setting;
|
||||
}
|
||||
|
||||
@ -103,6 +110,7 @@ namespace Oqtane.Repository
|
||||
_tenant.Setting.Remove(setting);
|
||||
_tenant.SaveChanges();
|
||||
}
|
||||
ManageCache(entityName);
|
||||
}
|
||||
|
||||
public void DeleteSettings(string entityName, int entityId)
|
||||
@ -129,11 +137,20 @@ namespace Oqtane.Repository
|
||||
}
|
||||
_tenant.SaveChanges();
|
||||
}
|
||||
ManageCache(entityName);
|
||||
}
|
||||
|
||||
private bool IsMaster(string EntityName)
|
||||
{
|
||||
return (EntityName == EntityNames.ModuleDefinition || EntityName == EntityNames.Host);
|
||||
}
|
||||
|
||||
private void ManageCache(string EntityName)
|
||||
{
|
||||
if (EntityName == EntityNames.Site)
|
||||
{
|
||||
_cache.Remove("sitesettings:" + _siteState.Alias.SiteKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user