diff --git a/Oqtane.Client/App.razor b/Oqtane.Client/App.razor index 5a5698af..3446ccbf 100644 --- a/Oqtane.Client/App.razor +++ b/Oqtane.Client/App.razor @@ -1,9 +1,11 @@ @using Oqtane.Shared @using Oqtane.Client.Shared - - - + + + + + @code { private PageState PageState { get; set; } diff --git a/Oqtane.Client/Modules/Admin/Login/Index.razor b/Oqtane.Client/Modules/Admin/Login/Index.razor index bd06d715..9a76cb88 100644 --- a/Oqtane.Client/Modules/Admin/Login/Index.razor +++ b/Oqtane.Client/Modules/Admin/Login/Index.razor @@ -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 -
- @((MarkupString)Message) -
- - -
-
- - -
- - Cancel -
+ + + ... + + + You are already logged in + + +
+ @((MarkupString)Message) +
+ + +
+
+ + +
+ + Cancel +
+
+
@code { public override SecurityAccessLevelEnum SecurityAccessLevel { get { return SecurityAccessLevelEnum.Anonymous; } } - public string Message { get; set; } = "
Use host/host For Demo Access
"; + public string Message { get; set; } = "
Use host/password For Demo Access
"; public string Username { get; set; } = ""; public string Password { get; set; } = ""; private async Task Login() { - List 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 { diff --git a/Oqtane.Client/Modules/Admin/Register/Index.razor b/Oqtane.Client/Modules/Admin/Register/Index.razor index 67120c3d..d2380ba2 100644 --- a/Oqtane.Client/Modules/Admin/Register/Index.razor +++ b/Oqtane.Client/Modules/Admin/Register/Index.razor @@ -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
@@ -17,13 +15,25 @@
- + Cancel
@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(""); +} } diff --git a/Oqtane.Client/Providers/ServerAuthenticationStateProvider.cs b/Oqtane.Client/Providers/ServerAuthenticationStateProvider.cs new file mode 100644 index 00000000..818ed0c7 --- /dev/null +++ b/Oqtane.Client/Providers/ServerAuthenticationStateProvider.cs @@ -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 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(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()); + } + } +} diff --git a/Oqtane.Client/Services/IUserService.cs b/Oqtane.Client/Services/IUserService.cs index bce0e7b9..c11e8e80 100644 --- a/Oqtane.Client/Services/IUserService.cs +++ b/Oqtane.Client/Services/IUserService.cs @@ -16,6 +16,12 @@ namespace Oqtane.Services Task DeleteUserAsync(int UserId); + Task GetCurrentUserAsync(); + + Task LoginUserAsync(User user); + + Task LogoutUserAsync(); + bool IsAuthorized(User user, string accesscontrollist); } } diff --git a/Oqtane.Client/Services/UserService.cs b/Oqtane.Client/Services/UserService.cs index bc00f00a..fbe718e1 100644 --- a/Oqtane.Client/Services/UserService.cs +++ b/Oqtane.Client/Services/UserService.cs @@ -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 users = await http.GetJsonAsync>(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 GetCurrentUserAsync() + { + return await http.GetJsonAsync(apiurl + "/current"); + } + + public async Task LoginUserAsync(User user) + { + return await http.PostJsonAsync(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) { diff --git a/Oqtane.Client/Shared/SiteRouter.razor b/Oqtane.Client/Shared/SiteRouter.razor index 65d15f27..fd30c9d8 100644 --- a/Oqtane.Client/Shared/SiteRouter.razor +++ b/Oqtane.Client/Shared/SiteRouter.razor @@ -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 != "") diff --git a/Oqtane.Client/Startup.cs b/Oqtane.Client/Startup.cs index 7f1b7078..3ed914c5 100644 --- a/Oqtane.Client/Startup.cs +++ b/Oqtane.Client/Startup.cs @@ -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(); + services.AddScoped(s => s.GetRequiredService()); + // register scoped core services services.AddScoped(); services.AddScoped(); @@ -39,6 +46,7 @@ namespace Oqtane.Client services.AddScoped(); services.AddScoped(); + // 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"); } #endif diff --git a/Oqtane.Client/Themes/Controls/Login.razor b/Oqtane.Client/Themes/Controls/Login.razor index a96ae9b9..92e006d1 100644 --- a/Oqtane.Client/Themes/Controls/Login.razor +++ b/Oqtane.Client/Themes/Controls/Login.razor @@ -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 + + + + ... + + + + + + + + - @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("")); } } diff --git a/Oqtane.Client/Themes/Controls/Profile.razor b/Oqtane.Client/Themes/Controls/Profile.razor index e6afd7de..0ff591a3 100644 --- a/Oqtane.Client/Themes/Controls/Profile.razor +++ b/Oqtane.Client/Themes/Controls/Profile.razor @@ -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 + + + + ... + + + + + + + + -@name @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")); } } diff --git a/Oqtane.Server/Controllers/UserController.cs b/Oqtane.Server/Controllers/UserController.cs index c2ce2932..cfad0479 100644 --- a/Oqtane.Server/Controllers/UserController.cs +++ b/Oqtane.Server/Controllers/UserController.cs @@ -2,6 +2,8 @@ using Microsoft.AspNetCore.Mvc; using Oqtane.Repository; using Oqtane.Models; +using Microsoft.AspNetCore.Identity; +using System.Threading.Tasks; namespace Oqtane.Controllers { @@ -9,10 +11,14 @@ namespace Oqtane.Controllers public class UserController : Controller { private readonly IUserRepository users; + private readonly UserManager identityUserManager; + private readonly SignInManager identitySignInManager; - public UserController(IUserRepository Users) + public UserController(IUserRepository Users, UserManager IdentityUserManager, SignInManager IdentitySignInManager) { users = Users; + identityUserManager = IdentityUserManager; + identitySignInManager = IdentitySignInManager; } // GET: api/ @@ -31,10 +37,23 @@ namespace Oqtane.Controllers // POST api/ [HttpPost] - public void Post([FromBody] User user) + public async Task Post([FromBody] User user) { if (ModelState.IsValid) - users.AddUser(user); + { + IdentityUser identityuser = await identityUserManager.FindByNameAsync(user.Username); + if (identityuser == null) + { + identityuser = new IdentityUser(); + identityuser.UserName = user.Username; + identityuser.Email = user.Username; + var result = await identityUserManager.CreateAsync(identityuser, user.Password); + if (result.Succeeded) + { + users.AddUser(user); + } + } + } } // PUT api//5 @@ -42,7 +61,9 @@ namespace Oqtane.Controllers public void Put(int id, [FromBody] User user) { if (ModelState.IsValid) + { users.UpdateUser(user); + } } // DELETE api//5 @@ -51,5 +72,72 @@ namespace Oqtane.Controllers { users.DeleteUser(id); } + + // GET api//current + [HttpGet("current")] + public User Current() + { + User user = null; + if (User.Identity.IsAuthenticated) + { + user = users.GetUser(User.Identity.Name); + user.IsAuthenticated = true; + } + return user; + } + + // POST api//login + [HttpPost("login")] + public async Task Login([FromBody] User user) + { + 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); + user = users.GetUser(identityuser.UserName); + user.IsAuthenticated = true; + } + else + { + user = null; + } + } + else + { + user = null; + } + } + return user; + } + + // POST api//logout + [HttpPost("logout")] + public async Task Logout([FromBody] User user) + { + await identitySignInManager.SignOutAsync(); + } + + // GET api//current + [HttpGet("authenticate")] + public User Authenticate() + { + return new User { Username = User.Identity.Name, IsAuthenticated = User.Identity.IsAuthenticated }; + } } } diff --git a/Oqtane.Server/Filters/UpgradeFilter.cs b/Oqtane.Server/Filters/UpgradeFilter.cs index 57b6ed28..b08203aa 100644 --- a/Oqtane.Server/Filters/UpgradeFilter.cs +++ b/Oqtane.Server/Filters/UpgradeFilter.cs @@ -7,6 +7,7 @@ using DbUp; using System.Data.SqlClient; using System.Threading; using System.IO; +using Microsoft.AspNetCore.Identity; namespace Oqtane.Filters { @@ -109,6 +110,7 @@ namespace Oqtane.Filters throw new Exception(); } } + return next; } } diff --git a/Oqtane.Server/Oqtane.Server.csproj b/Oqtane.Server/Oqtane.Server.csproj index c27a89be..efb5ecaf 100644 --- a/Oqtane.Server/Oqtane.Server.csproj +++ b/Oqtane.Server/Oqtane.Server.csproj @@ -26,11 +26,13 @@ + + @@ -38,8 +40,9 @@ - - + + + diff --git a/Oqtane.Server/Repository/IUserRepository.cs b/Oqtane.Server/Repository/IUserRepository.cs index 4e0c29cc..05ede0ad 100644 --- a/Oqtane.Server/Repository/IUserRepository.cs +++ b/Oqtane.Server/Repository/IUserRepository.cs @@ -9,6 +9,7 @@ namespace Oqtane.Repository void AddUser(User User); void UpdateUser(User User); User GetUser(int UserId); + User GetUser(string Username); void DeleteUser(int UserId); } } diff --git a/Oqtane.Server/Repository/TenantContext.cs b/Oqtane.Server/Repository/TenantContext.cs index e5c2baf2..b74ce34d 100644 --- a/Oqtane.Server/Repository/TenantContext.cs +++ b/Oqtane.Server/Repository/TenantContext.cs @@ -1,10 +1,12 @@ -using Microsoft.EntityFrameworkCore; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Identity.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; using Oqtane.Models; using System; namespace Oqtane.Repository { - public class TenantContext : DbContext + public class TenantContext : IdentityDbContext { public virtual DbSet Site { get; set; } public virtual DbSet Page { get; set; } diff --git a/Oqtane.Server/Repository/TenantResolver.cs b/Oqtane.Server/Repository/TenantResolver.cs index 1e806b31..4ba145ff 100644 --- a/Oqtane.Server/Repository/TenantResolver.cs +++ b/Oqtane.Server/Repository/TenantResolver.cs @@ -28,6 +28,10 @@ namespace Oqtane.Repository { aliasname += "/" + segments[1]; } + if (aliasname.EndsWith("/")) + { + aliasname = aliasname.Substring(0, aliasname.Length - 1); + } } public Tenant GetTenant() diff --git a/Oqtane.Server/Repository/UserRepository.cs b/Oqtane.Server/Repository/UserRepository.cs index 1d0bd9ca..a750a405 100644 --- a/Oqtane.Server/Repository/UserRepository.cs +++ b/Oqtane.Server/Repository/UserRepository.cs @@ -65,6 +65,19 @@ namespace Oqtane.Repository } } + public User GetUser(string Username) + { + try + { + User user = db.User.Where(item => item.Username == Username).FirstOrDefault(); + return user; + } + catch + { + throw; + } + } + public void DeleteUser(int userId) { try diff --git a/Oqtane.Server/Scripts/Identity.sql b/Oqtane.Server/Scripts/Identity.sql new file mode 100644 index 00000000..45f9cd66 --- /dev/null +++ b/Oqtane.Server/Scripts/Identity.sql @@ -0,0 +1,190 @@ +CREATE TABLE [dbo].[AspNetRoleClaims]( + [Id] [int] IDENTITY(1,1) NOT NULL, + [RoleId] [nvarchar](450) NOT NULL, + [ClaimType] [nvarchar](max) NULL, + [ClaimValue] [nvarchar](max) NULL, + CONSTRAINT [PK_AspNetRoleClaims] PRIMARY KEY CLUSTERED +( + [Id] ASC +) ON [PRIMARY] +) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY] +GO + +CREATE TABLE [dbo].[AspNetRoles]( + [Id] [nvarchar](450) NOT NULL, + [Name] [nvarchar](256) NULL, + [NormalizedName] [nvarchar](256) NULL, + [ConcurrencyStamp] [nvarchar](max) NULL, + CONSTRAINT [PK_AspNetRoles] PRIMARY KEY CLUSTERED +( + [Id] ASC +) ON [PRIMARY] +) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY] +GO + +CREATE TABLE [dbo].[AspNetUserClaims]( + [Id] [int] IDENTITY(1,1) NOT NULL, + [UserId] [nvarchar](450) NOT NULL, + [ClaimType] [nvarchar](max) NULL, + [ClaimValue] [nvarchar](max) NULL, + CONSTRAINT [PK_AspNetUserClaims] PRIMARY KEY CLUSTERED +( + [Id] ASC +) ON [PRIMARY] +) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY] +GO + +CREATE TABLE [dbo].[AspNetUserLogins]( + [LoginProvider] [nvarchar](128) NOT NULL, + [ProviderKey] [nvarchar](128) NOT NULL, + [ProviderDisplayName] [nvarchar](max) NULL, + [UserId] [nvarchar](450) NOT NULL, + CONSTRAINT [PK_AspNetUserLogins] PRIMARY KEY CLUSTERED +( + [LoginProvider] ASC, + [ProviderKey] ASC +) ON [PRIMARY] +) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY] +GO + +CREATE TABLE [dbo].[AspNetUserRoles]( + [UserId] [nvarchar](450) NOT NULL, + [RoleId] [nvarchar](450) NOT NULL, + CONSTRAINT [PK_AspNetUserRoles] PRIMARY KEY CLUSTERED +( + [UserId] ASC, + [RoleId] ASC +) ON [PRIMARY] +) ON [PRIMARY] +GO + +CREATE TABLE [dbo].[AspNetUsers]( + [Id] [nvarchar](450) NOT NULL, + [UserName] [nvarchar](256) NULL, + [NormalizedUserName] [nvarchar](256) NULL, + [Email] [nvarchar](256) NULL, + [NormalizedEmail] [nvarchar](256) NULL, + [EmailConfirmed] [bit] NOT NULL, + [PasswordHash] [nvarchar](max) NULL, + [SecurityStamp] [nvarchar](max) NULL, + [ConcurrencyStamp] [nvarchar](max) NULL, + [PhoneNumber] [nvarchar](max) NULL, + [PhoneNumberConfirmed] [bit] NOT NULL, + [TwoFactorEnabled] [bit] NOT NULL, + [LockoutEnd] [datetimeoffset](7) NULL, + [LockoutEnabled] [bit] NOT NULL, + [AccessFailedCount] [int] NOT NULL, + CONSTRAINT [PK_AspNetUsers] PRIMARY KEY CLUSTERED +( + [Id] ASC +) ON [PRIMARY] +) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY] +GO + +CREATE TABLE [dbo].[AspNetUserTokens]( + [UserId] [nvarchar](450) NOT NULL, + [LoginProvider] [nvarchar](128) NOT NULL, + [Name] [nvarchar](128) NOT NULL, + [Value] [nvarchar](max) NULL, + CONSTRAINT [PK_AspNetUserTokens] PRIMARY KEY CLUSTERED +( + [UserId] ASC, + [LoginProvider] ASC, + [Name] ASC +) ON [PRIMARY] +) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY] +GO + +CREATE NONCLUSTERED INDEX [IX_AspNetRoleClaims_RoleId] ON [dbo].[AspNetRoleClaims] +( + [RoleId] ASC +) ON [PRIMARY] +GO + +CREATE UNIQUE NONCLUSTERED INDEX [RoleNameIndex] ON [dbo].[AspNetRoles] +( + [NormalizedName] ASC +) +WHERE ([NormalizedName] IS NOT NULL) + ON [PRIMARY] +GO + +CREATE NONCLUSTERED INDEX [IX_AspNetUserClaims_UserId] ON [dbo].[AspNetUserClaims] +( + [UserId] ASC +) ON [PRIMARY] +GO + +CREATE NONCLUSTERED INDEX [IX_AspNetUserLogins_UserId] ON [dbo].[AspNetUserLogins] +( + [UserId] ASC +) ON [PRIMARY] +GO + +CREATE NONCLUSTERED INDEX [IX_AspNetUserRoles_RoleId] ON [dbo].[AspNetUserRoles] +( + [RoleId] ASC +) ON [PRIMARY] +GO + +CREATE NONCLUSTERED INDEX [EmailIndex] ON [dbo].[AspNetUsers] +( + [NormalizedEmail] ASC +) ON [PRIMARY] +GO + +CREATE UNIQUE NONCLUSTERED INDEX [UserNameIndex] ON [dbo].[AspNetUsers] +( + [NormalizedUserName] ASC +) +WHERE ([NormalizedUserName] IS NOT NULL) +ON [PRIMARY] +GO + +ALTER TABLE [dbo].[AspNetRoleClaims] WITH CHECK ADD CONSTRAINT [FK_AspNetRoleClaims_AspNetRoles_RoleId] FOREIGN KEY([RoleId]) +REFERENCES [dbo].[AspNetRoles] ([Id]) +ON DELETE CASCADE +GO + +ALTER TABLE [dbo].[AspNetRoleClaims] CHECK CONSTRAINT [FK_AspNetRoleClaims_AspNetRoles_RoleId] +GO + +ALTER TABLE [dbo].[AspNetUserClaims] WITH CHECK ADD CONSTRAINT [FK_AspNetUserClaims_AspNetUsers_UserId] FOREIGN KEY([UserId]) +REFERENCES [dbo].[AspNetUsers] ([Id]) +ON DELETE CASCADE +GO + +ALTER TABLE [dbo].[AspNetUserClaims] CHECK CONSTRAINT [FK_AspNetUserClaims_AspNetUsers_UserId] +GO + +ALTER TABLE [dbo].[AspNetUserLogins] WITH CHECK ADD CONSTRAINT [FK_AspNetUserLogins_AspNetUsers_UserId] FOREIGN KEY([UserId]) +REFERENCES [dbo].[AspNetUsers] ([Id]) +ON DELETE CASCADE +GO + +ALTER TABLE [dbo].[AspNetUserLogins] CHECK CONSTRAINT [FK_AspNetUserLogins_AspNetUsers_UserId] +GO + +ALTER TABLE [dbo].[AspNetUserRoles] WITH CHECK ADD CONSTRAINT [FK_AspNetUserRoles_AspNetRoles_RoleId] FOREIGN KEY([RoleId]) +REFERENCES [dbo].[AspNetRoles] ([Id]) +ON DELETE CASCADE +GO + +ALTER TABLE [dbo].[AspNetUserRoles] CHECK CONSTRAINT [FK_AspNetUserRoles_AspNetRoles_RoleId] +GO + +ALTER TABLE [dbo].[AspNetUserRoles] WITH CHECK ADD CONSTRAINT [FK_AspNetUserRoles_AspNetUsers_UserId] FOREIGN KEY([UserId]) +REFERENCES [dbo].[AspNetUsers] ([Id]) +ON DELETE CASCADE +GO + +ALTER TABLE [dbo].[AspNetUserRoles] CHECK CONSTRAINT [FK_AspNetUserRoles_AspNetUsers_UserId] +GO + +ALTER TABLE [dbo].[AspNetUserTokens] WITH CHECK ADD CONSTRAINT [FK_AspNetUserTokens_AspNetUsers_UserId] FOREIGN KEY([UserId]) +REFERENCES [dbo].[AspNetUsers] ([Id]) +ON DELETE CASCADE +GO + +ALTER TABLE [dbo].[AspNetUserTokens] CHECK CONSTRAINT [FK_AspNetUserTokens_AspNetUsers_UserId] +GO diff --git a/Oqtane.Server/Scripts/Tenant.sql b/Oqtane.Server/Scripts/Tenant.sql index b43bf28a..5719f48f 100644 --- a/Oqtane.Server/Scripts/Tenant.sql +++ b/Oqtane.Server/Scripts/Tenant.sql @@ -313,21 +313,4 @@ GO SET IDENTITY_INSERT [dbo].[HtmlText] OFF GO -SET IDENTITY_INSERT [dbo].[User] ON -GO -INSERT [dbo].[User] ([UserId], [Username], [DisplayName], [Roles], [IsSuperUser]) -VALUES (1, N'host', N'Host', N'', 1) -GO -INSERT [dbo].[User] ([UserId], [Username], [DisplayName], [Roles], [IsSuperUser]) -VALUES (2, N'admin', N'Administrator', N'Administrators;', 0) -GO -INSERT [dbo].[User] ([UserId], [Username], [DisplayName], [Roles], [IsSuperUser]) -VALUES (3, N'editor', N'Editor', N'Editors;', 0) -GO -INSERT [dbo].[User] ([UserId], [Username], [DisplayName], [Roles], [IsSuperUser]) -VALUES (4, N'member', N'Member', N'Members;', 0) -GO -SET IDENTITY_INSERT [dbo].[User] OFF -GO - diff --git a/Oqtane.Server/Startup.cs b/Oqtane.Server/Startup.cs index 7849d5e7..0e29d5df 100644 --- a/Oqtane.Server/Startup.cs +++ b/Oqtane.Server/Startup.cs @@ -19,6 +19,10 @@ 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 { @@ -58,6 +62,11 @@ namespace Oqtane.Server }); } + // register auth services + services.AddAuthorizationCore(); + services.AddScoped(); + services.AddScoped(s => s.GetRequiredService()); + // register scoped core services services.AddScoped(); services.AddScoped(); @@ -99,6 +108,38 @@ namespace Oqtane.Server )); services.AddDbContext(options => { }); + services.AddIdentity() + .AddEntityFrameworkStores() + .AddDefaultTokenProviders(); + + services.Configure(options => + { + // Password settings + options.Password.RequireDigit = false; + options.Password.RequiredLength = 6; + options.Password.RequireNonAlphanumeric = false; + options.Password.RequireUppercase = false; + options.Password.RequireLowercase = false; + + // Lockout settings + options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(30); + options.Lockout.MaxFailedAccessAttempts = 10; + options.Lockout.AllowedForNewUsers = true; + + // User settings + options.User.RequireUniqueEmail = false; + }); + + services.ConfigureApplicationCookie(options => + { + options.Cookie.HttpOnly = false; + options.Events.OnRedirectToLogin = context => + { + context.Response.StatusCode = 401; + return Task.CompletedTask; + }; + }); + services.AddMemoryCache(); services.AddMvc().AddNewtonsoftJson(); @@ -177,6 +218,8 @@ namespace Oqtane.Server app.UseStaticFiles(); app.UseRouting(); + app.UseAuthentication(); + app.UseAuthorization(); app.UseEndpoints(endpoints => { @@ -201,6 +244,38 @@ namespace Oqtane.Server )); services.AddDbContext(options => { }); + services.AddIdentity() + .AddEntityFrameworkStores() + .AddDefaultTokenProviders(); + + services.Configure(options => + { + // Password settings + options.Password.RequireDigit = false; + options.Password.RequiredLength = 6; + options.Password.RequireNonAlphanumeric = false; + options.Password.RequireUppercase = false; + options.Password.RequireLowercase = false; + + // Lockout settings + options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(30); + options.Lockout.MaxFailedAccessAttempts = 10; + options.Lockout.AllowedForNewUsers = true; + + // User settings + options.User.RequireUniqueEmail = false; + }); + + services.ConfigureApplicationCookie(options => + { + options.Cookie.HttpOnly = false; + options.Events.OnRedirectToLogin = context => + { + context.Response.StatusCode = 401; + return Task.CompletedTask; + }; + }); + services.AddMemoryCache(); services.AddMvc().AddNewtonsoftJson(); @@ -281,6 +356,8 @@ namespace Oqtane.Server app.UseClientSideBlazorFiles(); app.UseRouting(); + app.UseAuthentication(); + app.UseAuthorization(); app.UseEndpoints(endpoints => { diff --git a/Oqtane.Shared/Models/User.cs b/Oqtane.Shared/Models/User.cs index 7b55104f..d2bd5f6d 100644 --- a/Oqtane.Shared/Models/User.cs +++ b/Oqtane.Shared/Models/User.cs @@ -9,5 +9,10 @@ namespace Oqtane.Models public string DisplayName { get; set; } public string Roles { get; set; } public bool IsSuperUser { get; set; } + + [NotMapped] + public string Password { get; set; } + [NotMapped] + public bool IsAuthenticated { get; set; } } }