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,6 +4,8 @@ using Oqtane.Repository;
using Oqtane.Models;
using Microsoft.AspNetCore.Identity;
using System.Threading.Tasks;
using Microsoft.Extensions.Primitives;
using System.Security.Claims;
namespace Oqtane.Controllers
{
@ -34,7 +36,7 @@ namespace Oqtane.Controllers
{
return users.GetUser(id);
}
// POST api/<controller>
[HttpPost]
public async Task Post([FromBody] User user)
@ -73,54 +75,48 @@ namespace Oqtane.Controllers
users.DeleteUser(id);
}
// GET api/<controller>/current
[HttpGet("current")]
public User Current()
// GET api/<controller>/name/x
[HttpGet("name/{name}")]
public User GetByName(string name)
{
User user = null;
if (User.Identity.IsAuthenticated)
{
user = users.GetUser(User.Identity.Name);
user.IsAuthenticated = true;
}
return user;
return users.GetUser(name);
}
// POST api/<controller>/login
[HttpPost("login")]
public async Task<User> Login([FromBody] User user)
{
// TODO: seed host user - this logic should be moved to installation
IdentityUser identityuser = await identityUserManager.FindByNameAsync("host");
if (identityuser == null)
{
var result = await identityUserManager.CreateAsync(new IdentityUser { UserName = "host", Email = "host" }, "password");
if (result.Succeeded)
{
users.AddUser(new Models.User { Username = "host", DisplayName = "host", IsSuperUser = true, Roles = "" });
}
}
if (ModelState.IsValid)
{
// seed host user - this logic should be moved to installation
IdentityUser identityuser = await identityUserManager.FindByNameAsync("host");
if (identityuser == null)
{
var result = await identityUserManager.CreateAsync(new IdentityUser { UserName = "host", Email = "host" }, "password");
if (result.Succeeded)
{
users.AddUser(new Models.User { Username = "host", DisplayName = "host", IsSuperUser = true, Roles = "" });
}
}
identityuser = await identityUserManager.FindByNameAsync(user.Username);
if (identityuser != null)
{
var result = await identitySignInManager.CheckPasswordSignInAsync(identityuser, user.Password, false);
if (result.Succeeded)
{
await identitySignInManager.SignInAsync(identityuser, false);
await identitySignInManager.SignInAsync(identityuser, user.IsPersistent);
user = users.GetUser(identityuser.UserName);
user.IsAuthenticated = true;
}
else
{
user = null;
user = new Models.User { Username = user.Username, IsAuthenticated = false };
}
}
else
{
user = null;
user = new Models.User { Username = user.Username, IsAuthenticated = false };
}
}
return user;

View File

@ -0,0 +1,3 @@
@page "/login"
@namespace Oqtane.Pages
@model Oqtane.Pages.LoginModel

View File

@ -0,0 +1,52 @@
using System;
using System.Collections.Generic;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace Oqtane.Pages
{
[AllowAnonymous]
public class LoginModel : PageModel
{
private readonly UserManager<IdentityUser> identityUserManager;
private readonly SignInManager<IdentityUser> identitySignInManager;
public LoginModel(UserManager<IdentityUser> IdentityUserManager, SignInManager<IdentityUser> IdentitySignInManager)
{
identityUserManager = IdentityUserManager;
identitySignInManager = IdentitySignInManager;
}
public async Task<IActionResult> OnPostAsync(string username, string password, bool remember, string returnurl)
{
await HttpContext.SignOutAsync(IdentityConstants.ApplicationScheme);
bool validuser = false;
IdentityUser identityuser = await identityUserManager.FindByNameAsync(username);
if (identityuser != null)
{
var result = await identitySignInManager.CheckPasswordSignInAsync(identityuser, password, false);
if (result.Succeeded)
{
validuser = true;
}
}
if (validuser)
{
var claims = new List<Claim>{ new Claim(ClaimTypes.Name, username) };
var claimsIdentity = new ClaimsIdentity(claims, IdentityConstants.ApplicationScheme);
var authProperties = new AuthenticationProperties{IsPersistent = remember};
await HttpContext.SignInAsync(IdentityConstants.ApplicationScheme, new ClaimsPrincipal(claimsIdentity), authProperties);
}
return LocalRedirect(Url.Content("~/" + returnurl));
}
}
}

View File

@ -0,0 +1,3 @@
@page "/logout"
@namespace Oqtane.Pages
@model Oqtane.Pages.LogoutModel

View File

@ -0,0 +1,26 @@
using System;
using System.Collections.Generic;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Oqtane.Models;
namespace Oqtane.Pages
{
[IgnoreAntiforgeryToken(Order = 1001)]
[AllowAnonymous]
public class LogoutModel : PageModel
{
public async Task<IActionResult> OnPostAsync()
{
await HttpContext.SignOutAsync(IdentityConstants.ApplicationScheme);
return LocalRedirect(Url.Content("~/"));
}
}
}

View File

@ -14,6 +14,7 @@
<link href="css/site.css" rel="stylesheet" />
</head>
<body>
@(Html.AntiForgeryToken())
<app>@(await Html.RenderComponentAsync<App>())</app>
<script src="js/site.js"></script>

View File

@ -52,6 +52,7 @@ namespace Oqtane.Repository
{
/// determine if this module implements IModule
Type moduletype = assembly.GetTypes()
.Where(item => item.Namespace != null)
.Where(item => item.Namespace.StartsWith(ModuleType))
.Where(item => item.GetInterfaces().Contains(typeof(IModule)))
.FirstOrDefault();

View File

@ -24,7 +24,7 @@ namespace Oqtane.Repository
aliasname = accessor.HttpContext.Request.Host.Value;
string path = accessor.HttpContext.Request.Path.Value;
string[] segments = path.Split('/');
if (segments[1] != "~")
if (segments[0] == "api" && segments[1] != "~")
{
aliasname += "/" + segments[1];
}

View File

@ -52,6 +52,7 @@ namespace Oqtane.Repository
{
/// determine if this theme implements ITheme
Type themeType = assembly.GetTypes()
.Where(item => item.Namespace != null)
.Where(item => item.Namespace.StartsWith(Namespace))
.Where(item => item.GetInterfaces().Contains(typeof(ITheme))).FirstOrDefault();
if (themeType != null)
@ -105,7 +106,10 @@ namespace Oqtane.Repository
theme.ThemeControls += (themeControlType.FullName + ", " + typename[1] + ";");
}
// containers
Type[] containertypes = assembly.GetTypes().Where(item => item.Namespace.StartsWith(Namespace)).Where(item => item.GetInterfaces().Contains(typeof(IContainerControl))).ToArray();
Type[] containertypes = assembly.GetTypes()
.Where(item => item.Namespace != null)
.Where(item => item.Namespace.StartsWith(Namespace))
.Where(item => item.GetInterfaces().Contains(typeof(IContainerControl))).ToArray();
foreach (Type containertype in containertypes)
{
theme.ContainerControls += (containertype.FullName + ", " + typename[1] + ";");

View File

@ -17,12 +17,9 @@ using System.Runtime.Loader;
using Oqtane.Services;
using System.Net.Http;
using Microsoft.AspNetCore.Components;
using Oqtane.Client;
using Oqtane.Shared;
using Microsoft.AspNetCore.Identity;
using Oqtane.Providers;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication.Cookies;
namespace Oqtane.Server
{
@ -47,25 +44,27 @@ namespace Oqtane.Server
services.AddRazorPages();
services.AddServerSideBlazor();
// server-side Blazor does not register HttpClient by default
// setup HttpClient for server side in a client side compatible fashion ( with auth cookie )
if (!services.Any(x => x.ServiceType == typeof(HttpClient)))
{
// setup HttpClient for server side in a client side compatible fashion
services.AddScoped<HttpClient>(s =>
{
// creating the URI helper needs to wait until the JS Runtime is initialized, so defer it.
var uriHelper = s.GetRequiredService<IUriHelper>();
return new HttpClient
var httpContextAccessor = s.GetRequiredService<IHttpContextAccessor>();
var authToken = httpContextAccessor.HttpContext.Request.Cookies[".AspNetCore.Identity.Application"];
var client = new HttpClient(new HttpClientHandler { UseCookies = false });
if (authToken != null)
{
BaseAddress = new Uri(uriHelper.GetBaseUri())
};
client.DefaultRequestHeaders.Add("Cookie", ".AspNetCore.Identity.Application=" + authToken);
}
client.BaseAddress = new Uri(uriHelper.GetBaseUri());
return client;
});
}
// register auth services
// register auth services
services.AddAuthorizationCore();
services.AddScoped<ServerAuthenticationStateProvider>();
services.AddScoped<AuthenticationStateProvider>(s => s.GetRequiredService<ServerAuthenticationStateProvider>());
// register scoped core services
services.AddScoped<SiteState>();

View File

@ -20,6 +20,14 @@ window.interop = {
}
return "";
},
getElementByName: function (name) {
var elements = document.getElementsByName(name);
if (elements.length) {
return elements[0].value;
} else {
return "";
}
},
addCSS: function (fileName) {
var head = document.head;
var link = document.createElement("link");
@ -29,5 +37,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();
}
};