OIDC improvements
This commit is contained in:
@ -2,7 +2,6 @@ using System;
|
||||
using System.Linq;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
|
||||
using Oqtane.Infrastructure;
|
||||
using Oqtane.Models;
|
||||
@ -15,7 +14,6 @@ using Oqtane.Repository;
|
||||
using System.IO;
|
||||
using System.Collections.Generic;
|
||||
using Oqtane.Security;
|
||||
using System.Net;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace Oqtane.Extensions
|
||||
@ -26,22 +24,11 @@ namespace Oqtane.Extensions
|
||||
this OqtaneSiteOptionsBuilder<TAlias> builder)
|
||||
where TAlias : class, IAlias, new()
|
||||
{
|
||||
builder.WithSiteAuthenticationCore();
|
||||
builder.WithSiteAuthenticationOptions();
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
public static OqtaneSiteOptionsBuilder<TAlias> WithSiteAuthenticationCore<TAlias>(
|
||||
this OqtaneSiteOptionsBuilder<TAlias> builder)
|
||||
where TAlias : class, IAlias, new()
|
||||
{
|
||||
builder.Services.DecorateService<IAuthenticationService, SiteAuthenticationService<TAlias>>();
|
||||
builder.Services.Replace(ServiceDescriptor.Singleton<IAuthenticationSchemeProvider, SiteAuthenticationSchemeProvider>());
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
public static OqtaneSiteOptionsBuilder<TAlias> WithSiteAuthenticationOptions<TAlias>(
|
||||
this OqtaneSiteOptionsBuilder<TAlias> builder)
|
||||
where TAlias : class, IAlias, new()
|
||||
@ -75,8 +62,8 @@ namespace Oqtane.Extensions
|
||||
|
||||
// openid connect events
|
||||
options.Events.OnTokenValidated = OnTokenValidated;
|
||||
options.Events.OnRedirectToIdentityProvider = OnRedirectToIdentityProvider;
|
||||
options.Events.OnRedirectToIdentityProviderForSignOut = OnRedirectToIdentityProviderForSignOut;
|
||||
options.Events.OnAccessDenied = OnAccessDenied;
|
||||
options.Events.OnRemoteFailure = OnRemoteFailure;
|
||||
});
|
||||
|
||||
@ -97,6 +84,7 @@ namespace Oqtane.Extensions
|
||||
var email = context.Principal.FindFirstValue(ClaimTypes.Email);
|
||||
var providerKey = context.Principal.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||
var loginProvider = context.HttpContext.GetAlias().SiteSettings["OpenIdConnectOptions:Authority"];
|
||||
var alias = context.HttpContext.GetAlias();
|
||||
var _logger = context.HttpContext.RequestServices.GetRequiredService<ILogManager>();
|
||||
|
||||
if (email != null)
|
||||
@ -117,10 +105,10 @@ namespace Oqtane.Extensions
|
||||
if (result.Succeeded)
|
||||
{
|
||||
// add user login
|
||||
await _identityUserManager.AddLoginAsync(identityuser, new UserLoginInfo(loginProvider, providerKey, ""));
|
||||
await _identityUserManager.AddLoginAsync(identityuser, new UserLoginInfo(loginProvider, providerKey, email));
|
||||
|
||||
user = new User();
|
||||
user.SiteId = context.HttpContext.GetAlias().SiteId;
|
||||
user.SiteId = alias.SiteId;
|
||||
user.Username = email;
|
||||
user.DisplayName = email;
|
||||
user.Email = email;
|
||||
@ -145,11 +133,11 @@ namespace Oqtane.Extensions
|
||||
Capacity = Constants.UserFolderCapacity,
|
||||
IsSystem = true,
|
||||
Permissions = new List<Permission>
|
||||
{
|
||||
new Permission(PermissionNames.Browse, user.UserId, true),
|
||||
new Permission(PermissionNames.View, RoleNames.Everyone, true),
|
||||
new Permission(PermissionNames.Edit, user.UserId, true)
|
||||
}.EncodePermissions()
|
||||
{
|
||||
new Permission(PermissionNames.Browse, user.UserId, true),
|
||||
new Permission(PermissionNames.View, RoleNames.Everyone, true),
|
||||
new Permission(PermissionNames.Edit, user.UserId, true)
|
||||
}.EncodePermissions()
|
||||
});
|
||||
}
|
||||
|
||||
@ -210,8 +198,8 @@ namespace Oqtane.Extensions
|
||||
}
|
||||
|
||||
// add Oqtane claims
|
||||
List<UserRole> userroles = _userRoles.GetUserRoles(user.UserId, context.HttpContext.GetAlias().SiteId).ToList();
|
||||
var identity = UserSecurity.CreateClaimsIdentity(context.HttpContext.GetAlias(), user, userroles);
|
||||
List<UserRole> userroles = _userRoles.GetUserRoles(user.UserId, user.SiteId).ToList();
|
||||
var identity = UserSecurity.CreateClaimsIdentity(alias, user, userroles);
|
||||
principal.AddClaims(identity.Claims);
|
||||
|
||||
// add provider
|
||||
@ -224,12 +212,6 @@ namespace Oqtane.Extensions
|
||||
}
|
||||
}
|
||||
|
||||
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", "");
|
||||
@ -251,59 +233,25 @@ namespace Oqtane.Extensions
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private static Task OnRemoteFailure(RemoteFailureContext context)
|
||||
private static Task OnAccessDenied(AccessDeniedContext 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);
|
||||
_logger.Log(LogLevel.Information, nameof(OqtaneSiteAuthenticationBuilderExtensions), Enums.LogFunction.Security, "OpenId Connect Access Denied - User May Have Cancelled Their External Login Attempt");
|
||||
// redirect to login page
|
||||
var alias = context.HttpContext.GetAlias();
|
||||
context.Response.Redirect(alias.Path + "/login?returnurl=" + context.Properties.RedirectUri);
|
||||
context.HandleResponse();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public static bool DecorateService<TService, TImpl>(this IServiceCollection services, params object[] parameters)
|
||||
private static Task OnRemoteFailure(RemoteFailureContext context)
|
||||
{
|
||||
var existingService = services.SingleOrDefault(s => s.ServiceType == typeof(TService));
|
||||
if (existingService == null)
|
||||
return false;
|
||||
|
||||
var newService = new ServiceDescriptor(existingService.ServiceType,
|
||||
sp =>
|
||||
{
|
||||
TService inner = (TService)ActivatorUtilities.CreateInstance(sp, existingService.ImplementationType!);
|
||||
|
||||
var parameters2 = new object[parameters.Length + 1];
|
||||
Array.Copy(parameters, 0, parameters2, 1, parameters.Length);
|
||||
parameters2[0] = inner;
|
||||
|
||||
return ActivatorUtilities.CreateInstance<TImpl>(sp, parameters2)!;
|
||||
},
|
||||
existingService.Lifetime);
|
||||
|
||||
if (existingService.ImplementationInstance != null)
|
||||
{
|
||||
newService = new ServiceDescriptor(existingService.ServiceType,
|
||||
sp =>
|
||||
{
|
||||
TService inner = (TService)existingService.ImplementationInstance;
|
||||
return ActivatorUtilities.CreateInstance<TImpl>(sp, inner, parameters)!;
|
||||
},
|
||||
existingService.Lifetime);
|
||||
}
|
||||
else if (existingService.ImplementationFactory != null)
|
||||
{
|
||||
newService = new ServiceDescriptor(existingService.ServiceType,
|
||||
sp =>
|
||||
{
|
||||
TService inner = (TService)existingService.ImplementationFactory(sp);
|
||||
return ActivatorUtilities.CreateInstance<TImpl>(sp, inner, parameters)!;
|
||||
},
|
||||
existingService.Lifetime);
|
||||
}
|
||||
|
||||
services.Remove(existingService);
|
||||
services.Add(newService);
|
||||
|
||||
return true;
|
||||
var _logger = context.HttpContext.RequestServices.GetRequiredService<ILogManager>();
|
||||
_logger.Log(LogLevel.Error, nameof(OqtaneSiteAuthenticationBuilderExtensions), Enums.LogFunction.Security, "OpenId Connect Remote Failure - {Error}", context.Failure.Message);
|
||||
// redirect to original page
|
||||
context.Response.Redirect(context.Properties.RedirectUri);
|
||||
context.HandleResponse();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,112 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Oqtane.Infrastructure
|
||||
{
|
||||
internal class SiteAuthenticationSchemeProvider : IAuthenticationSchemeProvider
|
||||
{
|
||||
public SiteAuthenticationSchemeProvider(IOptions<AuthenticationOptions> options)
|
||||
: this(options, new Dictionary<string, AuthenticationScheme>(StringComparer.Ordinal))
|
||||
{
|
||||
}
|
||||
|
||||
public SiteAuthenticationSchemeProvider(IOptions<AuthenticationOptions> options, IDictionary<string, AuthenticationScheme> schemes)
|
||||
{
|
||||
_optionsProvider = options;
|
||||
|
||||
_schemes = schemes ?? throw new ArgumentNullException(nameof(schemes));
|
||||
_requestHandlers = new List<AuthenticationScheme>();
|
||||
|
||||
foreach (var builder in _optionsProvider.Value.Schemes)
|
||||
{
|
||||
var scheme = builder.Build();
|
||||
AddScheme(scheme);
|
||||
}
|
||||
}
|
||||
|
||||
private readonly IOptions<AuthenticationOptions> _optionsProvider;
|
||||
private readonly object _lock = new object();
|
||||
|
||||
private readonly IDictionary<string, AuthenticationScheme> _schemes;
|
||||
private readonly List<AuthenticationScheme> _requestHandlers;
|
||||
|
||||
private Task<AuthenticationScheme> GetDefaultSchemeAsync()
|
||||
=> _optionsProvider.Value.DefaultScheme != null
|
||||
? GetSchemeAsync(_optionsProvider.Value.DefaultScheme)
|
||||
: Task.FromResult<AuthenticationScheme>(null);
|
||||
|
||||
public virtual Task<AuthenticationScheme> GetDefaultAuthenticateSchemeAsync()
|
||||
=> _optionsProvider.Value.DefaultAuthenticateScheme != null
|
||||
? GetSchemeAsync(_optionsProvider.Value.DefaultAuthenticateScheme)
|
||||
: GetDefaultSchemeAsync();
|
||||
|
||||
public virtual Task<AuthenticationScheme> GetDefaultChallengeSchemeAsync()
|
||||
=> _optionsProvider.Value.DefaultChallengeScheme != null
|
||||
? GetSchemeAsync(_optionsProvider.Value.DefaultChallengeScheme)
|
||||
: GetDefaultSchemeAsync();
|
||||
|
||||
public virtual Task<AuthenticationScheme> GetDefaultForbidSchemeAsync()
|
||||
=> _optionsProvider.Value.DefaultForbidScheme != null
|
||||
? GetSchemeAsync(_optionsProvider.Value.DefaultForbidScheme)
|
||||
: GetDefaultChallengeSchemeAsync();
|
||||
|
||||
public virtual Task<AuthenticationScheme> GetDefaultSignInSchemeAsync()
|
||||
=> _optionsProvider.Value.DefaultSignInScheme != null
|
||||
? GetSchemeAsync(_optionsProvider.Value.DefaultSignInScheme)
|
||||
: GetDefaultSchemeAsync();
|
||||
|
||||
public virtual Task<AuthenticationScheme> GetDefaultSignOutSchemeAsync()
|
||||
=> _optionsProvider.Value.DefaultSignOutScheme != null
|
||||
? GetSchemeAsync(_optionsProvider.Value.DefaultSignOutScheme)
|
||||
: GetDefaultSignInSchemeAsync();
|
||||
|
||||
public virtual Task<AuthenticationScheme> GetSchemeAsync(string name)
|
||||
=> Task.FromResult(_schemes.ContainsKey(name) ? _schemes[name] : null);
|
||||
|
||||
public virtual Task<IEnumerable<AuthenticationScheme>> GetRequestHandlerSchemesAsync()
|
||||
=> Task.FromResult<IEnumerable<AuthenticationScheme>>(_requestHandlers);
|
||||
|
||||
public virtual void AddScheme(AuthenticationScheme scheme)
|
||||
{
|
||||
if (_schemes.ContainsKey(scheme.Name))
|
||||
{
|
||||
throw new InvalidOperationException("Scheme already exists: " + scheme.Name);
|
||||
}
|
||||
lock (_lock)
|
||||
{
|
||||
if (_schemes.ContainsKey(scheme.Name))
|
||||
{
|
||||
throw new InvalidOperationException("Scheme already exists: " + scheme.Name);
|
||||
}
|
||||
if (typeof(IAuthenticationRequestHandler).IsAssignableFrom(scheme.HandlerType))
|
||||
{
|
||||
_requestHandlers.Add(scheme);
|
||||
}
|
||||
_schemes[scheme.Name] = scheme;
|
||||
}
|
||||
}
|
||||
|
||||
public virtual void RemoveScheme(string name)
|
||||
{
|
||||
if (!_schemes.ContainsKey(name))
|
||||
{
|
||||
return;
|
||||
}
|
||||
lock (_lock)
|
||||
{
|
||||
if (_schemes.ContainsKey(name))
|
||||
{
|
||||
var scheme = _schemes[name];
|
||||
_requestHandlers.Remove(scheme);
|
||||
_schemes.Remove(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public virtual Task<IEnumerable<AuthenticationScheme>> GetAllSchemesAsync()
|
||||
=> Task.FromResult<IEnumerable<AuthenticationScheme>>(_schemes.Values);
|
||||
}
|
||||
}
|
@ -1,62 +0,0 @@
|
||||
using System.Security.Claims;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Oqtane.Extensions;
|
||||
using Oqtane.Models;
|
||||
using Oqtane.Shared;
|
||||
|
||||
namespace Oqtane.Infrastructure
|
||||
{
|
||||
internal class SiteAuthenticationService<TAlias> : IAuthenticationService
|
||||
where TAlias : class, IAlias, new()
|
||||
{
|
||||
private readonly IAuthenticationService _inner;
|
||||
|
||||
public SiteAuthenticationService(IAuthenticationService inner)
|
||||
{
|
||||
_inner = inner ?? throw new System.ArgumentNullException(nameof(inner));
|
||||
}
|
||||
|
||||
private static void AddTenantIdentifierToProperties(HttpContext context, ref AuthenticationProperties properties)
|
||||
{
|
||||
// add site identifier to the authentication properties so on the callback we can use it to set context
|
||||
var alias = context.GetAlias();
|
||||
if (alias != null)
|
||||
{
|
||||
properties ??= new AuthenticationProperties();
|
||||
if (!properties.Items.Keys.Contains(Constants.SiteToken))
|
||||
{
|
||||
properties.Items.Add(Constants.SiteToken, alias.SiteKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Task<AuthenticateResult> AuthenticateAsync(HttpContext context, string scheme)
|
||||
=> _inner.AuthenticateAsync(context, scheme);
|
||||
|
||||
public async Task ChallengeAsync(HttpContext context, string scheme, AuthenticationProperties properties)
|
||||
{
|
||||
AddTenantIdentifierToProperties(context, ref properties);
|
||||
await _inner.ChallengeAsync(context, scheme, properties);
|
||||
}
|
||||
|
||||
public async Task ForbidAsync(HttpContext context, string scheme, AuthenticationProperties properties)
|
||||
{
|
||||
AddTenantIdentifierToProperties(context, ref properties);
|
||||
await _inner.ForbidAsync(context, scheme, properties);
|
||||
}
|
||||
|
||||
public async Task SignInAsync(HttpContext context, string scheme, ClaimsPrincipal principal, AuthenticationProperties properties)
|
||||
{
|
||||
AddTenantIdentifierToProperties(context, ref properties);
|
||||
await _inner.SignInAsync(context, scheme, principal, properties);
|
||||
}
|
||||
|
||||
public async Task SignOutAsync(HttpContext context, string scheme, AuthenticationProperties properties)
|
||||
{
|
||||
AddTenantIdentifierToProperties(context, ref properties);
|
||||
await _inner.SignOutAsync(context, scheme, properties);
|
||||
}
|
||||
}
|
||||
}
|
@ -33,7 +33,7 @@ namespace Oqtane.Infrastructure
|
||||
alias.SiteSettings = cache.GetOrCreate("sitesettings:" + alias.SiteKey, entry =>
|
||||
{
|
||||
var settingRepository = context.RequestServices.GetService(typeof(ISettingRepository)) as ISettingRepository;
|
||||
return settingRepository.GetSettings(EntityNames.Site)
|
||||
return settingRepository.GetSettings(EntityNames.Site, alias.SiteId)
|
||||
.ToDictionary(setting => setting.SettingName, setting => setting.SettingValue);
|
||||
});
|
||||
// save alias in HttpContext
|
||||
|
@ -24,7 +24,7 @@ namespace Oqtane.Security
|
||||
if (alias != null)
|
||||
{
|
||||
// verify principal was authenticated for current tenant
|
||||
if (context.Principal.Claims.FirstOrDefault(item => item.Type == ClaimTypes.GroupSid)?.Value != alias.AliasId.ToString())
|
||||
if (context.Principal.Claims.FirstOrDefault(item => item.Type == ClaimTypes.GroupSid)?.Value != alias.SiteKey)
|
||||
{
|
||||
// tenant agnostic requests must be ignored
|
||||
string path = context.Request.Path.ToString().ToLower();
|
||||
|
Reference in New Issue
Block a user