Add OAuth2 support

This commit is contained in:
Shaun Walker 2022-03-23 10:51:52 -04:00
parent ca17dd3ca3
commit 9d86d923aa
11 changed files with 601 additions and 293 deletions

View File

@ -21,7 +21,7 @@
<div class="Oqtane-Modules-Admin-Login" @onkeypress="@(e => KeyPressed(e))">
@if (_allowexternallogin)
{
<button type="button" class="btn btn-primary" @onclick="ExternalLogin">Use @PageState.Site.Settings["OpenIdConnectOptions:Provider"]</button>
<button type="button" class="btn btn-primary" @onclick="ExternalLogin">Use @PageState.Site.Settings["ExternalLogin:ProviderName"]</button>
<br /><br />
}
@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);
}
}

View File

@ -56,7 +56,7 @@ else
<TabPanel Name="Settings" Heading="Settings" ResourceKey="Settings">
<div class="container">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="allowregistration" HelpText="Do You Want To Allow Visitors To Be Able To Register For A User Account On This Site?" ResourceKey="AllowRegistration">Allow User Registration? </Label>
<Label Class="col-sm-3" For="allowregistration" HelpText="Do You Want To Allow Visitors To Be Able To Register For A User Account On This Site?" ResourceKey="AllowRegistration">Allow User Registration?</Label>
<div class="col-sm-9">
<select id="allowregistration" class="form-select" @bind="@_allowregistration" required>
<option value="True">@SharedLocalizer["Yes"]</option>
@ -131,62 +131,129 @@ else
</Section>
<Section Name="ExternalLogin" Heading="External Login Settings" ResourceKey="ExternalLoginSettings">
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="provider" HelpText="The OpenID Connect Provider Name. This Name Will Be Displayed On The Login Page" ResourceKey="Provider">Provider:</Label>
<Label Class="col-sm-3" For="providertype" HelpText="Select the external login provider type" ResourceKey="ProviderType">Provider Type:</Label>
<div class="col-sm-9">
<input id="provider" class="form-control" @bind="@_provider" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="authority" HelpText="The Authority Or Issuer URL Associated With The OpenID Connect Provider. " ResourceKey="Authority">Authority:</Label>
<div class="col-sm-9">
<input id="authority" class="form-control" @bind="@_authority" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="clientid" HelpText="The OpenID Connect Client ID" ResourceKey="ClientID">Client ID:</Label>
<div class="col-sm-9">
<input id="clientid" class="form-control" @bind="@_clientid" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="clientsecret" HelpText="The OpenID Connect Client Secret" ResourceKey="ClientSecret">Client Secret:</Label>
<div class="col-sm-9">
<input id="clientsecret" class="form-control" @bind="@_clientsecret" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="redirecturl" HelpText="The Redirect Url (or Callback Url) Which May Need To Be Registered With The OpenID Connect Provider" ResourceKey="RedirectUrl">Redirect Url:</Label>
<div class="col-sm-9">
<input id="redirecturl" class="form-control" @bind="@_redirecturl" readonly />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="emailclaimtype" HelpText="The Type Name For The Email Address Claim Provided By The OpenID Connect Provider" ResourceKey="EmailClaimType">Email Claim Type:</Label>
<div class="col-sm-9">
<input id="emailclaimtype" class="form-control" @bind="@_emailclaimtype" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="metadata" HelpText="The Discovery Endpoint For Obtaining Metadata. Only Specify If The OpenID Connect Provider Does Not Use The Standard Approach (ie. /.well-known/openid-configuration)" ResourceKey="Metadata">Metadata Address:</Label>
<div class="col-sm-9">
<input id="metadata" class="form-control" @bind="@_metadata" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="logouturl" HelpText="The Url For Logging Out The User From The OpenID Connect Provider. Only Specify If The OpenID Connect Provider Supports This Feature And You Do Not Want The User To Remain Signed In To The OpenID Connect Provider After Logging Out From The Site." ResourceKey="LogoutUrl">Logout Url:</Label>
<div class="col-sm-9">
<input id="logouturl" class="form-control" @bind="@_logouturl" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="allowsitelogin" HelpText="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 This Site." ResourceKey="AllowSiteLogin">Allow Site Login? </Label>
<div class="col-sm-9">
<select id="allowsitelogin" class="form-select" @bind="@_allowsitelogin" required>
<option value="true">@SharedLocalizer["Yes"]</option>
<option value="false">@SharedLocalizer["No"]</option>
<select id="providertype" class="form-select" value="@_providertype" @onchange="(e => ProviderTypeChanged(e))">
<option value="" selected>@Localizer["Not Specified"]</option>
<option value="oidc">@Localizer["OpenID Connect"]</option>
<option value="oauth2">@Localizer["OAuth 2.0"]</option>
</select>
</div>
</div>
@if (_providertype != "")
{
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="providername" HelpText="The external login provider name which will be displayed on the login page" ResourceKey="ProviderName">Provider Name:</Label>
<div class="col-sm-9">
<input id="providername" class="form-control" @bind="@_providername" />
</div>
</div>
}
@if (_providertype == "oidc")
{
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="authority" HelpText="The Authority Url or Issuer Url associated with the OpenID Connect provider" ResourceKey="Authority">Authority:</Label>
<div class="col-sm-9">
<input id="authority" class="form-control" @bind="@_authority" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="metadataurl" HelpText="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)" ResourceKey="MetadataUrl">Metadata Url:</Label>
<div class="col-sm-9">
<input id="metadataurl" class="form-control" @bind="@_metadataurl" />
</div>
</div>
}
@if (_providertype == "oauth2")
{
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="authorizationurl" HelpText="The endpoint for obtaining an Authorization Code" ResourceKey="AuthorizationUrl">Authorization Url:</Label>
<div class="col-sm-9">
<input id="authorizationurl" class="form-control" @bind="@_authorizationurl" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="tokenurl" HelpText="The endpoint for obtaining an Auth Token" ResourceKey="TokenUrl">Token Url:</Label>
<div class="col-sm-9">
<input id="tokenurl" class="form-control" @bind="@_tokenurl" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="userinfourl" HelpText="The endpoint for obtaining user information. This should be an API or Page Url which contains the users email address." ResourceKey="UserInfoUrl">User Info Url:</Label>
<div class="col-sm-9">
<input id="userinfourl" class="form-control" @bind="@_userinfourl" />
</div>
</div>
}
@if (_providertype != "")
{
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="clientid" HelpText="The Client ID from the provider" ResourceKey="ClientID">Client ID:</Label>
<div class="col-sm-9">
<input id="clientid" class="form-control" @bind="@_clientid" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="clientsecret" HelpText="The Client Secret from the provider" ResourceKey="ClientSecret">Client Secret:</Label>
<div class="col-sm-9">
<input type="password" id="clientsecret" class="form-control" @bind="@_clientsecret" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="scopes" HelpText="A list of Scopes to request from the provider (separated by commas). If none are specified, standard Scopes will be used by default." ResourceKey="Scopes">Scopes:</Label>
<div class="col-sm-9">
<input id="scopes" class="form-control" @bind="@_scopes" />
</div>
</div>
<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>
<div class="col-sm-9">
<select id="pkce" class="form-select" @bind="@_pkce" required>
<option value="true">@SharedLocalizer["Yes"]</option>
<option value="false">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="redirecturl" HelpText="The Redirect Url (or Callback Url) which usually needs to be registered with the provider" ResourceKey="RedirectUrl">Redirect Url:</Label>
<div class="col-sm-9">
<input id="redirecturl" class="form-control" @bind="@_redirecturl" readonly />
</div>
</div>
@if (_providertype == "oidc")
{
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="emailclaimtype" HelpText="The type name for the email address claim provided by the provider" ResourceKey="EmailClaimType">Email Claim Type:</Label>
<div class="col-sm-9">
<input id="emailclaimtype" class="form-control" @bind="@_emailclaimtype" />
</div>
</div>
}
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="domainfilter" HelpText="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." ResourceKey="DomainFilter">Domain Filter:</Label>
<div class="col-sm-9">
<input id="domainfilter" class="form-control" @bind="@_domainfilter" />
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="createusers" HelpText="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." ResourceKey="CreateUsers">Create New Users?</Label>
<div class="col-sm-9">
<select id="createusers" class="form-select" @bind="@_createusers">
<option value="true">@SharedLocalizer["Yes"]</option>
<option value="false">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
<div class="row mb-1 align-items-center">
<Label Class="col-sm-3" For="allowsitelogin" HelpText="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." ResourceKey="AllowSiteLogin">Allow Site Login?</Label>
<div class="col-sm-9">
<select id="allowsitelogin" class="form-select" @bind="@_allowsitelogin">
<option value="true">@SharedLocalizer["Yes"]</option>
<option value="false">@SharedLocalizer["No"]</option>
</select>
</div>
</div>
}
</Section>
</div>
<br />
@ -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<UserRole> 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();
}
}

