Refactoring authentication to support server-side Blazor using a seamless login flow.

This commit is contained in:
Shaun Walker
2019-07-15 08:30:03 -04:00
parent f3c823e667
commit ce069ed45b
28 changed files with 307 additions and 86 deletions

View File

@ -4,11 +4,12 @@
@using Oqtane.Models
@using Oqtane.Services
@using Oqtane.Providers
@using Oqtane.Shared
@inherits ModuleBase
@inject IUriHelper UriHelper
@inject IJSRuntime jsRuntime
@inject IUserService UserService
@inject ServerAuthenticationStateProvider AuthStateProvider
@inject IServiceProvider ServiceProvider
<AuthorizeView>
<Authorizing>
@ -28,33 +29,62 @@
<label for="Password" class="control-label">Password: </label>
<input type="password" name="Password" class="form-control" placeholder="Password" @bind="@Password" />
</div>
<div class="form-group">
<div class="form-check form-check-inline">
<label class="form-check-label" for="Remember">Remember Me?</label>&nbsp;
<input type="checkbox" class="form-check-input" name="Remember" @bind="@Remember" />
</div>
</div>
<button type="button" class="btn btn-primary" @onclick="@Login">Login</button>
<NavLink class="btn btn-secondary" href="/">Cancel</NavLink>
<button type="button" class="btn btn-secondary" @onclick="@Cancel">Cancel</button>
</div>
</NotAuthorized>
</AuthorizeView>
@code {
public override SecurityAccessLevelEnum SecurityAccessLevel { get { return SecurityAccessLevelEnum.Anonymous; } }
public override SecurityAccessLevelEnum SecurityAccessLevel { get { return SecurityAccessLevelEnum.Anonymous; } }
public string Message { get; set; } = "<div class=\"alert alert-info\" role=\"alert\">Use host/password For Demo Access</div>";
public string Username { get; set; } = "";
public string Password { get; set; } = "";
public string Message { get; set; } = "<div class=\"alert alert-info\" role=\"alert\">Use host/password For Demo Access</div>";
public string Username { get; set; } = "";
public string Password { get; set; } = "";
public bool Remember { get; set; } = false;
private async Task Login()
private async Task Login()
{
User user = new User();
user.Username = Username;
user.Password = Password;
user.IsPersistent = Remember;
user = await UserService.LoginUserAsync(user);
if (user.IsAuthenticated)
{
User user = new User();
user.Username = Username;
user.Password = Password;
user = await UserService.LoginUserAsync(user);
if (user != null)
string ReturnUrl = PageState.QueryString["returnurl"];
var authstateprovider = (IdentityAuthenticationStateProvider)ServiceProvider.GetService(typeof(IdentityAuthenticationStateProvider));
if (authstateprovider == null)
{
AuthStateProvider.NotifyAuthenticationChanged();
UriHelper.NavigateTo(NavigateUrl("", true));
// server-side Blazor
var interop = new Interop(jsRuntime);
string antiforgerytoken = await interop.GetElementByName("__RequestVerificationToken");
var fields = new { __RequestVerificationToken = antiforgerytoken, username = Username, password = Password, remember = Remember, returnurl = ReturnUrl };
await interop.SubmitForm("/login/", fields);
}
else
{
Message = "<div class=\"alert alert-danger\" role=\"alert\">User Does Not Exist</div>";
// client-side Blazor
authstateprovider.NotifyAuthenticationChanged();
UriHelper.NavigateTo(NavigateUrl(ReturnUrl, true));
}
}
else
{
Message = "<div class=\"alert alert-danger\" role=\"alert\">Login Failed. Please Remember That Passwords Are Case Sensitive.</div>";
}
}
private void Cancel()
{
string ReturnUrl = PageState.QueryString["returnurl"];
UriHelper.NavigateTo(NavigateUrl(ReturnUrl));
}
}

View File

