oqtane.framework/Oqtane.Server/Extensions/OqtaneSiteAuthenticationBuilderExtensions.cs
2022-03-19 13:42:19 -04:00

310 lines
15 KiB
C#

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;
using Oqtane.Shared;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity;
using Oqtane.Repository;
using System.IO;
using System.Collections.Generic;
using Oqtane.Security;
using System.Net;
using Microsoft.AspNetCore.Http;
namespace Oqtane.Extensions
{
public static class OqtaneSiteAuthenticationBuilderExtensions
{
public static OqtaneSiteOptionsBuilder<TAlias> WithSiteAuthentication<TAlias>(
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()
{
// site OpenIdConnect options
builder.AddSiteOptions<OpenIdConnectOptions>((options, alias) =>
{
// 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"); // 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.GetValue("OpenIdConnectOptions:Authority", "") != "")
{
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
}
});
return builder;
}
private static async Task OnTokenValidated(TokenValidatedContext context)
{
var email = context.Principal.FindFirstValue(ClaimTypes.Email);
var providerKey = context.Principal.FindFirstValue(ClaimTypes.NameIdentifier);
var loginProvider = context.HttpContext.GetAlias().SiteSettings["OpenIdConnectOptions:Authority"];
var _logger = context.HttpContext.RequestServices.GetRequiredService<ILogManager>();
if (email != null)
{
var _identityUserManager = context.HttpContext.RequestServices.GetRequiredService<UserManager<IdentityUser>>();
var _users = context.HttpContext.RequestServices.GetRequiredService<IUserRepository>();
var _userRoles = context.HttpContext.RequestServices.GetRequiredService<IUserRoleRepository>();
User user = null;
var identityuser = await _identityUserManager.FindByEmailAsync(email);
if (identityuser == null)
{
identityuser = new IdentityUser();
identityuser.UserName = email;
identityuser.Email = email;
identityuser.EmailConfirmed = true;
var result = await _identityUserManager.CreateAsync(identityuser, DateTime.UtcNow.ToString("yyyy-MMM-dd-HH-mm-ss"));
if (result.Succeeded)
{
// add user login
await _identityUserManager.AddLoginAsync(identityuser, new UserLoginInfo(loginProvider, providerKey, ""));
user = new User();
user.SiteId = context.HttpContext.GetAlias().SiteId;
user.Username = email;
user.DisplayName = email;
user.Email = email;
user.LastLoginOn = null;
user.LastIPAddress = "";
user = _users.AddUser(user);
// add folder for user
var _folders = context.HttpContext.RequestServices.GetRequiredService<IFolderRepository>();
Folder folder = _folders.GetFolder(user.SiteId, Utilities.PathCombine("Users", Path.DirectorySeparatorChar.ToString()));
if (folder != null)
{
_folders.AddFolder(new Folder
{
SiteId = folder.SiteId,
ParentId = folder.FolderId,
Name = "My Folder",
Type = FolderTypes.Private,
Path = Utilities.PathCombine(folder.Path, user.UserId.ToString(), Path.DirectorySeparatorChar.ToString()),
Order = 1,
ImageSizes = "",
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()
});
}
// add auto assigned roles to user for site
var _roles = context.HttpContext.RequestServices.GetRequiredService<IRoleRepository>();
List<Role> roles = _roles.GetRoles(user.SiteId).Where(item => item.IsAutoAssigned).ToList();
foreach (Role role in roles)
{
UserRole userrole = new UserRole();
userrole.UserId = user.UserId;
userrole.RoleId = role.RoleId;
userrole.EffectiveDate = null;
userrole.ExpiryDate = null;
_userRoles.AddUserRole(userrole);
}
}
}
else
{
var logins = await _identityUserManager.GetLoginsAsync(identityuser);
var login = logins.FirstOrDefault(item => item.LoginProvider == loginProvider);
if (login != null)
{
if (login.ProviderKey == providerKey)
{
user = _users.GetUser(identityuser.UserName);
}
else
{
// provider keys do not match
_logger.Log(LogLevel.Error, nameof(OqtaneSiteAuthenticationBuilderExtensions), Enums.LogFunction.Security, "OpenId Connect Provider Key Does Not Match For User {Email}", email);
}
}
else
{
// add user login
await _identityUserManager.AddLoginAsync(identityuser, new UserLoginInfo(loginProvider, providerKey, identityuser.UserName));
user = _users.GetUser(identityuser.UserName);
}
}
// add claims to principal
if (user != null)
{
// update user
user.LastLoginOn = DateTime.UtcNow;
user.LastIPAddress = context.HttpContext.Connection.RemoteIpAddress.ToString();
_users.UpdateUser(user);
_logger.Log(LogLevel.Information, nameof(OqtaneSiteAuthenticationBuilderExtensions), Enums.LogFunction.Security, "User Login Successful {Username}", user.Username);
var principal = (ClaimsIdentity)context.Principal.Identity;
// remove the name claim if it exists in the principal
var nameclaim = principal.Claims.FirstOrDefault(item => item.Type == ClaimTypes.Name);
if (nameclaim != null)
{
principal.RemoveClaim(nameclaim);
}
// add Oqtane claims
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 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));
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;
}
}
}