View File

@ -154,43 +154,43 @@
<value>Roles</value>
</data>
<data name="LockoutDuration.HelpText" xml:space="preserve">
<value>The Number Of Minutes A User Should Be Locked Out</value>
<value>The number of minutes a user should be locked out</value>
</data>
<data name="LockoutDuration.Text" xml:space="preserve">
<value>Lockout Duration:</value>
</data>
<data name="MaximumFailures.HelpText" xml:space="preserve">
<value>The Maximum Number Of Sign In Attempts Before A User Is Locked Out</value>
<value>The maximum number of sign in attempts before a user is locked out</value>
</data>
<data name="MaximumFailures.Text" xml:space="preserve">
<value>Maximum Failures:</value>
</data>
<data name="RequireDigit.HelpText" xml:space="preserve">
<value>Indicate If Passwords Must Contain A Digit</value>
<value>Indicate if passwords must contain a digit</value>
</data>
<data name="RequireDigit.Text" xml:space="preserve">
<value>Require Digit?</value>
</data>
<data name="RequiredLength.HelpText" xml:space="preserve">
<value>The Minimum Length For A Password</value>
<value>The minimum length for a password</value>
</data>
<data name="RequiredLength.Text" xml:space="preserve">
<value>Minimum Length:</value>
</data>
<data name="RequireLower.HelpText" xml:space="preserve">
<value>Indicate If Passwords Must Contain A Lower Case Character</value>
<value>Indicate if passwords must contain a lower case character</value>
</data>
<data name="RequireLower.Text" xml:space="preserve">
<value>Require Lowercase?</value>
</data>
<data name="RequirePunctuation.HelpText" xml:space="preserve">
<value>Indicate if Passwords Must Contain A Non-alphanumeric Character (ie. Punctuation)</value>
<value>Indicate if passwords must contain a non-alphanumeric character (ie. punctuation)</value>
</data>
<data name="RequirePunctuation.Text" xml:space="preserve">
<value>Require Punctuation?</value>
</data>
<data name="RequireUpper.HelpText" xml:space="preserve">
<value>Indicate If Passwords Must Contain An Upper Case Character</value>
<value>Indicate if passwords must contain an upper case character</value>
</data>
<data name="RequireUpper.Text" xml:space="preserve">
<value>Require Uppercase?</value>
@ -199,9 +199,114 @@
<value>Configuration Updated. Please Select Restart Application For These Changes To Be Activated.</value>
</data>
<data name="UniqueCharacters.HelpText" xml:space="preserve">
<value>The Minimum Number Of Unique Characters Which A Password Must Contain</value>
<value>The minimum number of unique characters which a password must contain</value>
</data>
<data name="UniqueCharacters.Text" xml:space="preserve">
<value>Unique Characters:</value>
</data>
<data name="AllowSiteLogin.HelpText" xml:space="preserve">
<value>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.</value>
</data>
<data name="AllowSiteLogin.Text" xml:space="preserve">
<value>Allow Site Login?</value>
</data>
<data name="Authority.HelpText" xml:space="preserve">
<value>The Authority Url or Issuer Url associated with the OpenID Connect provider</value>
</data>
<data name="Authority.Text" xml:space="preserve">
<value>Authority:</value>
</data>
<data name="AuthorizationUrl.HelpText" xml:space="preserve">
<value>The endpoint for obtaining an Authorization Code</value>
</data>
<data name="AuthorizationUrl.Text" xml:space="preserve">
<value>Authorization Url:</value>
</data>
<data name="ClientID.HelpText" xml:space="preserve">
<value>The Client ID from the provider</value>
</data>
<data name="ClientID.Text" xml:space="preserve">
<value>Client ID:</value>
</data>
<data name="ClientSecret.HelpText" xml:space="preserve">
<value>The Client Secret from the provider</value>
</data>
<data name="ClientSecret.Text" xml:space="preserve">
<value>Client Secret:</value>
</data>
<data name="CreateUsers.HelpText" xml:space="preserve">
<value>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.</value>
</data>
<data name="CreateUsers.Text" xml:space="preserve">
<value>Create New Users?</value>
</data>
<data name="DomainFilter.HelpText" xml:space="preserve">
<value>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.</value>
</data>
<data name="DomainFilter.Text" xml:space="preserve">
<value>Domain Filter:</value>
</data>
<data name="EmailClaimType.HelpText" xml:space="preserve">
<value>The type name for the email address claim provided by the provider</value>
</data>
<data name="EmailClaimType.Text" xml:space="preserve">
<value>Email Claim Type:</value>
</data>
<data name="ExternalLoginSettings.Heading" xml:space="preserve">
<value>External Login Settings</value>
</data>
<data name="LockoutSettings.Heading" xml:space="preserve">
<value>Lockout Settings</value>
</data>
<data name="MetadataUrl.HelpText" xml:space="preserve">
<value>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)</value>
</data>
<data name="MetadataUrl.Text" xml:space="preserve">
<value>Metadata Url:</value>
</data>
<data name="PasswordSettings.Heading" xml:space="preserve">
<value>Password Settings</value>
</data>
<data name="PKCE.HelpText" xml:space="preserve">
<value>Indicate if the provider supports Proof Key for Code Exchange (PKCE)</value>
</data>
<data name="PKCE.Text" xml:space="preserve">
<value>Use PKCE?</value>
</data>
<data name="ProviderName.HelpText" xml:space="preserve">
<value>The external login provider name which will be displayed on the login page</value>
</data>
<data name="ProviderName.Text" xml:space="preserve">
<value>Provider Name:</value>
</data>
<data name="ProviderType.HelpText" xml:space="preserve">
<value>Select the external login provider type</value>
</data>
<data name="ProviderType.Text" xml:space="preserve">
<value>Provider Type:</value>
</data>
<data name="RedirectUrl.HelpText" xml:space="preserve">
<value>The Redirect Url (or Callback Url) which usually needs to be registered with the provider</value>
</data>
<data name="RedirectUrl.Text" xml:space="preserve">
<value>Redirect Url:</value>
</data>
<data name="Scopes.HelpText" xml:space="preserve">
<value>A list of Scopes to request from the provider (separated by commas). If none are specified, standard Scopes will be used by default.</value>
</data>
<data name="Scopes.Text" xml:space="preserve">
<value>Scopes:</value>
</data>
<data name="TokenUrl.HelpText" xml:space="preserve">
<value>The endpoint for obtaining an Auth Token</value>
</data>
<data name="TokenUrl.Text" xml:space="preserve">
<value>Token Url:</value>
</data>
<data name="UserInfoUrl.HelpText" xml:space="preserve">
<value>The endpoint for obtaining user information. This should be an API or Page Url which contains the users email address.</value>
</data>
<data name="UserInfoUrl.Text" xml:space="preserve">
<value>User Info Url:</value>
</data>
</root>

