diff --git a/Oqtane.Client/Modules/Admin/Login/Index.razor b/Oqtane.Client/Modules/Admin/Login/Index.razor index 234ebb0c..cd7bcd83 100644 --- a/Oqtane.Client/Modules/Admin/Login/Index.razor +++ b/Oqtane.Client/Modules/Admin/Login/Index.razor @@ -21,7 +21,7 @@
@if (_allowexternallogin) { - +

} @if (_allowsitelogin) @@ -95,12 +95,12 @@ { _togglepassword = Localizer["ShowPassword"]; - if (PageState.Site.Settings.ContainsKey("AllowSiteLogin") && !string.IsNullOrEmpty(PageState.Site.Settings["AllowSiteLogin"])) + if (PageState.Site.Settings.ContainsKey("ExternalLogin:AllowSiteLogin") && !string.IsNullOrEmpty(PageState.Site.Settings["ExternalLogin:AllowSiteLogin"])) { - _allowsitelogin = bool.Parse(PageState.Site.Settings["AllowSiteLogin"]); + _allowsitelogin = bool.Parse(PageState.Site.Settings["ExternalLogin:AllowSiteLogin"]); } - if (PageState.Site.Settings.ContainsKey("OpenIdConnectOptions:Provider") && !string.IsNullOrEmpty(PageState.Site.Settings["OpenIdConnectOptions:Provider"])) + if (PageState.Site.Settings.ContainsKey("ExternalLogin:ProviderType") && !string.IsNullOrEmpty(PageState.Site.Settings["ExternalLogin:ProviderType"])) { _allowexternallogin = true; } @@ -269,7 +269,7 @@ private void ExternalLogin() { - NavigationManager.NavigateTo(Utilities.TenantUrl(PageState.Alias, "/pages/oidc?returnurl=" + _returnUrl), true); + NavigationManager.NavigateTo(Utilities.TenantUrl(PageState.Alias, "/pages/external?returnurl=" + _returnUrl), true); } } diff --git a/Oqtane.Client/Modules/Admin/Users/Index.razor b/Oqtane.Client/Modules/Admin/Users/Index.razor index 29a7d9cf..1298bcfc 100644 --- a/Oqtane.Client/Modules/Admin/Users/Index.razor +++ b/Oqtane.Client/Modules/Admin/Users/Index.razor @@ -56,7 +56,7 @@ else
- +
-
+
@@ -131,62 +131,129 @@ else
- +
- -
-
-
- -
- -
-
-
- -
- -
-
-
- -
- -
-
-
- -
- -
-
-
- -
- -
-
-
- -
- -
-
-
- -
- -
-
-
- -
- + + +
-
+
+ @if (_providertype != "") + { +
+ +
+ +
+
+ } + @if (_providertype == "oidc") + { +
+ +
+ +
+
+
+ +
+ +
+
+ } + @if (_providertype == "oauth2") + { +
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+ } + @if (_providertype != "") + { +
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+ @if (_providertype == "oidc") + { +
+ +
+ +
+
+ } +
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+ }