@ -7,14 +7,12 @@ using Oqtane.Models;
namespace Oqtane.Providers
{
public class ServerAuthenticationStateProvider : AuthenticationStateProvider
public class IdentityAuthenticationStateProvider : AuthenticationStateProvider
{
//private readonly IUserService UserService;
private readonly IUriHelper urihelper;
public ServerAuthenticationStateProvider(IUriHelper urihelper)
public IdentityAuthenticationStateProvider(IUriHelper urihelper)
{
//this.UserService = UserService;
this.urihelper = urihelper;
}
@ -25,6 +23,7 @@ namespace Oqtane.Providers
Uri uri = new Uri(urihelper.GetAbsoluteUri());
string apiurl = uri.Scheme + "://" + uri.Authority + "/~/api/User/authenticate";
User user = await http.GetJsonAsync<User>(apiurl);
var identity = user.IsAuthenticated
? new ClaimsIdentity(new[] { new Claim(ClaimTypes.Name, user.Username) }, "Identity.Application")
: new ClaimsIdentity();

View File

@ -32,8 +32,7 @@ namespace Oqtane.Services
public async Task<Alias> GetAliasAsync(int AliasId)
{
List<Alias> aliases = await http.GetJsonAsync<List<Alias>>(apiurl);
return aliases.Where(item => item.AliasId == AliasId).FirstOrDefault();
return await http.GetJsonAsync<Alias>(apiurl + "/" + AliasId.ToString());
}
public async Task AddAliasAsync(Alias alias)

View File

@ -8,6 +8,7 @@ namespace Oqtane.Services
{
Task<List<Module>> GetModulesAsync(int PageId);
Task<List<Module>> GetModulesAsync(int SiteId, string ModuleDefinitionName);
Task<Module> GetModuleAsync(int ModuleId);
Task AddModuleAsync(Module module);
Task UpdateModuleAsync(Module module);
Task DeleteModuleAsync(int ModuleId);

View File

@ -7,6 +7,7 @@ namespace Oqtane.Services
public interface IPageService
{
Task<List<Page>> GetPagesAsync(int SiteId);
Task<Page> GetPageAsync(int PageId);
Task AddPageAsync(Page page);
Task UpdatePageAsync(Page page);
Task DeletePageAsync(int PageId);

View File

@ -10,6 +10,8 @@ namespace Oqtane.Services
Task<User> GetUserAsync(int UserId);
Task<User> GetUserAsync(string Username);
Task AddUserAsync(User user);
Task UpdateUserAsync(User user);

View File

@ -39,6 +39,11 @@ namespace Oqtane.Services
return modules.ToList();
}
public async Task<Module> GetModuleAsync(int ModuleId)
{
return await http.GetJsonAsync<Module>(apiurl + "/" + ModuleId.ToString());
}
public async Task AddModuleAsync(Module module)
{
await http.PostJsonAsync(apiurl, module);

View File

@ -30,6 +30,11 @@ namespace Oqtane.Services
return pages.OrderBy(item => item.Order).ToList();
}
public async Task<Page> GetPageAsync(int PageId)
{
return await http.GetJsonAsync<Page>(apiurl + "/" + PageId.ToString());
}
public async Task AddPageAsync(Page page)
{
await http.PostJsonAsync(apiurl, page);

View File

@ -32,17 +32,7 @@ namespace Oqtane.Services
public async Task<Site> GetSiteAsync(int SiteId)
{
List<Site> sites = await http.GetJsonAsync<List<Site>>(apiurl);
Site site;
if (sites.Count == 1)
{
site = sites.FirstOrDefault();
}
else
{
site = sites.Where(item => item.SiteId == SiteId).FirstOrDefault();
}
return site;
return await http.GetJsonAsync<Site>(apiurl + "/" + SiteId.ToString());
}
public async Task AddSiteAsync(Site site)

View File

@ -35,8 +35,12 @@ namespace Oqtane.Services
public async Task<User> GetUserAsync(int UserId)
{
List<User> users = await http.GetJsonAsync<List<User>>(apiurl);
return users.Where(item => item.UserId == UserId).FirstOrDefault();
return await http.GetJsonAsync<User>(apiurl + "/" + UserId.ToString());
}
public async Task<User> GetUserAsync(string Username)
{
return await http.GetJsonAsync<User>(apiurl + "/name/" + Username);
}
public async Task AddUserAsync(User user)

View File

@ -13,17 +13,18 @@ namespace Oqtane.Shared
this.jsRuntime = jsRuntime;
}
public Task<string> SetCookie(string name, string value, int days)
public Task SetCookie(string name, string value, int days)
{
try
{
return jsRuntime.InvokeAsync<string>(
jsRuntime.InvokeAsync<string>(
"interop.setCookie",
name, value, days);
return Task.CompletedTask;
}
catch
{
return Task.FromResult(string.Empty);
return Task.CompletedTask;
}
}
@ -41,18 +42,48 @@ namespace Oqtane.Shared
}
}
public Task<string> AddCSS(string filename)
public Task AddCSS(string filename)
{
try
{
jsRuntime.InvokeAsync<string>(
"interop.addCSS",
filename);
return Task.CompletedTask;
}
catch
{
return Task.CompletedTask;
}
}
public Task<string> GetElementByName(string name)
{
try
{
return jsRuntime.InvokeAsync<string>(
"interop.addCSS",
filename);
"interop.getElementByName",
name);
}
catch
{
return Task.FromResult(string.Empty);
}
}
public Task SubmitForm(string path, object fields)
{
try
{
jsRuntime.InvokeAsync<string>(
"interop.submitForm",
path, fields);
return Task.CompletedTask;
}
catch
{
return Task.CompletedTask;
}
}
}
}

