diff --git a/Oqtane.Client/Modules/Admin/Users/Index.razor b/Oqtane.Client/Modules/Admin/Users/Index.razor
index 6173bed4..37bd69fd 100644
--- a/Oqtane.Client/Modules/Admin/Users/Index.razor
+++ b/Oqtane.Client/Modules/Admin/Users/Index.razor
@@ -259,6 +259,12 @@ else
+
@@ -380,6 +386,7 @@ else
private string _clientsecrettype = "password";
private string _toggleclientsecret = string.Empty;
private string _scopes;
+ private string _parameters;
private string _pkce;
private string _redirecturl;
private string _identifierclaimtype;
@@ -432,6 +439,7 @@ else
_clientsecret = SettingService.GetSetting(settings, "ExternalLogin:ClientSecret", "");
_toggleclientsecret = SharedLocalizer["ShowPassword"];
_scopes = SettingService.GetSetting(settings, "ExternalLogin:Scopes", "");
+ _parameters = SettingService.GetSetting(settings, "ExternalLogin:Parameters", "");
_pkce = SettingService.GetSetting(settings, "ExternalLogin:PKCE", "false");
_redirecturl = PageState.Uri.Scheme + "://" + PageState.Alias.Name + "/signin-" + _providertype;
_identifierclaimtype = SettingService.GetSetting(settings, "ExternalLogin:IdentifierClaimType", "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier");
@@ -549,6 +557,7 @@ else
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:Parameters", _parameters, true);
settings = SettingService.SetSetting(settings, "ExternalLogin:PKCE", _pkce, true);
settings = SettingService.SetSetting(settings, "ExternalLogin:IdentifierClaimType", _identifierclaimtype, true);
settings = SettingService.SetSetting(settings, "ExternalLogin:EmailClaimType", _emailclaimtype, true);
diff --git a/Oqtane.Client/Resources/Modules/Admin/Users/Index.resx b/Oqtane.Client/Resources/Modules/Admin/Users/Index.resx
index e37f3f20..c821f3eb 100644
--- a/Oqtane.Client/Resources/Modules/Admin/Users/Index.resx
+++ b/Oqtane.Client/Resources/Modules/Admin/Users/Index.resx
@@ -384,4 +384,10 @@
Identifier Claim:
+
+ Optionally specify any additional parameters as name/value pairs to send to the provider (separated by commas if there are multiple). For example you could specify p=B2C_1_Signin if you are using a specific Azure B2C User Flow policy.
+
+
+ Parameters:
+
\ No newline at end of file
diff --git a/Oqtane.Server/Extensions/OqtaneSiteAuthenticationBuilderExtensions.cs b/Oqtane.Server/Extensions/OqtaneSiteAuthenticationBuilderExtensions.cs
index b63b9ad3..a725f862 100644
--- a/Oqtane.Server/Extensions/OqtaneSiteAuthenticationBuilderExtensions.cs
+++ b/Oqtane.Server/Extensions/OqtaneSiteAuthenticationBuilderExtensions.cs
@@ -66,6 +66,20 @@ namespace Oqtane.Extensions
options.Events.OnTokenValidated = OnTokenValidated;
options.Events.OnAccessDenied = OnAccessDenied;
options.Events.OnRemoteFailure = OnRemoteFailure;
+ if (sitesettings.GetValue("ExternalLogin:Parameters", "") != "")
+ {
+ options.Events = new OpenIdConnectEvents
+ {
+ OnRedirectToIdentityProvider = context =>
+ {
+ foreach(var parameter in sitesettings.GetValue("ExternalLogin:Parameters", "").Split(","))
+ {
+ context.ProtocolMessage.SetParameter(parameter.Split("=")[0], parameter.Split("=")[1]);
+ }
+ return Task.FromResult(0);
+ }
+ };
+ }
}
});
@@ -100,6 +114,22 @@ namespace Oqtane.Extensions
options.Events.OnTicketReceived = OnTicketReceived;
options.Events.OnAccessDenied = OnAccessDenied;
options.Events.OnRemoteFailure = OnRemoteFailure;
+ if (sitesettings.GetValue("ExternalLogin:Parameters", "") != "")
+ {
+ options.Events = new OAuthEvents
+ {
+ OnRedirectToAuthorizationEndpoint = context =>
+ {
+ var url = context.RedirectUri;
+ foreach (var parameter in sitesettings.GetValue("ExternalLogin:Parameters", "").Split(","))
+ {
+ url += (!url.Contains("?")) ? "?" + parameter : "&" + parameter;
+ }
+ context.Response.Redirect(url);
+ return Task.FromResult(0);
+ }
+ };
+ }
}
});
@@ -111,6 +141,7 @@ namespace Oqtane.Extensions
// OAuth 2.0
var email = "";
var id = "";
+ var claims = "";
if (context.Options.UserInformationEndpoint != "")
{
@@ -123,16 +154,16 @@ namespace Oqtane.Extensions
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();
+ claims = await response.Content.ReadAsStringAsync();
// parse json output
var idClaimType = context.HttpContext.GetSiteSettings().GetValue("ExternalLogin:IdentifierClaimType", "");
var emailClaimType = context.HttpContext.GetSiteSettings().GetValue("ExternalLogin:EmailClaimType", "");
- if (!output.StartsWith("[") && !output.EndsWith("]"))
+ if (!claims.StartsWith("[") && !claims.EndsWith("]"))
{
- output = "[" + output + "]"; // convert to json array
+ claims = "[" + claims + "]"; // convert to json array
}
- JsonNode items = JsonNode.Parse(output)!;
+ JsonNode items = JsonNode.Parse(claims)!;
foreach (var item in items.AsArray())
{
if (item[emailClaimType] != null)
@@ -161,7 +192,7 @@ namespace Oqtane.Extensions
}
// validate user
- var identity = await ValidateUser(email, id, context.HttpContext);
+ var identity = await ValidateUser(email, id, claims, context.HttpContext);
if (identity.Label == ExternalLoginStatus.Success)
{
identity.AddClaim(new Claim("access_token", context.AccessToken));
@@ -193,9 +224,10 @@ namespace Oqtane.Extensions
var id = context.Principal.FindFirstValue(idClaimType);
var emailClaimType = context.HttpContext.GetSiteSettings().GetValue("ExternalLogin:EmailClaimType", "");
var email = context.Principal.FindFirstValue(emailClaimType);
+ var claims = string.Join(", ", context.Principal.Claims.Select(item => item.Type).ToArray());
// validate user
- var identity = await ValidateUser(email, id, context.HttpContext);
+ var identity = await ValidateUser(email, id, claims, context.HttpContext);
if (identity.Label == ExternalLoginStatus.Success)
{
identity.AddClaim(new Claim("access_token", context.SecurityToken.RawData));
@@ -229,7 +261,7 @@ namespace Oqtane.Extensions
return Task.CompletedTask;
}
- private static async Task ValidateUser(string email, string id, HttpContext httpContext)
+ private static async Task ValidateUser(string email, string id, string claims, HttpContext httpContext)
{
var _logger = httpContext.RequestServices.GetRequiredService();
ClaimsIdentity identity = new ClaimsIdentity(Constants.AuthenticationScheme);
@@ -241,142 +273,149 @@ namespace Oqtane.Extensions
var _users = httpContext.RequestServices.GetRequiredService();
User user = null;
- // verify if external user is already registerd for this site
- var _identityUserManager = httpContext.RequestServices.GetRequiredService>();
- var identityuser = await _identityUserManager.FindByLoginAsync(providerType + ":" + alias.SiteId.ToString(), id);
- if (identityuser != null)
+ if (!string.IsNullOrEmpty(id))
{
- user = _users.GetUser(identityuser.UserName);
- }
- else
- {
- if (EmailValid(email, httpContext.GetSiteSettings().GetValue("ExternalLogin:DomainFilter", "")))
+ // verify if external user is already registered for this site
+ var _identityUserManager = httpContext.RequestServices.GetRequiredService>();
+ var identityuser = await _identityUserManager.FindByLoginAsync(providerType + ":" + alias.SiteId.ToString(), id);
+ if (identityuser != null)
{
- bool duplicates = false;
- try
+ user = _users.GetUser(identityuser.UserName);
+ }
+ else
+ {
+ if (EmailValid(email, httpContext.GetSiteSettings().GetValue("ExternalLogin:DomainFilter", "")))
{
- identityuser = await _identityUserManager.FindByEmailAsync(email);
- }
- catch
- {
- // FindByEmailAsync will throw an error if the email matches multiple user accounts
- duplicates = true;
- }
- if (identityuser == null)
- {
- if (duplicates)
+ bool duplicates = false;
+ try
{
- identity.Label = ExternalLoginStatus.DuplicateEmail;
- _logger.Log(LogLevel.Error, "ExternalLogin", Enums.LogFunction.Security, "Multiple Users Exist With Email Address {Email}. Login Denied.", email);
+ identityuser = await _identityUserManager.FindByEmailAsync(email);
}
- else
+ catch
{
- if (bool.Parse(httpContext.GetSiteSettings().GetValue("ExternalLogin:CreateUsers", "true")))
+ // FindByEmailAsync will throw an error if the email matches multiple user accounts
+ duplicates = true;
+ }
+ if (identityuser == null)
+ {
+ if (duplicates)
{
- 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)
+ identity.Label = ExternalLoginStatus.DuplicateEmail;
+ _logger.Log(LogLevel.Error, "ExternalLogin", Enums.LogFunction.Security, "Multiple Users Exist With Email Address {Email}. Login Denied.", email);
+ }
+ else
+ {
+ if (bool.Parse(httpContext.GetSiteSettings().GetValue("ExternalLogin:CreateUsers", "true")))
{
- user = new User
+ 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)
{
- SiteId = alias.SiteId,
- Username = email,
- DisplayName = email,
- Email = email,
- LastLoginOn = null,
- LastIPAddress = ""
- };
- user = _users.AddUser(user);
+ user = new User
+ {
+ SiteId = alias.SiteId,
+ Username = email,
+ DisplayName = email,
+ Email = email,
+ LastLoginOn = null,
+ LastIPAddress = ""
+ };
+ user = _users.AddUser(user);
- if (user != null)
- {
- var _notifications = httpContext.RequestServices.GetRequiredService();
- string url = httpContext.Request.Scheme + "://" + alias.Name;
- string body = "You Recently Used An External Account To Sign In To Our Site.\n\n" + url + "\n\nThank You!";
- var notification = new Notification(user.SiteId, user, "User Account Notification", body);
- _notifications.AddNotification(notification);
+ if (user != null)
+ {
+ var _notifications = httpContext.RequestServices.GetRequiredService();
+ string url = httpContext.Request.Scheme + "://" + alias.Name;
+ string body = "You Recently Used An External Account To Sign In To Our Site.\n\n" + url + "\n\nThank You!";
+ var notification = new Notification(user.SiteId, user, "User Account Notification", body);
+ _notifications.AddNotification(notification);
- // add user login
- await _identityUserManager.AddLoginAsync(identityuser, new UserLoginInfo(providerType + ":" + alias.SiteId.ToString(), id, providerName));
+ // add user login
+ await _identityUserManager.AddLoginAsync(identityuser, new UserLoginInfo(providerType + ":" + alias.SiteId.ToString(), id, providerName));
- _logger.Log(user.SiteId, LogLevel.Information, "ExternalLogin", Enums.LogFunction.Create, "User Added {User}", user);
+ _logger.Log(user.SiteId, LogLevel.Information, "ExternalLogin", Enums.LogFunction.Create, "User Added {User}", user);
+ }
+ else
+ {
+ identity.Label = ExternalLoginStatus.UserNotCreated;
+ _logger.Log(user.SiteId, LogLevel.Error, "ExternalLogin", Enums.LogFunction.Create, "Unable To Add User {Email}", email);
+ }
}
else
{
identity.Label = ExternalLoginStatus.UserNotCreated;
- _logger.Log(user.SiteId, LogLevel.Error, "ExternalLogin", Enums.LogFunction.Create, "Unable To Add User {Email}", email);
+ _logger.Log(user.SiteId, LogLevel.Error, "ExternalLogin", Enums.LogFunction.Create, "Unable To Add Identity User {Email} {Error}", email, result.Errors.ToString());
}
}
else
{
- identity.Label = ExternalLoginStatus.UserNotCreated;
- _logger.Log(user.SiteId, LogLevel.Error, "ExternalLogin", Enums.LogFunction.Create, "Unable To Add Identity User {Email} {Error}", email, result.Errors.ToString());
+ identity.Label = ExternalLoginStatus.UserDoesNotExist;
+ _logger.Log(LogLevel.Error, "ExternalLogin", Enums.LogFunction.Security, "Creation Of New Users Is Disabled For This Site. User With Email Address {Email} Will First Need To Be Registered On The Site.", email);
}
}
- else
- {
- identity.Label = ExternalLoginStatus.UserDoesNotExist;
- _logger.Log(LogLevel.Error, "ExternalLogin", Enums.LogFunction.Security, "Creation Of New Users Is Disabled For This Site. 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 == (providerType + ":" + alias.SiteId.ToString()));
- if (login == null)
- {
- // new external login using existing user account - verification required
- var _notifications = httpContext.RequestServices.GetRequiredService();
- string token = await _identityUserManager.GenerateEmailConfirmationTokenAsync(identityuser);
- string url = httpContext.Request.Scheme + "://" + alias.Name;
- url += $"/login?name={identityuser.UserName}&token={WebUtility.UrlEncode(token)}&key={WebUtility.UrlEncode(id)}";
- string body = $"You Recently Signed In To Our Site With {providerName} Using The Email Address {email}. ";
- body += "In Order To Complete The Linkage Of Your User Account Please Click The Link Displayed Below:\n\n" + url + "\n\nThank You!";
- var notification = new Notification(alias.SiteId, email, email, "External Login Linkage", body);
- _notifications.AddNotification(notification);
-
- identity.Label = ExternalLoginStatus.VerificationRequired;
- _logger.Log(alias.SiteId, LogLevel.Information, "ExternalLogin", Enums.LogFunction.Create, "External Login Linkage Verification For Provider {Provider} Sent To {Email}", providerName, email);
}
else
{
- // provider keys do not match
- identity.Label = ExternalLoginStatus.ProviderKeyMismatch;
- _logger.Log(LogLevel.Error, "ExternalLogin", Enums.LogFunction.Security, "Provider Key Does Not Match For User {Username}. Login Denied.", identityuser.UserName);
+ var logins = await _identityUserManager.GetLoginsAsync(identityuser);
+ var login = logins.FirstOrDefault(item => item.LoginProvider == (providerType + ":" + alias.SiteId.ToString()));
+ if (login == null)
+ {
+ // new external login using existing user account - verification required
+ var _notifications = httpContext.RequestServices.GetRequiredService();
+ string token = await _identityUserManager.GenerateEmailConfirmationTokenAsync(identityuser);
+ string url = httpContext.Request.Scheme + "://" + alias.Name;
+ url += $"/login?name={identityuser.UserName}&token={WebUtility.UrlEncode(token)}&key={WebUtility.UrlEncode(id)}";
+ string body = $"You Recently Signed In To Our Site With {providerName} Using The Email Address {email}. ";
+ body += "In Order To Complete The Linkage Of Your User Account Please Click The Link Displayed Below:\n\n" + url + "\n\nThank You!";
+ var notification = new Notification(alias.SiteId, email, email, "External Login Linkage", body);
+ _notifications.AddNotification(notification);
+
+ identity.Label = ExternalLoginStatus.VerificationRequired;
+ _logger.Log(alias.SiteId, LogLevel.Information, "ExternalLogin", Enums.LogFunction.Create, "External Login Linkage Verification For Provider {Provider} Sent To {Email}", providerName, email);
+ }
+ else
+ {
+ // provider keys do not match
+ identity.Label = ExternalLoginStatus.ProviderKeyMismatch;
+ _logger.Log(LogLevel.Error, "ExternalLogin", Enums.LogFunction.Security, "Provider Key Does Not Match For User {Username}. Login Denied.", identityuser.UserName);
+ }
+ }
+ }
+ else // email invalid
+ {
+ identity.Label = ExternalLoginStatus.InvalidEmail;
+ 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
+ {
+ _logger.Log(LogLevel.Error, "ExternalLogin", Enums.LogFunction.Security, "Provider Did Not Return An Email Address To Uniquely Identify The User. The Email Claim Specified Was {EmailCLaimType} And Actual Claim Types Are {Claims}. Login Denied.", httpContext.GetSiteSettings().GetValue("ExternalLogin:EmailClaimType", ""), claims);
}
}
}
- else // email invalid
+
+ // manage user
+ if (user != null)
{
- identity.Label = ExternalLoginStatus.InvalidEmail;
- 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
- {
- _logger.Log(LogLevel.Error, "ExternalLogin", Enums.LogFunction.Security, "Provider Did Not Return An Email To Uniquely Identify The User.");
- }
+ // create claims identity
+ var _userRoles = httpContext.RequestServices.GetRequiredService();
+ identity = UserSecurity.CreateClaimsIdentity(alias, user, _userRoles.GetUserRoles(user.UserId, user.SiteId).ToList());
+ identity.Label = ExternalLoginStatus.Success;
+
+ // update user
+ user.LastLoginOn = DateTime.UtcNow;
+ user.LastIPAddress = httpContext.Connection.RemoteIpAddress.ToString();
+ _users.UpdateUser(user);
+ _logger.Log(LogLevel.Information, "ExternalLogin", Enums.LogFunction.Security, "External User Login Successful For {Username} Using Provider {Provider}", user.Username, providerName);
}
}
-
- // manage user
- if (user != null)
+ else // id invalid
{
- // create claims identity
- var _userRoles = httpContext.RequestServices.GetRequiredService();
- identity = UserSecurity.CreateClaimsIdentity(alias, user, _userRoles.GetUserRoles(user.UserId, user.SiteId).ToList());
- identity.Label = ExternalLoginStatus.Success;
-
- // update user
- user.LastLoginOn = DateTime.UtcNow;
- user.LastIPAddress = httpContext.Connection.RemoteIpAddress.ToString();
- _users.UpdateUser(user);
- _logger.Log(LogLevel.Information, "ExternalLogin", Enums.LogFunction.Security, "External User Login Successful For {Username} Using Provider {Provider}", user.Username, providerName);
+ _logger.Log(LogLevel.Error, "ExternalLogin", Enums.LogFunction.Security, "Provider Did Not Return An Identifier To Uniquely Identify The User. The Identifier Claim Specified Was {IdentifierCLaimType} And Actual Claim Types Are {Claims}. Login Denied.", httpContext.GetSiteSettings().GetValue("ExternalLogin:IdentifierClaimType", ""), claims);
}
return identity;
diff --git a/Oqtane.Server/Repository/AliasRepository.cs b/Oqtane.Server/Repository/AliasRepository.cs
index 1e33d3d1..c2d8d690 100644
--- a/Oqtane.Server/Repository/AliasRepository.cs
+++ b/Oqtane.Server/Repository/AliasRepository.cs
@@ -96,7 +96,7 @@ namespace Oqtane.Repository
alias = new Alias();
alias.TenantId = aliases.First().TenantId;
alias.SiteId = aliases.First().SiteId;
- alias.Name = string.Join("/", segments.ToArray(), 0, start);
+ alias.Name = segments[0]; // root domain
alias.IsDefault = false;
alias = AddAlias(alias);
}