@@ -209,14 +276,22 @@ else private string _requirepunctuation; private string _maximumfailures; private string _lockoutduration; - private string _provider; + + private string _providertype; + private string _providername; private string _authority; + private string _metadataurl; + private string _authorizationurl; + private string _tokenurl; + private string _userinfourl; private string _clientid; private string _clientsecret; + private string _scopes; + private string _pkce; private string _redirecturl; private string _emailclaimtype; - private string _metadata; - private string _logouturl; + private string _domainfilter; + private string _createusers; private string _allowsitelogin; public override SecurityAccessLevel SecurityAccessLevel => SecurityAccessLevel.Admin; @@ -229,6 +304,7 @@ else _allowregistration = PageState.Site.AllowRegistration.ToString(); var settings = await SettingService.GetSiteSettingsAsync(PageState.Site.SiteId); + _minimumlength = SettingService.GetSetting(settings, "IdentityOptions:Password:RequiredLength", "6"); _uniquecharacters = SettingService.GetSetting(settings, "IdentityOptions:Password:RequiredUniqueChars", "1"); _requiredigit = SettingService.GetSetting(settings, "IdentityOptions:Password:RequireDigit", "true"); @@ -237,15 +313,23 @@ else _requirepunctuation = SettingService.GetSetting(settings, "IdentityOptions:Password:RequireNonAlphanumeric", "true"); _maximumfailures = SettingService.GetSetting(settings, "IdentityOptions:Lockout:MaxFailedAccessAttempts", "5"); _lockoutduration = TimeSpan.Parse(SettingService.GetSetting(settings, "IdentityOptions:Lockout:DefaultLockoutTimeSpan", "00:05:00")).TotalMinutes.ToString(); - _provider = SettingService.GetSetting(settings, "OpenIdConnectOptions:Provider", ""); - _authority = SettingService.GetSetting(settings, "OpenIdConnectOptions:Authority", ""); - _clientid = SettingService.GetSetting(settings, "OpenIdConnectOptions:ClientId", ""); - _clientsecret = SettingService.GetSetting(settings, "OpenIdConnectOptions:ClientSecret", ""); - _redirecturl = PageState.Uri.Scheme + "://" + PageState.Alias.Name + "/signin-oidc"; - _emailclaimtype = SettingService.GetSetting(settings, "OpenIdConnectOptions:EmailClaimType", "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress"); - _metadata = SettingService.GetSetting(settings, "OpenIdConnectOptions:MetadataAddress", ""); - _logouturl = SettingService.GetSetting(settings, "OpenIdConnectOptions:LogoutUrl", ""); - _allowsitelogin = SettingService.GetSetting(settings, "AllowSiteLogin", "true"); + + _providertype = SettingService.GetSetting(settings, "ExternalLogin:ProviderType", ""); + _providername = SettingService.GetSetting(settings, "ExternalLogin:ProviderName", ""); + _authority = SettingService.GetSetting(settings, "ExternalLogin:Authority", ""); + _metadataurl = SettingService.GetSetting(settings, "ExternalLogin:MetadataUrl", ""); + _authorizationurl = SettingService.GetSetting(settings, "ExternalLogin:AuthorizationUrl", ""); + _tokenurl = SettingService.GetSetting(settings, "ExternalLogin:TokenUrl", ""); + _userinfourl = SettingService.GetSetting(settings, "ExternalLogin:UserInfoUrl", ""); + _clientid = SettingService.GetSetting(settings, "ExternalLogin:ClientId", ""); + _clientsecret = SettingService.GetSetting(settings, "ExternalLogin:ClientSecret", ""); + _scopes = SettingService.GetSetting(settings, "ExternalLogin:Scopes", ""); + _pkce = SettingService.GetSetting(settings, "ExternalLogin:PKCE", "false"); + _redirecturl = PageState.Uri.Scheme + "://" + PageState.Alias.Name + "/signin-" + _providertype; + _emailclaimtype = SettingService.GetSetting(settings, "ExternalLogin:EmailClaimType", "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress"); + _domainfilter = SettingService.GetSetting(settings, "ExternalLogin:DomainFilter", ""); + _createusers = SettingService.GetSetting(settings, "ExternalLogin:CreateUsers", "true"); + _allowsitelogin = SettingService.GetSetting(settings, "ExternalLogin:AllowSiteLogin", "true"); } private List Search(string search) @@ -324,14 +408,23 @@ else settings = SettingService.SetSetting(settings, "IdentityOptions:Password:RequireNonAlphanumeric", _requirepunctuation, true); settings = SettingService.SetSetting(settings, "IdentityOptions:Lockout:MaxFailedAccessAttempts", _maximumfailures, true); settings = SettingService.SetSetting(settings, "IdentityOptions:Lockout:DefaultLockoutTimeSpan", TimeSpan.FromMinutes(Convert.ToInt64(_lockoutduration)).ToString(), true); - settings = SettingService.SetSetting(settings, "OpenIdConnectOptions:Provider", _provider, false); - settings = SettingService.SetSetting(settings, "OpenIdConnectOptions:Authority", _authority, true); - settings = SettingService.SetSetting(settings, "OpenIdConnectOptions:ClientId", _clientid, true); - settings = SettingService.SetSetting(settings, "OpenIdConnectOptions:ClientSecret", _clientsecret, true); - settings = SettingService.SetSetting(settings, "OpenIdConnectOptions:EmailClaimType", _emailclaimtype, true); - settings = SettingService.SetSetting(settings, "OpenIdConnectOptions:MetadataAddress", _metadata, true); - settings = SettingService.SetSetting(settings, "OpenIdConnectOptions:LogoutUrl", _logouturl, true); - settings = SettingService.SetSetting(settings, "AllowSiteLogin", _allowsitelogin, false); + + settings = SettingService.SetSetting(settings, "ExternalLogin:ProviderType", _providertype, false); + settings = SettingService.SetSetting(settings, "ExternalLogin:ProviderName", _providername, false); + settings = SettingService.SetSetting(settings, "ExternalLogin:Authority", _authority, true); + settings = SettingService.SetSetting(settings, "ExternalLogin:MetadataUrl", _metadataurl, true); + settings = SettingService.SetSetting(settings, "ExternalLogin:AuthorizationUrl", _authorizationurl, true); + settings = SettingService.SetSetting(settings, "ExternalLogin:TokenUrl", _tokenurl, true); + settings = SettingService.SetSetting(settings, "ExternalLogin:UserInfoUrl", _userinfourl, true); + settings = SettingService.SetSetting(settings, "ExternalLogin:ClientId", _clientid, true); + settings = SettingService.SetSetting(settings, "ExternalLogin:ClientSecret", _clientsecret, true); + settings = SettingService.SetSetting(settings, "ExternalLogin:Scopes", _scopes, true); + settings = SettingService.SetSetting(settings, "ExternalLogin:PKCE", _pkce, true); + settings = SettingService.SetSetting(settings, "ExternalLogin:EmailClaimType", _emailclaimtype, true); + settings = SettingService.SetSetting(settings, "ExternalLogin:DomainFilter", _domainfilter, true); + settings = SettingService.SetSetting(settings, "ExternalLogin:CreateUsers", _createusers, true); + settings = SettingService.SetSetting(settings, "ExternalLogin:AllowSiteLogin", _allowsitelogin, false); + await SettingService.UpdateSiteSettingsAsync(settings, site.SiteId); await SettingService.ClearSiteSettingsCacheAsync(site.SiteId); @@ -343,4 +436,19 @@ else AddModuleMessage(Localizer["Error.SaveSiteSettings"], MessageType.Error); } } + + private void ProviderTypeChanged(ChangeEventArgs e) + { + _providertype = (string)e.Value; + if (_providertype == "oidc") + { + _scopes = "openid,profile,email"; + } + else + { + _scopes = ""; + } + _redirecturl = PageState.Uri.Scheme + "://" + PageState.Alias.Name + "/signin-" + _providertype; + StateHasChanged(); + } } diff --git a/Oqtane.Client/Resources/Modules/Admin/Users/Index.resx b/Oqtane.Client/Resources/Modules/Admin/Users/Index.resx index a2b74514..d3f75d3e 100644 --- a/Oqtane.Client/Resources/Modules/Admin/Users/Index.resx +++ b/Oqtane.Client/Resources/Modules/Admin/Users/Index.resx @@ -154,43 +154,43 @@ Roles - The Number Of Minutes A User Should Be Locked Out + The number of minutes a user should be locked out Lockout Duration: - The Maximum Number Of Sign In Attempts Before A User Is Locked Out + The maximum number of sign in attempts before a user is locked out Maximum Failures: - Indicate If Passwords Must Contain A Digit + Indicate if passwords must contain a digit Require Digit? - The Minimum Length For A Password + The minimum length for a password Minimum Length: - Indicate If Passwords Must Contain A Lower Case Character + Indicate if passwords must contain a lower case character Require Lowercase? - Indicate if Passwords Must Contain A Non-alphanumeric Character (ie. Punctuation) + Indicate if passwords must contain a non-alphanumeric character (ie. punctuation) Require Punctuation? - Indicate If Passwords Must Contain An Upper Case Character + Indicate if passwords must contain an upper case character Require Uppercase? @@ -199,9 +199,114 @@ Configuration Updated. Please Select Restart Application For These Changes To Be Activated. - The Minimum Number Of Unique Characters Which A Password Must Contain + The minimum number of unique characters which a password must contain Unique Characters: + + Do you want to allow users to sign in using a username and password that is managed locally on this site? Note that you should only disable this option if you have already sucessfully configured an external login provider, or else you may lock yourself out of the site. + + + Allow Site Login? + + + The Authority Url or Issuer Url associated with the OpenID Connect provider + + + Authority: + + + The endpoint for obtaining an Authorization Code + + + Authorization Url: + + + The Client ID from the provider + + + Client ID: + + + The Client Secret from the provider + + + Client Secret: + + + Do you want new users to be created automatically? If you disable this option, users must already be registered on the site in order to sign in with their external login. + + + Create New Users? + + + Provide any email domain filter criteria (separated by commas). Domains to exclude should be prefixed with an exclamation point (!). For example 'microsoft.com,!hotmail.com' would include microsoft.com email addresses but not hotmail.com email addresses. + + + Domain Filter: + + + The type name for the email address claim provided by the provider + + + Email Claim Type: + + + External Login Settings + + + Lockout Settings + + + The discovery endpoint for obtaining metadata for this provider. Only specify if the OpenID Connect provider does not use the standard approach (ie. /.well-known/openid-configuration) + + + Metadata Url: + + + Password Settings + + + Indicate if the provider supports Proof Key for Code Exchange (PKCE) + + + Use PKCE? + + + The external login provider name which will be displayed on the login page + + + Provider Name: + + + Select the external login provider type + + + Provider Type: + + + The Redirect Url (or Callback Url) which usually needs to be registered with the provider + + + Redirect Url: + + + A list of Scopes to request from the provider (separated by commas). If none are specified, standard Scopes will be used by default. + + + Scopes: + + + The endpoint for obtaining an Auth Token + + + Token Url: + + + The endpoint for obtaining user information. This should be an API or Page Url which contains the users email address. + + + User Info Url: + \ No newline at end of file diff --git a/Oqtane.Server/Extensions/OqtaneSiteAuthenticationBuilderExtensions.cs b/Oqtane.Server/Extensions/OqtaneSiteAuthenticationBuilderExtensions.cs index a1589bda..344f1c47 100644 --- a/Oqtane.Server/Extensions/OqtaneSiteAuthenticationBuilderExtensions.cs +++ b/Oqtane.Server/Extensions/OqtaneSiteAuthenticationBuilderExtensions.cs @@ -15,6 +15,10 @@ using System.IO; using System.Collections.Generic; using Oqtane.Security; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Authentication.OAuth; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text.RegularExpressions; namespace Oqtane.Extensions { @@ -36,152 +40,236 @@ namespace Oqtane.Extensions // site OpenIdConnect options builder.AddSiteOptions((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 + if (alias.SiteSettings.GetValue("ExternalLogin:ProviderType", "") == "oidc") + { + // 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 - // cookie config is required to avoid Correlation Failed errors - options.NonceCookie.SameSite = SameSiteMode.Unspecified; - options.CorrelationCookie.SameSite = SameSiteMode.Unspecified; + // 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); + // site options + options.Authority = alias.SiteSettings.GetValue("ExternalLogin:Authority", ""); + options.MetadataAddress = alias.SiteSettings.GetValue("ExternalLogin:MetadataUrl", ""); + options.ClientId = alias.SiteSettings.GetValue("ExternalLogin:ClientId", ""); + options.ClientSecret = alias.SiteSettings.GetValue("ExternalLogin:ClientSecret", ""); + options.UsePkce = bool.Parse(alias.SiteSettings.GetValue("ExternalLogin:PKCE", "false")); + options.Scope.Clear(); + foreach (var scope in alias.SiteSettings.GetValue("ExternalLogin:Scopes", "openid,profile,email").Split(',', StringSplitOptions.RemoveEmptyEntries)) + { + options.Scope.Add(scope); + } - // openid connect events - options.Events.OnTokenValidated = OnTokenValidated; - options.Events.OnRedirectToIdentityProviderForSignOut = OnRedirectToIdentityProviderForSignOut; - options.Events.OnAccessDenied = OnAccessDenied; - options.Events.OnRemoteFailure = OnRemoteFailure; + // openid connect events + options.Events.OnTokenValidated = OnTokenValidated; + options.Events.OnAccessDenied = OnAccessDenied; + options.Events.OnRemoteFailure = OnRemoteFailure; + } }); - // site ChallengeScheme options - builder.AddSiteOptions((options, alias) => + // site OAuth2.0 options + builder.AddSiteOptions((options, alias) => { - if (alias.SiteSettings.GetValue("OpenIdConnectOptions:Authority", "") != "") + if (alias.SiteSettings.GetValue("ExternalLogin:ProviderType", "") == "oauth2") { - options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme; + // default options + options.SignInScheme = Constants.AuthenticationScheme; // identity cookie + options.CallbackPath = string.IsNullOrEmpty(alias.Path) ? "/signin-oauth2" : "/" + alias.Path + "/signin-oauth2"; + options.SaveTokens = true; + + // site options + options.AuthorizationEndpoint = alias.SiteSettings.GetValue("ExternalLogin:AuthorizationUrl", ""); + options.TokenEndpoint = alias.SiteSettings.GetValue("ExternalLogin:TokenUrl", ""); + options.UserInformationEndpoint = alias.SiteSettings.GetValue("ExternalLogin:UserInfoUrl", ""); + options.ClientId = alias.SiteSettings.GetValue("ExternalLogin:ClientId", ""); + options.ClientSecret = alias.SiteSettings.GetValue("ExternalLogin:ClientSecret", ""); + options.UsePkce = bool.Parse(alias.SiteSettings.GetValue("ExternalLogin:PKCE", "false")); + options.Scope.Clear(); + foreach (var scope in alias.SiteSettings.GetValue("ExternalLogin:Scopes", "").Split(',', StringSplitOptions.RemoveEmptyEntries)) + { + options.Scope.Add(scope); + } + + // cookie config is required to avoid Correlation Failed errors + options.CorrelationCookie.SameSite = SameSiteMode.Unspecified; + + // oauth2 events + options.Events.OnCreatingTicket = OnCreatingTicket; + options.Events.OnAccessDenied = OnAccessDenied; + options.Events.OnRemoteFailure = OnRemoteFailure; } }); return builder; } + private static async Task OnCreatingTicket(OAuthCreatingTicketContext context) + { + // OAuth 2.0 + var email = ""; + if (context.Options.UserInformationEndpoint != "") + { + try + { + var request = new HttpRequestMessage(HttpMethod.Get, context.Options.UserInformationEndpoint); + request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + request.Headers.UserAgent.Add(new ProductInfoHeaderValue(Constants.PackageId, Constants.Version)); + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", context.AccessToken); + var response = await context.Backchannel.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, context.HttpContext.RequestAborted); + response.EnsureSuccessStatusCode(); + var output = await response.Content.ReadAsStringAsync(); + + // get email address using Regex on the raw output (could be json or html) + var regex = new Regex(@"\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*", RegexOptions.IgnoreCase); + foreach (Match match in regex.Matches(output)) + { + if (EmailValid(match.Value, context.HttpContext.GetAlias().SiteSettings.GetValue("ExternalLogin:DomainFilter", ""))) + { + email = match.Value.ToLower(); + break; + } + } + } + catch (Exception ex) + { + var _logger = context.HttpContext.RequestServices.GetRequiredService(); + _logger.Log(LogLevel.Error, "ExternalLogin", Enums.LogFunction.Security, "An Error Occurred Accessing The User Info Endpoint - {Error}", ex.Message); + } + } + + // login user + await LoginUser(email, context.HttpContext, context.Principal); + } + private static async Task OnTokenValidated(TokenValidatedContext context) { - var providerKey = context.Principal.FindFirstValue(ClaimTypes.NameIdentifier); - var loginProvider = context.HttpContext.GetAlias().SiteSettings.GetValue("OpenIdConnectOptions:Authority", ""); - var emailClaimType = context.HttpContext.GetAlias().SiteSettings.GetValue("OpenIdConnectOptions:EmailClaimType", ""); - if (string.IsNullOrEmpty(emailClaimType)) - { - emailClaimType = ClaimTypes.Email; - } - var alias = context.HttpContext.GetAlias(); - var _logger = context.HttpContext.RequestServices.GetRequiredService(); - - // custom logic may be needed here to manipulate Principal sent by Provider - use interface similar to IClaimsTransformation - + // OpenID Connect + var emailClaimType = context.HttpContext.GetAlias().SiteSettings.GetValue("ExternalLogin:EmailClaimType", ""); var email = context.Principal.FindFirstValue(emailClaimType); - // validate email claim - if (email == null || !email.Contains("@") || !email.Contains(".")) - { - var emailclaimtype = context.Principal.Claims.FirstOrDefault(item => item.Value.Contains("@") && item.Value.Contains(".")); - if (emailclaimtype != null) - { - email = emailclaimtype.Value; - _logger.Log(LogLevel.Information, nameof(OqtaneSiteAuthenticationBuilderExtensions), Enums.LogFunction.Security, "Please Update The Email Claim Type For The OpenID Connect Provider To {EmailClaimType} In Site Settings", emailclaimtype.Type); - } - else - { - email = null; - } - } + // login user + await LoginUser(email, context.HttpContext, context.Principal); + } - if (email != null) + private static Task OnAccessDenied(AccessDeniedContext context) + { + var _logger = context.HttpContext.RequestServices.GetRequiredService(); + _logger.Log(LogLevel.Information, "ExternalLogin", Enums.LogFunction.Security, "External Login 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, true); + context.HandleResponse(); + return Task.CompletedTask; + } + + private static Task OnRemoteFailure(RemoteFailureContext context) + { + var _logger = context.HttpContext.RequestServices.GetRequiredService(); + _logger.Log(LogLevel.Error, "ExternalLogin", Enums.LogFunction.Security, "External Login Remote Failure - {Error}", context.Failure.Message); + // redirect to login page + var alias = context.HttpContext.GetAlias(); + context.Response.Redirect(alias.Path + "/login", true); + context.HandleResponse(); + return Task.CompletedTask; + } + + private static async Task LoginUser(string email, HttpContext httpContext, ClaimsPrincipal claimsPrincipal) + { + var _logger = httpContext.RequestServices.GetRequiredService(); + var alias = httpContext.GetAlias(); + + if (EmailValid(email, alias.SiteSettings.GetValue("ExternalLogin:DomainFilter", ""))) { - var _identityUserManager = context.HttpContext.RequestServices.GetRequiredService>(); - var _users = context.HttpContext.RequestServices.GetRequiredService(); - var _userRoles = context.HttpContext.RequestServices.GetRequiredService(); + var _identityUserManager = httpContext.RequestServices.GetRequiredService>(); + var _users = httpContext.RequestServices.GetRequiredService(); + var _userRoles = httpContext.RequestServices.GetRequiredService(); + var providerType = httpContext.GetAlias().SiteSettings.GetValue("ExternalLogin:ProviderType", ""); + var providerKey = claimsPrincipal.FindFirstValue(ClaimTypes.NameIdentifier); + if (providerKey == null) + { + providerKey = email; // OAuth2 does not pass claims + } 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) + if (bool.Parse(alias.SiteSettings.GetValue("ExternalLogin:CreateUsers", "true"))) { - // add user login - await _identityUserManager.AddLoginAsync(identityuser, new UserLoginInfo(loginProvider, providerKey, email)); - - user = new User(); - user.SiteId = alias.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(); - Folder folder = _folders.GetFolder(user.SiteId, Utilities.PathCombine("Users", Path.DirectorySeparatorChar.ToString())); - if (folder != 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) { - _folders.AddFolder(new Folder + // add user login + await _identityUserManager.AddLoginAsync(identityuser, new UserLoginInfo(providerType, providerKey, "")); + + user = new User(); + user.SiteId = alias.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 = httpContext.RequestServices.GetRequiredService(); + Folder folder = _folders.GetFolder(user.SiteId, Utilities.PathCombine("Users", Path.DirectorySeparatorChar.ToString())); + if (folder != null) { - 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 + _folders.AddFolder(new Folder { - new Permission(PermissionNames.Browse, user.UserId, true), - new Permission(PermissionNames.View, RoleNames.Everyone, true), - new Permission(PermissionNames.Edit, user.UserId, true) - }.EncodePermissions() - }); - } + 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 + { + 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(); - List 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); + // add auto assigned roles to user for site + var _roles = httpContext.RequestServices.GetRequiredService(); + List 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 + { + _logger.Log(LogLevel.Error, "ExternalLogin", Enums.LogFunction.Security, "Creation Of New Users Is Disabled. User With Email Address {Email} Will First Need To Be Registered On The Site.", email); + } } else { var logins = await _identityUserManager.GetLoginsAsync(identityuser); - var login = logins.FirstOrDefault(item => item.LoginProvider == loginProvider); + var login = logins.FirstOrDefault(item => item.LoginProvider == providerType); if (login != null) { if (login.ProviderKey == providerKey) @@ -191,13 +279,13 @@ namespace Oqtane.Extensions 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}. Login Denied.", email); + _logger.Log(LogLevel.Error, "ExternalLogin", Enums.LogFunction.Security, "Provider Key Does Not Match For User {Username}. Login Denied.", identityuser.UserName); } } else { // add user login - await _identityUserManager.AddLoginAsync(identityuser, new UserLoginInfo(loginProvider, providerKey, identityuser.UserName)); + await _identityUserManager.AddLoginAsync(identityuser, new UserLoginInfo(providerType, providerKey, "")); user = _users.GetUser(identityuser.UserName); } } @@ -207,75 +295,63 @@ namespace Oqtane.Extensions { // update user user.LastLoginOn = DateTime.UtcNow; - user.LastIPAddress = context.HttpContext.Connection.RemoteIpAddress.ToString(); + user.LastIPAddress = 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); - } + _logger.Log(LogLevel.Information, "ExternalLogin", Enums.LogFunction.Security, "User Login Successful For {Username} Using Provider {Provider}", user.Username, providerType); // add Oqtane claims + var principal = (ClaimsIdentity)claimsPrincipal.Identity; + UserSecurity.ResetClaimsIdentity(principal); List userroles = _userRoles.GetUserRoles(user.UserId, user.SiteId).ToList(); var identity = UserSecurity.CreateClaimsIdentity(alias, user, userroles); principal.AddClaims(identity.Claims); - - // add provider - principal.AddClaim(new Claim("Provider", context.HttpContext.GetAlias().SiteSettings["OpenIdConnectOptions:Authority"])); } - } - else // no email claim - { - _logger.Log(LogLevel.Error, nameof(OqtaneSiteAuthenticationBuilderExtensions), Enums.LogFunction.Security, "OpenID Connect Provider Did Not Return An Email Claim To Uniquely Identify The User"); - } - } - - 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)) + else // user not logged in { - if (postLogoutUri.StartsWith("/")) - { - var request = context.Request; - postLogoutUri = request.Scheme + "://" + request.Host + request.PathBase + postLogoutUri; - } - logoutUrl += $"&returnTo={Uri.EscapeDataString(postLogoutUri)}"; + await httpContext.SignOutAsync(); } - context.Response.Redirect(logoutUrl); - context.HandleResponse(); } - return Task.CompletedTask; + else // email invalid + { + if (!string.IsNullOrEmpty(email)) + { + _logger.Log(LogLevel.Error, "ExternalLogin", Enums.LogFunction.Security, "The Email Address {Email} Is Invalid Or Does Not Match The Domain Filter Criteria. Login Denied.", email); + } + else + { + var emailclaimtype = claimsPrincipal.Claims.FirstOrDefault(item => item.Value.Contains("@") && item.Value.Contains(".")); + if (emailclaimtype != null) + { + _logger.Log(LogLevel.Error, "ExternalLogin", Enums.LogFunction.Security, "Please Verify If \"{ClaimType}\" Is A Valid Email Claim Type For The Provider And Update Your External Login Settings Accordingly", emailclaimtype.Type); + } + else + { + _logger.Log(LogLevel.Error, "ExternalLogin", Enums.LogFunction.Security, "Provider Did Not Return An Email To Uniquely Identify The User."); + } + } + await httpContext.SignOutAsync(); + } } - private static Task OnAccessDenied(AccessDeniedContext context) + private static bool EmailValid(string email, string domainfilter) { - var _logger = context.HttpContext.RequestServices.GetRequiredService(); - _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; - } - - private static Task OnRemoteFailure(RemoteFailureContext context) - { - var _logger = context.HttpContext.RequestServices.GetRequiredService(); - _logger.Log(LogLevel.Error, nameof(OqtaneSiteAuthenticationBuilderExtensions), Enums.LogFunction.Security, "OpenID Connect Remote Failure - {Error}", context.Failure.Message); - // redirect to login page - var alias = context.HttpContext.GetAlias(); - context.Response.Redirect(alias.Path + "/login?returnurl=" + context.Properties.RedirectUri); - context.HandleResponse(); - return Task.CompletedTask; + if (!string.IsNullOrEmpty(email) && email.Contains("@") && email.Contains(".")) + { + var domains = domainfilter.ToLower().Split(',', StringSplitOptions.RemoveEmptyEntries); + foreach (var domain in domains) + { + if (domain.StartsWith("!")) + { + if (email.ToLower().Contains(domain.Substring(1))) return false; + } + else + { + if (!email.ToLower().Contains(domain)) return false; + } + } + return true; + } + return false; } } } diff --git a/Oqtane.Server/Pages/External.cshtml b/Oqtane.Server/Pages/External.cshtml new file mode 100644 index 00000000..8898e154 --- /dev/null +++ b/Oqtane.Server/Pages/External.cshtml @@ -0,0 +1,3 @@ +@page "/pages/external" +@namespace Oqtane.Pages +@model Oqtane.Pages.ExternalModel diff --git a/Oqtane.Server/Pages/External.cshtml.cs b/Oqtane.Server/Pages/External.cshtml.cs new file mode 100644 index 00000000..5a554366 --- /dev/null +++ b/Oqtane.Server/Pages/External.cshtml.cs @@ -0,0 +1,29 @@ +using System.Net; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.OpenIdConnect; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Oqtane.Extensions; + +namespace Oqtane.Pages +{ + public class ExternalModel : PageModel + { + public IActionResult OnGetAsync(string returnurl) + { + returnurl = (returnurl == null) ? "/" : returnurl; + returnurl = (!returnurl.StartsWith("/")) ? "/" + returnurl : returnurl; + + var providertype = HttpContext.GetAlias().SiteSettings.GetValue("ExternalLogin:ProviderType", ""); + if (providertype != "") + { + return new ChallengeResult(providertype, new AuthenticationProperties { RedirectUri = returnurl }); + } + else + { + HttpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden; + return new EmptyResult(); + } + } + } +} diff --git a/Oqtane.Server/Pages/Logout.cshtml.cs b/Oqtane.Server/Pages/Logout.cshtml.cs index ab9ed7ad..b33fefe3 100644 --- a/Oqtane.Server/Pages/Logout.cshtml.cs +++ b/Oqtane.Server/Pages/Logout.cshtml.cs @@ -20,18 +20,7 @@ namespace Oqtane.Pages returnurl = (returnurl == null) ? "/" : returnurl; returnurl = (!returnurl.StartsWith("/")) ? "/" + returnurl : 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)); - } + return LocalRedirect(Url.Content("~" + returnurl)); } } } diff --git a/Oqtane.Server/Pages/OIDC.cshtml b/Oqtane.Server/Pages/OIDC.cshtml deleted file mode 100644 index 47fe734a..00000000 --- a/Oqtane.Server/Pages/OIDC.cshtml +++ /dev/null @@ -1,3 +0,0 @@ -@page "/pages/oidc" -@namespace Oqtane.Pages -@model Oqtane.Pages.OIDCModel diff --git a/Oqtane.Server/Pages/OIDC.cshtml.cs b/Oqtane.Server/Pages/OIDC.cshtml.cs deleted file mode 100644 index d70f3931..00000000 --- a/Oqtane.Server/Pages/OIDC.cshtml.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Microsoft.AspNetCore.Authentication; -using Microsoft.AspNetCore.Authentication.OpenIdConnect; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.RazorPages; - -namespace Oqtane.Pages -{ - public class OIDCModel : PageModel - { - public IActionResult OnGetAsync(string returnurl) - { - returnurl = (returnurl == null) ? "/" : returnurl; - returnurl = (!returnurl.StartsWith("/")) ? "/" + returnurl : returnurl; - - return new ChallengeResult(OpenIdConnectDefaults.AuthenticationScheme, - new AuthenticationProperties { RedirectUri = returnurl }); - } - } -} diff --git a/Oqtane.Server/Startup.cs b/Oqtane.Server/Startup.cs index 119d3ab5..57692861 100644 --- a/Oqtane.Server/Startup.cs +++ b/Oqtane.Server/Startup.cs @@ -115,7 +115,8 @@ namespace Oqtane options.DefaultChallengeScheme = Constants.AuthenticationScheme; }) .AddCookie(Constants.AuthenticationScheme) - .AddOpenIdConnect(); + .AddOpenIdConnect("oidc", options => { }) + .AddOAuth("oauth2", options => { }); services.ConfigureOqtaneCookieOptions(); diff --git a/Oqtane.Shared/Security/UserSecurity.cs b/Oqtane.Shared/Security/UserSecurity.cs index 7ef1ea55..0520670f 100644 --- a/Oqtane.Shared/Security/UserSecurity.cs +++ b/Oqtane.Shared/Security/UserSecurity.cs @@ -45,7 +45,7 @@ namespace Oqtane.Security { if (user == null) { - authorized = IsAuthorized(-1, "", permissions); // user is not authenticated but may have access to resource + authorized = IsAuthorized(-1, "", permissions); // user is not authenticated but may have access to resource } else { @@ -152,5 +152,24 @@ namespace Oqtane.Security } return identity; } + + public static void ResetClaimsIdentity(ClaimsIdentity identity) + { + var claim = identity.Claims.FirstOrDefault(item => item.Type == ClaimTypes.Name); + if (claim != null) + { + identity.RemoveClaim(claim); + } + claim = identity.Claims.FirstOrDefault(item => item.Type == ClaimTypes.PrimarySid); + if (claim != null) + { + identity.RemoveClaim(claim); + } + claim = identity.Claims.FirstOrDefault(item => item.Type == ClaimTypes.GroupSid); + if (claim != null) + { + identity.RemoveClaim(claim); + } + } } }