Add authentication

This commit is contained in:
Robert Marshall 2020-01-03 13:32:20 +00:00
parent 8f0c4c0a45
commit a2d84e182d
11 changed files with 206 additions and 21 deletions

View 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();
}
}
}

View 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);
}
}
}

View file

@ -1,6 +1,7 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Website.Data;
using Website.Models;
@ -38,6 +39,7 @@ namespace Website.Controllers
}
}
[Authorize]
public async Task<IActionResult> Edit(int? id) {
if (!id.HasValue)
return View();
@ -56,6 +58,7 @@ namespace Website.Controllers
}
}
[Authorize]
[HttpPost]
public async Task<IActionResult> Save(BlogPostSubmission submission) {
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});
}
[Authorize]
public async Task<IActionResult> Manage() {
var posts = await _repo.GetAllPostsAsync();
var models = posts.OrderByDescending(post => post.Timestamp).Select(post => new BlogPostViewModel(post));
return View(models);
}
[Authorize]
public async Task<IActionResult> Publish(int id) {
var post = await _repo.GetPostByIdAsync(id);
post.Publish();
@ -82,6 +87,7 @@ namespace Website.Controllers
return RedirectToAction(nameof(Manage));
}
[Authorize]
public async Task<IActionResult> Delete(int id) {
await _repo.DeletePostAsync(id);

View 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; }
}
}

View 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());
}
}
}
}

View 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
View 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; }
}
}

View file

@ -1,4 +1,5 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
@ -27,15 +28,18 @@ namespace Website
options.MinimumSameSitePolicy = SameSiteMode.None;
});
services.AddSingleton<IConfiguration>(Configuration);
services.AddSingleton(Configuration);
RegisterRepositories(services);
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme).AddCookie();
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
}
private void RegisterRepositories(IServiceCollection services) =>
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.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
@ -51,20 +55,19 @@ namespace Website
app.UseHsts();
}
app.UseStatusCodePagesWithReExecute("/Error/PageNotFound");
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseMvc(routes =>
{
app.UseStatusCodePagesWithReExecute("/Error/PageNotFound")
.UseHttpsRedirection()
.UseStaticFiles()
.UseAuthentication()
.UseMvc(routes => {
routes.MapRoute(
name: "blogPages",
template: "blog/{action}/{page:int}",
defaults: new { controller = "Blog", action = "Page", page = 1 });
defaults: new {controller = "Blog", action = "Page", page = 1});
routes.MapRoute(
name: "blogView",
template: "blog/view/{url}",
defaults: new { controller = "Blog", action = "Entry"});
defaults: new {controller = "Blog", action = "Entry"});
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");

View file

@ -0,0 +1,6 @@
namespace Website.ViewModels {
public class LoginViewModel {
public string ReturnUrl { get; set; }
public bool FailedAttempt { get; set; }
}
}

View file

@ -0,0 +1,8 @@
@model object
@{
ViewBag.Title = "title";
Layout = "_Layout";
}
<h2>title</h2>

View 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>