Integrated AuthenticationStateProvider using .NET Core Identity

This commit is contained in:
Shaun Walker
2019-07-08 12:52:40 -04:00
parent 46821b8a10
commit 6cf1eb1c31
21 changed files with 565 additions and 137 deletions

View File

@ -1,9 +1,11 @@
@using Oqtane.Shared
@using Oqtane.Client.Shared
<CascadingValue Value="@PageState">
<SiteRouter OnStateChange="@ChangeState" />
</CascadingValue>
<CascadingAuthenticationState>
<CascadingValue Value="@PageState">
<SiteRouter OnStateChange="@ChangeState" />
</CascadingValue>
</CascadingAuthenticationState>
@code {
private PageState PageState { get; set; }

View File

@ -1,45 +1,56 @@
@using Microsoft.AspNetCore.Components.Routing
@using Oqtane.Shared
@using Oqtane.Modules
@using Microsoft.JSInterop
@using Oqtane.Models
@using Oqtane.Services
@using Oqtane.Client.Modules.Controls
@using Oqtane.Providers
@inherits ModuleBase
@inject IUriHelper UriHelper
@inject IJSRuntime jsRuntime
@inject IUserService UserService
@inject IUserService UserService
@inject ServerAuthenticationStateProvider AuthStateProvider
<div class="container">
@((MarkupString)Message)
<div class="form-group">
<label for="Username" class="control-label">Username: </label>
<input type="text" name="Username" class="form-control" placeholder="Username" @bind="@Username" />
</div>
<div class="form-group">
<label for="Password" class="control-label">Password: </label>
<input type="password" name="Password" class="form-control" placeholder="Password" @bind="@Password" />
</div>
<button type="button" class="btn btn-primary" @onclick="@Login">Login</button>
<NavLink class="btn btn-secondary" href="/">Cancel</NavLink>
</div>
<AuthorizeView>
<Authorizing>
<text>...</text>
</Authorizing>
<Authorized>
You are already logged in
</Authorized>
<NotAuthorized>
<div class="container">
@((MarkupString)Message)
<div class="form-group">
<label for="Username" class="control-label">Username: </label>
<input type="text" name="Username" class="form-control" placeholder="Username" @bind="@Username" />
</div>
<div class="form-group">
<label for="Password" class="control-label">Password: </label>
<input type="password" name="Password" class="form-control" placeholder="Password" @bind="@Password" />
</div>
<button type="button" class="btn btn-primary" @onclick="@Login">Login</button>
<NavLink class="btn btn-secondary" href="/">Cancel</NavLink>
</div>
</NotAuthorized>
</AuthorizeView>
@code {
public override SecurityAccessLevelEnum SecurityAccessLevel { get { return SecurityAccessLevelEnum.Anonymous; } }
public string Message { get; set; } = "<div class=\"alert alert-info\" role=\"alert\">Use host/host For Demo Access</div>";
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; } = "";
private async Task Login()
{
List<User> users = await UserService.GetUsersAsync();
User user = users.Where(item => item.Username == Username).FirstOrDefault();
User user = new User();
user.Username = Username;
user.Password = Password;
user = await UserService.LoginUserAsync(user);
if (user != null)
{
var interop = new Interop(jsRuntime);
await interop.SetCookie("user", user.UserId.ToString(), 7);
UriHelper.NavigateTo(NavigateUrl(""), true);
AuthStateProvider.NotifyAuthenticationChanged();
UriHelper.NavigateTo(NavigateUrl(""));
}
else
{

View File

@ -1,12 +1,10 @@
@using Microsoft.AspNetCore.Components.Routing
@using Oqtane.Shared
@using Oqtane.Modules
@using Microsoft.JSInterop
@using Oqtane.Client.Modules.Controls
@using Oqtane.Models
@using Oqtane.Services
@inherits ModuleBase
@inject IUriHelper UriHelper
@inject IJSRuntime jsRuntime
@inject IUserService UserService
<div class="container">
<div class="form-group">
@ -17,13 +15,25 @@
<label for="Password" class="control-label">Password: </label>
<input type="password" name="Password" class="form-control" placeholder="Password" @bind="@Password" />
</div>
<button type="button" class="btn btn-primary">Register</button>
<button type="button" class="btn btn-primary" @onclick="@RegisterUser">Register</button>
<NavLink class="btn btn-secondary" href="/">Cancel</NavLink>
</div>
@code {
public override SecurityAccessLevelEnum SecurityAccessLevel { get { return SecurityAccessLevelEnum.Anonymous; } }
public override SecurityAccessLevelEnum SecurityAccessLevel { get { return SecurityAccessLevelEnum.Anonymous; } }
public string Username { get; set; } = "";
public string Password { get; set; } = "";
public string Username { get; set; } = "";
public string Password { get; set; } = "";
private async Task RegisterUser()
{
User user = new User();
user.Username = Username;
user.DisplayName = Username;
user.Roles = "Administrators;";
user.IsSuperUser = false;
user.Password = Password;
await UserService.AddUserAsync(user);
UriHelper.NavigateTo("");
}
}

View File

@ -0,0 +1,39 @@
using System;
using System.Net.Http;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components;
using Oqtane.Models;
namespace Oqtane.Providers
{
public class ServerAuthenticationStateProvider : AuthenticationStateProvider
{
//private readonly IUserService UserService;
private readonly IUriHelper urihelper;
public ServerAuthenticationStateProvider(IUriHelper urihelper)
{
//this.UserService = UserService;
this.urihelper = urihelper;
}
public override async Task<AuthenticationState> GetAuthenticationStateAsync()
{
// hack: create a new HttpClient rather than relying on the registered service as the AuthenticationStateProvider is initialized prior to IUriHelper ( https://github.com/aspnet/AspNetCore/issues/11867 )
HttpClient http = new HttpClient();
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();
return new AuthenticationState(new ClaimsPrincipal(identity));
}
public void NotifyAuthenticationChanged()
{
NotifyAuthenticationStateChanged(GetAuthenticationStateAsync());
}
}
}

View File

@ -16,6 +16,12 @@ namespace Oqtane.Services
Task DeleteUserAsync(int UserId);
Task<User> GetCurrentUserAsync();
Task<User> LoginUserAsync(User user);
Task LogoutUserAsync();
bool IsAuthorized(User user, string accesscontrollist);
}
}

View File

@ -13,11 +13,13 @@ namespace Oqtane.Services
{
private readonly HttpClient http;
private readonly SiteState sitestate;
private readonly IUriHelper urihelper;
public UserService(HttpClient http, SiteState sitestate)
public UserService(HttpClient http, SiteState sitestate, IUriHelper urihelper)
{
this.http = http;
this.sitestate = sitestate;
this.urihelper = urihelper;
}
private string apiurl
@ -35,7 +37,7 @@ namespace Oqtane.Services
{
List<User> users = await http.GetJsonAsync<List<User>>(apiurl);
return users.Where(item => item.UserId == UserId).FirstOrDefault();
}
}
public async Task AddUserAsync(User user)
{
@ -51,6 +53,22 @@ namespace Oqtane.Services
await http.DeleteAsync(apiurl + "/" + UserId.ToString());
}
public async Task<User> GetCurrentUserAsync()
{
return await http.GetJsonAsync<User>(apiurl + "/current");
}
public async Task<User> LoginUserAsync(User user)
{
return await http.PostJsonAsync<User>(apiurl + "/login", user);
}
public async Task LogoutUserAsync()
{
// best practices recommend post is preferrable to get for logout
await http.PostJsonAsync(apiurl + "/logout", null);
}
// ACLs are stored in the format "!rolename1;![userid1];rolename2;rolename3;[userid2];[userid3]" where "!" designates Deny permissions
public bool IsAuthorized(User user, string accesscontrollist)
{

View File

@ -6,6 +6,7 @@
@using Oqtane.Shared
@using Microsoft.JSInterop
@using Microsoft.AspNetCore.Components.Routing
@inject AuthenticationStateProvider AuthenticationStateProvider
@inject SiteState SiteState
@inject IUriHelper UriHelper
@inject INavigationInterception NavigationInterception
@ -106,15 +107,13 @@ private async Task Refresh()
}
if (site != null || reload == true)
{
var interop = new Interop(jsRuntime);
string userid = await interop.GetCookie("user");
user = null;
if (PageState == null || reload == true)
{
if (!string.IsNullOrEmpty(userid))
var authState = await AuthenticationStateProvider.GetAuthenticationStateAsync();
if (authState.User.Identity.IsAuthenticated)
{
user = await UserService.GetUserAsync(int.Parse(userid));
user = await UserService.GetCurrentUserAsync();
}
}
else
@ -122,23 +121,6 @@ private async Task Refresh()
user = PageState.User;
}
if (!string.IsNullOrEmpty(userid))
{
if (user != null && user.UserId != int.Parse(userid))
{
user = await UserService.GetUserAsync(int.Parse(userid));
}
// this is a hack for server-side Blazor where JSInterop is not working OnInit() which means the userid is not being retrieved from the cookie on the initial render and is not being loaded into PageState
if (user == null)
{
user = await UserService.GetUserAsync(int.Parse(userid));
}
}
else
{
user = null;
}
string path = new Uri(_absoluteUri).PathAndQuery.Substring(1);
if (path.EndsWith("/")) { path = path.Substring(0, path.Length - 1); }
if (alias.Path != "")

View File

@ -8,6 +8,8 @@ using Microsoft.AspNetCore.Components;
using System.Reflection;
using Oqtane.Modules;
using Oqtane.Shared;
using Oqtane.Providers;
using Microsoft.AspNetCore.Blazor.Http;
namespace Oqtane.Client
{
@ -27,6 +29,11 @@ namespace Oqtane.Client
#if WASM
public void ConfigureServices(IServiceCollection services)
{
// register auth services
services.AddAuthorizationCore();
services.AddScoped<ServerAuthenticationStateProvider>();
services.AddScoped<AuthenticationStateProvider>(s => s.GetRequiredService<ServerAuthenticationStateProvider>());
// register scoped core services
services.AddScoped<SiteState>();
services.AddScoped<IModuleDefinitionService, ModuleDefinitionService>();
@ -39,6 +46,7 @@ namespace Oqtane.Client
services.AddScoped<IPageModuleService, PageModuleService>();
services.AddScoped<IUserService, UserService>();
// dynamically register module contexts and repository services
Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies();
foreach (Assembly assembly in assemblies)
@ -63,6 +71,7 @@ namespace Oqtane.Client
public void Configure(IComponentsApplicationBuilder app)
{
WebAssemblyHttpMessageHandler.DefaultCredentials = FetchCredentialsOption.Include;
app.AddComponent<App>("app");
}
#endif

View File

@ -1,41 +1,34 @@
@using Oqtane.Themes
@using Oqtane.Shared
@using Microsoft.JSInterop
@using Oqtane.Services
@using Oqtane.Providers
@inherits ThemeObjectBase
@inject IUriHelper UriHelper
@inject IJSRuntime jsRuntime
@inject IUserService UserService
@inject ServerAuthenticationStateProvider AuthStateProvider
<AuthorizeView>
<Authorizing>
<text>...</text>
</Authorizing>
<Authorized>
<button type="button" class="btn btn-primary" @onclick="@LogoutUser">Logout</button>
</Authorized>
<NotAuthorized>
<button type="button" class="btn btn-primary" @onclick="@LoginUser">Login</button>
</NotAuthorized>
</AuthorizeView>
<button type="button" class="btn btn-primary" @onclick="@Click">@name</button>
@code {
string name = "";
protected override async Task OnInitAsync()
private void LoginUser()
{
var interop = new Interop(jsRuntime);
string user = await interop.GetCookie("user");
if (user == "")
{
name = "Login";
}
else
{
name = "Logout";
}
UriHelper.NavigateTo(NavigateUrl("login"));
}
private async Task Click()
private async Task LogoutUser()
{
if (name == "Login")
{
UriHelper.NavigateTo(NavigateUrl("login"));
}
else
{
var interop = new Interop(jsRuntime);
await interop.SetCookie("user", "", 7);
UriHelper.NavigateTo(NavigateUrl(""), true);
}
await UserService.LogoutUserAsync();
AuthStateProvider.NotifyAuthenticationChanged();
UriHelper.NavigateTo(NavigateUrl(""));
}
}

View File

@ -1,35 +1,25 @@
@using Microsoft.AspNetCore.Components.Routing
@using Oqtane.Themes
@using Oqtane.Shared
@using Oqtane.Services;
@using Oqtane.Models;
@using Microsoft.JSInterop
@inject IJSRuntime jsRuntime
@using Oqtane.Themes
@inherits ThemeObjectBase
@inject IUriHelper UriHelper
<AuthorizeView>
<Authorizing>
<text>...</text>
</Authorizing>
<Authorized>
<button type="button" class="btn btn-primary" @onclick="@RegisterUser">@context.User.Identity.Name</button>
</Authorized>
<NotAuthorized>
<button type="button" class="btn btn-primary" @onclick="@RegisterUser">Register</button>
</NotAuthorized>
</AuthorizeView>
<NavLink class="btn btn-primary" href="@url">@name</NavLink>
@code {
string name = "";
string url = "";
protected override async Task OnInitAsync()
private void RegisterUser()
{
var interop = new Interop(jsRuntime);
string userid = await interop.GetCookie("user");
if (userid == "")
{
name = "Register";
url = "register";
}
else
{
if (PageState.User != null)
{
name = PageState.User.DisplayName;
}
}
UriHelper.NavigateTo(NavigateUrl("register"));
}
}