View File

@ -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<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
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<AuthenticationOptions>((options, alias) =>
// site OAuth2.0 options
builder.AddSiteOptions<OAuthOptions>((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<ILogManager>();
_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<ILogManager>();
// 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<ILogManager>();
_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<ILogManager>();
_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<ILogManager>();
var alias = httpContext.GetAlias();
if (EmailValid(email, alias.SiteSettings.GetValue("ExternalLogin:DomainFilter", "")))
{
var _identityUserManager = context.HttpContext.RequestServices.GetRequiredService<UserManager<IdentityUser>>();
var _users = context.HttpContext.RequestServices.GetRequiredService<IUserRepository>();
var _userRoles = context.HttpContext.RequestServices.GetRequiredService<IUserRoleRepository>();
var _identityUserManager = httpContext.RequestServices.GetRequiredService<UserManager<IdentityUser>>();
var _users = httpContext.RequestServices.GetRequiredService<IUserRepository>();
var _userRoles = httpContext.RequestServices.GetRequiredService<IUserRoleRepository>();
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<IFolderRepository>();
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<IFolderRepository>();
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<Permission>
_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<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);
// add auto assigned roles to user for site
var _roles = 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
{
_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<UserRole> 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<ILogManager>();
_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<ILogManager>();
_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;
}
}
}

View File

@ -0,0 +1,3 @@
@page "/pages/external"
@namespace Oqtane.Pages
@model Oqtane.Pages.ExternalModel

View File

@ -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();
}
}
}
}

View File

@ -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));
}
}
}

View File

@ -1,3 +0,0 @@
@page "/pages/oidc"
@namespace Oqtane.Pages
@model Oqtane.Pages.OIDCModel

View File

@ -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 });
}
}
}

View File

@ -115,7 +115,8 @@ namespace Oqtane
options.DefaultChallengeScheme = Constants.AuthenticationScheme;
})
.AddCookie(Constants.AuthenticationScheme)
.AddOpenIdConnect();
.AddOpenIdConnect("oidc", options => { })
.AddOAuth("oauth2", options => { });
services.ConfigureOqtaneCookieOptions();

View File

@ -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);
}
}
}
}