add support for external login parameters and improve diagnostic messages related to claims

This commit is contained in:
Shaun Walker
2022-05-12 13:51:46 -04:00
parent 53ead7a03f
commit 1978bf151f
4 changed files with 166 additions and 112 deletions

View File

@ -259,6 +259,12 @@ else
<input id="scopes" class="form-control" @bind="@_scopes" /> <input id="scopes" class="form-control" @bind="@_scopes" />
</div> </div>
</div> </div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="parameters" HelpText="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." ResourceKey="Parameters">Parameters:</Label>
<div class="col-sm-9">
<input id="parameters" class="form-control" @bind="@_parameters" />
</div>
</div>
<div class="row mb-1 align-items-center"> <div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="pkce" HelpText="Indicate if the provider supports Proof Key for Code Exchange (PKCE)" ResourceKey="PKCE">Use PKCE?</Label> <Label Class="col-sm-3" For="pkce" HelpText="Indicate if the provider supports Proof Key for Code Exchange (PKCE)" ResourceKey="PKCE">Use PKCE?</Label>
<div class="col-sm-9"> <div class="col-sm-9">
@ -380,6 +386,7 @@ else
private string _clientsecrettype = "password"; private string _clientsecrettype = "password";
private string _toggleclientsecret = string.Empty; private string _toggleclientsecret = string.Empty;
private string _scopes; private string _scopes;
private string _parameters;
private string _pkce; private string _pkce;
private string _redirecturl; private string _redirecturl;
private string _identifierclaimtype; private string _identifierclaimtype;
@ -432,6 +439,7 @@ else
_clientsecret = SettingService.GetSetting(settings, "ExternalLogin:ClientSecret", ""); _clientsecret = SettingService.GetSetting(settings, "ExternalLogin:ClientSecret", "");
_toggleclientsecret = SharedLocalizer["ShowPassword"]; _toggleclientsecret = SharedLocalizer["ShowPassword"];
_scopes = SettingService.GetSetting(settings, "ExternalLogin:Scopes", ""); _scopes = SettingService.GetSetting(settings, "ExternalLogin:Scopes", "");
_parameters = SettingService.GetSetting(settings, "ExternalLogin:Parameters", "");
_pkce = SettingService.GetSetting(settings, "ExternalLogin:PKCE", "false"); _pkce = SettingService.GetSetting(settings, "ExternalLogin:PKCE", "false");
_redirecturl = PageState.Uri.Scheme + "://" + PageState.Alias.Name + "/signin-" + _providertype; _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"); _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:ClientId", _clientid, true);
settings = SettingService.SetSetting(settings, "ExternalLogin:ClientSecret", _clientsecret, true); settings = SettingService.SetSetting(settings, "ExternalLogin:ClientSecret", _clientsecret, true);
settings = SettingService.SetSetting(settings, "ExternalLogin:Scopes", _scopes, 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:PKCE", _pkce, true);
settings = SettingService.SetSetting(settings, "ExternalLogin:IdentifierClaimType", _identifierclaimtype, true); settings = SettingService.SetSetting(settings, "ExternalLogin:IdentifierClaimType", _identifierclaimtype, true);
settings = SettingService.SetSetting(settings, "ExternalLogin:EmailClaimType", _emailclaimtype, true); settings = SettingService.SetSetting(settings, "ExternalLogin:EmailClaimType", _emailclaimtype, true);

View File

@ -384,4 +384,10 @@
<data name="IdentifierClaimType.Text" xml:space="preserve"> <data name="IdentifierClaimType.Text" xml:space="preserve">
<value>Identifier Claim:</value> <value>Identifier Claim:</value>
</data> </data>
<data name="Parameters.HelpText" xml:space="preserve">
<value>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.</value>
</data>
<data name="Parameters.Text" xml:space="preserve">
<value>Parameters:</value>
</data>
</root> </root>

View File

@ -66,6 +66,20 @@ namespace Oqtane.Extensions
options.Events.OnTokenValidated = OnTokenValidated; options.Events.OnTokenValidated = OnTokenValidated;
options.Events.OnAccessDenied = OnAccessDenied; options.Events.OnAccessDenied = OnAccessDenied;
options.Events.OnRemoteFailure = OnRemoteFailure; 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.OnTicketReceived = OnTicketReceived;
options.Events.OnAccessDenied = OnAccessDenied; options.Events.OnAccessDenied = OnAccessDenied;
options.Events.OnRemoteFailure = OnRemoteFailure; 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 // OAuth 2.0
var email = ""; var email = "";
var id = ""; var id = "";
var claims = "";
if (context.Options.UserInformationEndpoint != "") if (context.Options.UserInformationEndpoint != "")
{ {
@ -123,16 +154,16 @@ namespace Oqtane.Extensions
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", context.AccessToken); request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", context.AccessToken);
var response = await context.Backchannel.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, context.HttpContext.RequestAborted); var response = await context.Backchannel.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, context.HttpContext.RequestAborted);
response.EnsureSuccessStatusCode(); response.EnsureSuccessStatusCode();
var output = await response.Content.ReadAsStringAsync(); claims = await response.Content.ReadAsStringAsync();
// parse json output // parse json output
var idClaimType = context.HttpContext.GetSiteSettings().GetValue("ExternalLogin:IdentifierClaimType", ""); var idClaimType = context.HttpContext.GetSiteSettings().GetValue("ExternalLogin:IdentifierClaimType", "");
var emailClaimType = context.HttpContext.GetSiteSettings().GetValue("ExternalLogin:EmailClaimType", ""); 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()) foreach (var item in items.AsArray())
{ {
if (item[emailClaimType] != null) if (item[emailClaimType] != null)
@ -161,7 +192,7 @@ namespace Oqtane.Extensions
} }
// validate user // validate user
var identity = await ValidateUser(email, id, context.HttpContext); var identity = await ValidateUser(email, id, claims, context.HttpContext);
if (identity.Label == ExternalLoginStatus.Success) if (identity.Label == ExternalLoginStatus.Success)
{ {
identity.AddClaim(new Claim("access_token", context.AccessToken)); identity.AddClaim(new Claim("access_token", context.AccessToken));
@ -193,9 +224,10 @@ namespace Oqtane.Extensions
var id = context.Principal.FindFirstValue(idClaimType); var id = context.Principal.FindFirstValue(idClaimType);
var emailClaimType = context.HttpContext.GetSiteSettings().GetValue("ExternalLogin:EmailClaimType", ""); var emailClaimType = context.HttpContext.GetSiteSettings().GetValue("ExternalLogin:EmailClaimType", "");
var email = context.Principal.FindFirstValue(emailClaimType); var email = context.Principal.FindFirstValue(emailClaimType);
var claims = string.Join(", ", context.Principal.Claims.Select(item => item.Type).ToArray());
// validate user // validate user
var identity = await ValidateUser(email, id, context.HttpContext); var identity = await ValidateUser(email, id, claims, context.HttpContext);
if (identity.Label == ExternalLoginStatus.Success) if (identity.Label == ExternalLoginStatus.Success)
{ {
identity.AddClaim(new Claim("access_token", context.SecurityToken.RawData)); identity.AddClaim(new Claim("access_token", context.SecurityToken.RawData));
@ -229,7 +261,7 @@ namespace Oqtane.Extensions
return Task.CompletedTask; return Task.CompletedTask;
} }
private static async Task<ClaimsIdentity> ValidateUser(string email, string id, HttpContext httpContext) private static async Task<ClaimsIdentity> ValidateUser(string email, string id, string claims, HttpContext httpContext)
{ {
var _logger = httpContext.RequestServices.GetRequiredService<ILogManager>(); var _logger = httpContext.RequestServices.GetRequiredService<ILogManager>();
ClaimsIdentity identity = new ClaimsIdentity(Constants.AuthenticationScheme); ClaimsIdentity identity = new ClaimsIdentity(Constants.AuthenticationScheme);
@ -241,7 +273,9 @@ namespace Oqtane.Extensions
var _users = httpContext.RequestServices.GetRequiredService<IUserRepository>(); var _users = httpContext.RequestServices.GetRequiredService<IUserRepository>();
User user = null; User user = null;
// verify if external user is already registerd for this site if (!string.IsNullOrEmpty(id))
{
// verify if external user is already registered for this site
var _identityUserManager = httpContext.RequestServices.GetRequiredService<UserManager<IdentityUser>>(); var _identityUserManager = httpContext.RequestServices.GetRequiredService<UserManager<IdentityUser>>();
var identityuser = await _identityUserManager.FindByLoginAsync(providerType + ":" + alias.SiteId.ToString(), id); var identityuser = await _identityUserManager.FindByLoginAsync(providerType + ":" + alias.SiteId.ToString(), id);
if (identityuser != null) if (identityuser != null)
@ -359,7 +393,7 @@ namespace Oqtane.Extensions
} }
else else
{ {
_logger.Log(LogLevel.Error, "ExternalLogin", Enums.LogFunction.Security, "Provider Did Not Return An Email To Uniquely Identify The User."); _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);
} }
} }
} }
@ -378,6 +412,11 @@ namespace Oqtane.Extensions
_users.UpdateUser(user); _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.Information, "ExternalLogin", Enums.LogFunction.Security, "External User Login Successful For {Username} Using Provider {Provider}", user.Username, providerName);
} }
}
else // id invalid
{
_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; return identity;
} }

View File

@ -96,7 +96,7 @@ namespace Oqtane.Repository
alias = new Alias(); alias = new Alias();
alias.TenantId = aliases.First().TenantId; alias.TenantId = aliases.First().TenantId;
alias.SiteId = aliases.First().SiteId; alias.SiteId = aliases.First().SiteId;
alias.Name = string.Join("/", segments.ToArray(), 0, start); alias.Name = segments[0]; // root domain
alias.IsDefault = false; alias.IsDefault = false;
alias = AddAlias(alias); alias = AddAlias(alias);
} }