Add authentication
This commit is contained in:
parent
8f0c4c0a45
commit
a2d84e182d
11 changed files with 206 additions and 21 deletions
17
Website.Tests/Models/UserTests.cs
Normal file
17
Website.Tests/Models/UserTests.cs
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
using FluentAssertions;
|
||||||
|
using Website.Models;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Website.Tests.Models {
|
||||||
|
public class UserTests {
|
||||||
|
[Fact]
|
||||||
|
public void ValidatePassword_WithValidSHA256Input_ReturnsTrue() {
|
||||||
|
var user = new User {
|
||||||
|
Password= "5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8"
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
user.ValidatePassword("password").Should().BeTrue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
54
Website/Controllers/AccountController.cs
Normal file
54
Website/Controllers/AccountController.cs
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
using System;
|
||||||
|
using System.Security.Claims;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNetCore.Authentication;
|
||||||
|
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Website.Data;
|
||||||
|
using Website.Models;
|
||||||
|
using Website.ViewModels;
|
||||||
|
|
||||||
|
namespace Website.Controllers {
|
||||||
|
public class AccountController:Controller {
|
||||||
|
private readonly UserRepository _repo;
|
||||||
|
|
||||||
|
public AccountController(UserRepository repo) => _repo = repo;
|
||||||
|
|
||||||
|
public IActionResult Index() => View();
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
public IActionResult Login(string returnUrl, bool failedAttempt = false) {
|
||||||
|
var model = new LoginViewModel {
|
||||||
|
ReturnUrl = returnUrl,
|
||||||
|
FailedAttempt = failedAttempt
|
||||||
|
};
|
||||||
|
return View(model);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
public async Task<IActionResult> Login(LoginRequest request) {
|
||||||
|
try {
|
||||||
|
var user = await _repo.GetUserByEmail(request.Username);
|
||||||
|
return user.ValidatePassword(request.Password)
|
||||||
|
? await SetIdentityAndRedirect(request.ReturnUrl, user)
|
||||||
|
: Login(request.ReturnUrl, true);
|
||||||
|
}
|
||||||
|
catch (Exception e) {
|
||||||
|
return Login(request.ReturnUrl, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<IActionResult> SetIdentityAndRedirect(string returnUrl, User user) {
|
||||||
|
var identity = new ClaimsIdentity(CookieAuthenticationDefaults.AuthenticationScheme);
|
||||||
|
identity.AddClaim(new Claim(ClaimTypes.Name, user.Username));
|
||||||
|
identity.AddClaim(new Claim(ClaimTypes.Email, user.Username));
|
||||||
|
|
||||||
|
var principal = new ClaimsPrincipal(identity);
|
||||||
|
await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, principal);
|
||||||
|
|
||||||
|
return string.IsNullOrEmpty(returnUrl)
|
||||||
|
? (IActionResult) RedirectToAction(nameof(Index))
|
||||||
|
: Redirect(returnUrl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,7 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Website.Data;
|
using Website.Data;
|
||||||
using Website.Models;
|
using Website.Models;
|
||||||
|
@ -38,6 +39,7 @@ namespace Website.Controllers
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Authorize]
|
||||||
public async Task<IActionResult> Edit(int? id) {
|
public async Task<IActionResult> Edit(int? id) {
|
||||||
if (!id.HasValue)
|
if (!id.HasValue)
|
||||||
return View();
|
return View();
|
||||||
|
@ -56,6 +58,7 @@ namespace Website.Controllers
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Authorize]
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
public async Task<IActionResult> Save(BlogPostSubmission submission) {
|
public async Task<IActionResult> Save(BlogPostSubmission submission) {
|
||||||
var post = submission.Id.HasValue ? await _repo.GetPostByIdAsync(submission.Id.Value) : new BlogPost();
|
var post = submission.Id.HasValue ? await _repo.GetPostByIdAsync(submission.Id.Value) : new BlogPost();
|
||||||
|
@ -68,12 +71,14 @@ namespace Website.Controllers
|
||||||
return RedirectToAction(nameof(Edit), new{savedPost.Id});
|
return RedirectToAction(nameof(Edit), new{savedPost.Id});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Authorize]
|
||||||
public async Task<IActionResult> Manage() {
|
public async Task<IActionResult> Manage() {
|
||||||
var posts = await _repo.GetAllPostsAsync();
|
var posts = await _repo.GetAllPostsAsync();
|
||||||
var models = posts.OrderByDescending(post => post.Timestamp).Select(post => new BlogPostViewModel(post));
|
var models = posts.OrderByDescending(post => post.Timestamp).Select(post => new BlogPostViewModel(post));
|
||||||
return View(models);
|
return View(models);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Authorize]
|
||||||
public async Task<IActionResult> Publish(int id) {
|
public async Task<IActionResult> Publish(int id) {
|
||||||
var post = await _repo.GetPostByIdAsync(id);
|
var post = await _repo.GetPostByIdAsync(id);
|
||||||
post.Publish();
|
post.Publish();
|
||||||
|
@ -82,6 +87,7 @@ namespace Website.Controllers
|
||||||
return RedirectToAction(nameof(Manage));
|
return RedirectToAction(nameof(Manage));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Authorize]
|
||||||
public async Task<IActionResult> Delete(int id) {
|
public async Task<IActionResult> Delete(int id) {
|
||||||
await _repo.DeletePostAsync(id);
|
await _repo.DeletePostAsync(id);
|
||||||
|
|
||||||
|
|
10
Website/Data/States/UserState.cs
Normal file
10
Website/Data/States/UserState.cs
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
namespace Website.Data.States {
|
||||||
|
public class UserState {
|
||||||
|
public string User_Id { get; set; }
|
||||||
|
public string User_Email { get; set; }
|
||||||
|
public string User_Password { get; set; }
|
||||||
|
public string User_Created { get; set; }
|
||||||
|
public string User_Deleted { get; set; }
|
||||||
|
public string Group_Id { get; set; }
|
||||||
|
}
|
||||||
|
}
|
25
Website/Data/UserRepository.cs
Normal file
25
Website/Data/UserRepository.cs
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Dapper;
|
||||||
|
using Website.Data.States;
|
||||||
|
using Website.Models;
|
||||||
|
|
||||||
|
namespace Website.Data {
|
||||||
|
public class UserRepository {
|
||||||
|
private readonly IDatabaseProvider _dbProvider;
|
||||||
|
|
||||||
|
public UserRepository(IDatabaseProvider dbProvider) {
|
||||||
|
_dbProvider = dbProvider;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<User> GetUserByEmail(string email) {
|
||||||
|
const string query = "SELECT * FROM users WHERE user_email=@email";
|
||||||
|
|
||||||
|
using (var connection = _dbProvider.NewConnection()) {
|
||||||
|
connection.Open();
|
||||||
|
var result = await connection.QueryAsync<UserState>(query, new {email});
|
||||||
|
return new User(result.Single());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
7
Website/Models/LoginRequest.cs
Normal file
7
Website/Models/LoginRequest.cs
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
namespace Website.Models {
|
||||||
|
public class LoginRequest {
|
||||||
|
public string Username { get; set; }
|
||||||
|
public string Password { get; set; }
|
||||||
|
public string ReturnUrl { get; set; }
|
||||||
|
}
|
||||||
|
}
|
29
Website/Models/User.cs
Normal file
29
Website/Models/User.cs
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Text;
|
||||||
|
using Website.Data.States;
|
||||||
|
|
||||||
|
namespace Website.Models {
|
||||||
|
public class User {
|
||||||
|
public User(UserState state) {
|
||||||
|
Username = state.User_Email;
|
||||||
|
Password = state.User_Password;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool ValidatePassword(string password) {
|
||||||
|
using (var sha256 = SHA256.Create()) {
|
||||||
|
var hash = sha256.ComputeHash(Encoding.UTF8.GetBytes(password));
|
||||||
|
|
||||||
|
var builder = new StringBuilder();
|
||||||
|
foreach (var b in hash)
|
||||||
|
builder.Append(b.ToString("x2"));
|
||||||
|
var hashString = builder.ToString();
|
||||||
|
|
||||||
|
return hashString == Password;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public string Username { get; set; }
|
||||||
|
public string Password { get; set; }
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,5 @@
|
||||||
using Microsoft.AspNetCore.Builder;
|
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||||
|
using Microsoft.AspNetCore.Builder;
|
||||||
using Microsoft.AspNetCore.Hosting;
|
using Microsoft.AspNetCore.Hosting;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
@ -27,15 +28,18 @@ namespace Website
|
||||||
options.MinimumSameSitePolicy = SameSiteMode.None;
|
options.MinimumSameSitePolicy = SameSiteMode.None;
|
||||||
});
|
});
|
||||||
|
|
||||||
services.AddSingleton<IConfiguration>(Configuration);
|
services.AddSingleton(Configuration);
|
||||||
RegisterRepositories(services);
|
RegisterRepositories(services);
|
||||||
|
|
||||||
|
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme).AddCookie();
|
||||||
|
|
||||||
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
|
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void RegisterRepositories(IServiceCollection services) =>
|
private void RegisterRepositories(IServiceCollection services) =>
|
||||||
services.AddSingleton<IDatabaseProvider, MySQLDatabaseProvider>()
|
services.AddSingleton<IDatabaseProvider, MySQLDatabaseProvider>()
|
||||||
.AddSingleton<BlogRepository, BlogRepository>();
|
.AddSingleton<BlogRepository, BlogRepository>()
|
||||||
|
.AddSingleton<UserRepository, UserRepository>();
|
||||||
|
|
||||||
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
|
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
|
||||||
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
|
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
|
||||||
|
@ -51,12 +55,11 @@ namespace Website
|
||||||
app.UseHsts();
|
app.UseHsts();
|
||||||
}
|
}
|
||||||
|
|
||||||
app.UseStatusCodePagesWithReExecute("/Error/PageNotFound");
|
app.UseStatusCodePagesWithReExecute("/Error/PageNotFound")
|
||||||
app.UseHttpsRedirection();
|
.UseHttpsRedirection()
|
||||||
app.UseStaticFiles();
|
.UseStaticFiles()
|
||||||
|
.UseAuthentication()
|
||||||
app.UseMvc(routes =>
|
.UseMvc(routes => {
|
||||||
{
|
|
||||||
routes.MapRoute(
|
routes.MapRoute(
|
||||||
name: "blogPages",
|
name: "blogPages",
|
||||||
template: "blog/{action}/{page:int}",
|
template: "blog/{action}/{page:int}",
|
||||||
|
|
6
Website/ViewModels/LoginViewModel.cs
Normal file
6
Website/ViewModels/LoginViewModel.cs
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
namespace Website.ViewModels {
|
||||||
|
public class LoginViewModel {
|
||||||
|
public string ReturnUrl { get; set; }
|
||||||
|
public bool FailedAttempt { get; set; }
|
||||||
|
}
|
||||||
|
}
|
8
Website/Views/Account/Index.cshtml
Normal file
8
Website/Views/Account/Index.cshtml
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
@model object
|
||||||
|
|
||||||
|
@{
|
||||||
|
ViewBag.Title = "title";
|
||||||
|
Layout = "_Layout";
|
||||||
|
}
|
||||||
|
|
||||||
|
<h2>title</h2>
|
20
Website/Views/Account/Login.cshtml
Normal file
20
Website/Views/Account/Login.cshtml
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
@model LoginViewModel
|
||||||
|
|
||||||
|
@{
|
||||||
|
ViewBag.Title = "Login";
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (!string.IsNullOrEmpty(Model.ReturnUrl)) {
|
||||||
|
<div>Please log in to perform that action</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (Model.FailedAttempt) {
|
||||||
|
<div>Could not log in with those credentials</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<form method="post" action="login">
|
||||||
|
<input type="hidden" name="returnUrl" value="@Model.ReturnUrl"/>
|
||||||
|
<label for="username">Username: </label><input name="username" id="username"/>
|
||||||
|
<label for="password">Password: </label><input name="password" type="password" id="password"/>
|
||||||
|
<button type="submit">Login</button>
|
||||||
|
</form>
|
Loading…
Add table
Add a link
Reference in a new issue