View File

@ -127,7 +127,7 @@ private async Task Refresh()
var authState = await AuthenticationStateProvider.GetAuthenticationStateAsync();
if (authState.User.Identity.IsAuthenticated)
{
user = await UserService.GetCurrentUserAsync();
user = await UserService.GetUserAsync(authState.User.Identity.Name);
}
}
else

View File

@ -25,7 +25,14 @@ namespace Oqtane.Shared
string url = pagestate.Alias.Path + "/" + path;
if (reload)
{
url += "?reload=true";
if (url.Contains("?"))
{
url += "&reload=true";
}
else
{
url += "?reload=true";
}
}
return url;
}

View File

@ -31,8 +31,8 @@ namespace Oqtane.Client
{
// register auth services
services.AddAuthorizationCore();
services.AddScoped<ServerAuthenticationStateProvider>();
services.AddScoped<AuthenticationStateProvider>(s => s.GetRequiredService<ServerAuthenticationStateProvider>());
services.AddScoped<IdentityAuthenticationStateProvider>();
services.AddScoped<AuthenticationStateProvider>(s => s.GetRequiredService<IdentityAuthenticationStateProvider>());
// register scoped core services
services.AddScoped<SiteState>();

View File

@ -1,10 +1,14 @@
@using Oqtane.Themes
@using Oqtane.Services
@using Oqtane.Providers
@using Oqtane.Shared
@using Oqtane.Models
@using Microsoft.JSInterop
@inherits ThemeObjectBase
@inject IUriHelper UriHelper
@inject IUserService UserService
@inject ServerAuthenticationStateProvider AuthStateProvider
@inject IJSRuntime jsRuntime
@inject IServiceProvider ServiceProvider
<AuthorizeView>
<Authorizing>
@ -22,13 +26,25 @@
@code {
private void LoginUser()
{
UriHelper.NavigateTo(NavigateUrl("login"));
UriHelper.NavigateTo(NavigateUrl("login?returnurl=" + PageState.Page.Path));
}
private async Task LogoutUser()
{
await UserService.LogoutUserAsync();
AuthStateProvider.NotifyAuthenticationChanged();
UriHelper.NavigateTo(NavigateUrl("", true));
var authstateprovider = (IdentityAuthenticationStateProvider)ServiceProvider.GetService(typeof(IdentityAuthenticationStateProvider));
if (authstateprovider == null)
{
// server-side Blazor
var interop = new Interop(jsRuntime);
await interop.SubmitForm("/logout/", "");
}
else
{
// client-side Blazor
authstateprovider.NotifyAuthenticationChanged();
UriHelper.NavigateTo(NavigateUrl("login", true));
}
}
}

View File

@ -29,5 +29,23 @@ window.interop = {
link.href = fileName;
head.appendChild(link);
},
submitForm: function (path, fields) {
const form = document.createElement('form');
form.method = 'post';
form.action = path;
for (const key in fields) {
if (fields.hasOwnProperty(key)) {
const hiddenField = document.createElement('input');
hiddenField.type = 'hidden';
hiddenField.name = key;
hiddenField.value = fields[key];
form.appendChild(hiddenField);
}
}
document.body.appendChild(form);
form.submit();
}
};