diff options
| author | ivarlovlie <git@ivarlovlie.no> | 2020-08-09 15:51:33 +0200 |
|---|---|---|
| committer | ivarlovlie <git@ivarlovlie.no> | 2020-08-09 15:51:33 +0200 |
| commit | 8614d18522441543e08c37c68121fed1fa8d6ae7 (patch) | |
| tree | dd53ae13bdf269098e385107d27dcc2a0d8d73db /src/server | |
| parent | 9b2c6f550a3a705e02dc4f86797c9223ad59d5fa (diff) | |
| download | dough-8614d18522441543e08c37c68121fed1fa8d6ae7.tar.xz dough-8614d18522441543e08c37c68121fed1fa8d6ae7.zip | |
auth user
Diffstat (limited to 'src/server')
| -rw-r--r-- | src/server/AppData/dpkeys/key-75aa5a6c-406f-4e39-b1df-58a8d3e6af7a.xml | 16 | ||||
| -rw-r--r-- | src/server/Controllers/AccountController.cs | 71 | ||||
| -rw-r--r-- | src/server/Controllers/BaseController.cs | 2 | ||||
| -rw-r--r-- | src/server/Dough.csproj | 11 | ||||
| -rw-r--r-- | src/server/IdentityServer/Config.cs | 8 | ||||
| -rw-r--r-- | src/server/Models/Constants.cs | 8 | ||||
| -rw-r--r-- | src/server/Models/Payloads/LoginPayload.cs | 2 | ||||
| -rw-r--r-- | src/server/Models/Results/ErrorResult.cs | 8 | ||||
| -rw-r--r-- | src/server/Pages/Error.cshtml | 19 | ||||
| -rw-r--r-- | src/server/Pages/Error.cshtml.cs | 12 | ||||
| -rw-r--r-- | src/server/Pages/Login.cshtml | 95 | ||||
| -rw-r--r-- | src/server/Pages/Login.cshtml.cs | 12 | ||||
| -rw-r--r-- | src/server/Services/EmailService.cs | 35 | ||||
| -rw-r--r-- | src/server/Startup.cs | 45 | ||||
| -rw-r--r-- | src/server/tempkey.jwk | 1 |
15 files changed, 305 insertions, 40 deletions
diff --git a/src/server/AppData/dpkeys/key-75aa5a6c-406f-4e39-b1df-58a8d3e6af7a.xml b/src/server/AppData/dpkeys/key-75aa5a6c-406f-4e39-b1df-58a8d3e6af7a.xml new file mode 100644 index 0000000..51115c2 --- /dev/null +++ b/src/server/AppData/dpkeys/key-75aa5a6c-406f-4e39-b1df-58a8d3e6af7a.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="utf-8"?> +<key id="75aa5a6c-406f-4e39-b1df-58a8d3e6af7a" version="1"> + <creationDate>2020-08-09T12:29:14.3954895Z</creationDate> + <activationDate>2020-08-09T12:29:14.3177582Z</activationDate> + <expirationDate>2020-11-07T12:29:14.3177582Z</expirationDate> + <descriptor deserializerType="Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel.AuthenticatedEncryptorDescriptorDeserializer, Microsoft.AspNetCore.DataProtection, Version=3.1.6.0, Culture=neutral, PublicKeyToken=adb9793829ddae60"> + <descriptor> + <encryption algorithm="AES_256_CBC" /> + <validation algorithm="HMACSHA256" /> + <masterKey p4:requiresEncryption="true" xmlns:p4="http://schemas.asp.net/2015/03/dataProtection"> + <!-- Warning: the key below is in an unencrypted form. --> + <value>iO4MQasV7DgDGtCJg0PE5yYk/skUTg3Hvzk1gxTzq2sVxDSysKKKJGYuT0k5yctxWlGnOACrGeKyBhTelz3hbA==</value> + </masterKey> + </descriptor> + </descriptor> +</key>
\ No newline at end of file diff --git a/src/server/Controllers/AccountController.cs b/src/server/Controllers/AccountController.cs index fe7b7a2..5c760e2 100644 --- a/src/server/Controllers/AccountController.cs +++ b/src/server/Controllers/AccountController.cs @@ -1,11 +1,20 @@ using System; +using System.Collections.Generic; +using System.IO; +using System.Security.Claims; using System.Threading.Tasks; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Dough.Models; using Dough.Models.Database; +using Dough.Models.Payloads; +using Dough.Models.Results; +using Dough.Services; using Dough.Utilities; +using IdentityServer4; using IdentityServer4.Services; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Http; namespace Dough.Controllers { @@ -13,34 +22,68 @@ namespace Dough.Controllers public class AccountController : BaseController { private readonly MainDbContext _context; - private readonly IIdentityServerInteractionService _identityServerInteractionService; + private readonly IIdentityServerInteractionService _interaction; + private readonly EmailService _emailService; public AccountController(MainDbContext context, - IIdentityServerInteractionService identityServerInteractionService) + IIdentityServerInteractionService interaction, + EmailService emailService) { _context = context; - _identityServerInteractionService = identityServerInteractionService; + _interaction = interaction; + _emailService = emailService; } + [HttpGet("login")] + public ActionResult GetLogin() + { + var pathToLoginFile = Path.Combine(Directory.GetCurrentDirectory(), "AppData", "login.html"); + var fileContent = System.IO.File.ReadAllText(pathToLoginFile); + return Content(fileContent, "text/html"); + } - // This is the default route for identityserver4 logins (https://identityserver4.readthedocs.io/en/latest/topics/signin.html#login-workflow) [HttpPost("login")] - public async Task<ActionResult> Login(string returnUrl) + [ValidateAntiForgeryToken] + public async Task<ActionResult> PostLogin(LoginPayload payload) { - if (returnUrl.IsMissing() || !_identityServerInteractionService.IsValidReturnUrl(returnUrl)) - return BadRequest("route parameter returnUrl is invalid"); + if (!_interaction.IsValidReturnUrl(payload.ReturnUrl)) + return BadRequest(new ErrorResult()); + var user = _context.Users.SingleByNameOrDefault(payload.Username); + if (user == default) + { + await Task.Delay(1500); + return BadRequest(new ErrorResult("Username or password is incorrect","Please try again with a different username and/or password")); + } - Console.WriteLine("returnUrl: " + returnUrl); - var reqBody = await HttpContext.Request.ReadFormAsync(); - foreach (var formEl in reqBody) + if (!user.VerifyPassword(payload.Password)) { - Console.WriteLine(formEl.Key); - foreach (var value in formEl.Value) - Console.WriteLine(" - " + value); + await Task.Delay(1000); + return BadRequest(new ErrorResult("Username or password is incorrect","Please try again with a different username and/or password")); } - return Ok(); + + var props = new AuthenticationProperties + { + AllowRefresh = true, + IssuedUtc = DateTime.UtcNow, + }; + + if (payload.Persist) + { + props.IsPersistent = true; + props.ExpiresUtc = DateTime.UtcNow.AddDays(15); + } + + var identityServerUser = new IdentityServerUser(user.Id.ToString()) + { + DisplayName = user.Username, + AuthenticationTime = DateTime.UtcNow, + }; + + await HttpContext.SignInAsync(identityServerUser, props); + + return Ok(payload.ReturnUrl); } diff --git a/src/server/Controllers/BaseController.cs b/src/server/Controllers/BaseController.cs index 046c060..44b11b2 100644 --- a/src/server/Controllers/BaseController.cs +++ b/src/server/Controllers/BaseController.cs @@ -6,7 +6,7 @@ using Dough.Utilities; namespace Dough.Controllers
{
[ApiController]
- [Route("api/[controller]")]
+ [Route("[controller]")]
public class BaseController : ControllerBase
{
public LoggedInUserModel LoggedInUser => new LoggedInUserModel
diff --git a/src/server/Dough.csproj b/src/server/Dough.csproj index e54ff49..407dc84 100644 --- a/src/server/Dough.csproj +++ b/src/server/Dough.csproj @@ -8,6 +8,7 @@ <PackageReference Include="BCrypt.Net-Core" Version="1.6.0" /> <PackageReference Include="IdentityServer4" Version="4.0.4" /> <PackageReference Include="IdentityServer4.EntityFramework" Version="4.0.4" /> + <PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="3.1.6" /> <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="3.1.6" /> <PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="3.1.2" /> <PackageReference Include="Serilog.AspNetCore" Version="3.2.0" /> @@ -15,6 +16,16 @@ <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="3.1.6" /> </ItemGroup> <ItemGroup> + <Content Update="Pages\Login.cshtml"> + <ExcludeFromSingleFile>true</ExcludeFromSingleFile> + <CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory> + </Content> + <Content Update="Pages\Error.cshtml"> + <ExcludeFromSingleFile>true</ExcludeFromSingleFile> + <CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory> + </Content> + </ItemGroup> + <ItemGroup> <Folder Include="AppData" /> </ItemGroup> </Project>
\ No newline at end of file diff --git a/src/server/IdentityServer/Config.cs b/src/server/IdentityServer/Config.cs index 41363f1..5b2bf13 100644 --- a/src/server/IdentityServer/Config.cs +++ b/src/server/IdentityServer/Config.cs @@ -22,11 +22,17 @@ namespace Dough.IdentityServer RedirectUris = Constants.BrowserAppLoginRedirectUrls, PostLogoutRedirectUris = Constants.BrowserAppLogoutRedirectUrls, AllowedCorsOrigins = Constants.BrowserAppUrls, + AccessTokenType = AccessTokenType.Reference, + RequireConsent = false, + RefreshTokenExpiration = TokenExpiration.Sliding, + AlwaysSendClientClaims = true, + AllowOfflineAccess = true, AllowedScopes = { IdentityServerConstants.StandardScopes.OpenId, IdentityServerConstants.StandardScopes.Profile, + IdentityServerConstants.StandardScopes.OfflineAccess, MainApiScopeName } } @@ -45,4 +51,4 @@ namespace Dough.IdentityServer new IdentityResources.Profile(), }; } -} +}
\ No newline at end of file diff --git a/src/server/Models/Constants.cs b/src/server/Models/Constants.cs index 3afaaad..04e8a2b 100644 --- a/src/server/Models/Constants.cs +++ b/src/server/Models/Constants.cs @@ -11,13 +11,13 @@ namespace Dough.Models }; public static readonly string[] BrowserAppLoginRedirectUrls = { - "http://localhost:8080/signin-oidc", - "http://localhost:3000/signin-oidc", + "http://localhost:8080/oidc-callback", + "http://localhost:3000/oidc-callback", }; public static readonly string[] BrowserAppLogoutRedirectUrls = { - "http://localhost:8080/signout-callback-oidc", - "http://localhost:3000/signout-callback-oidc", + "http://localhost:8080", + "http://localhost:3000", }; } diff --git a/src/server/Models/Payloads/LoginPayload.cs b/src/server/Models/Payloads/LoginPayload.cs index d7bc50b..1a112a6 100644 --- a/src/server/Models/Payloads/LoginPayload.cs +++ b/src/server/Models/Payloads/LoginPayload.cs @@ -4,5 +4,7 @@ namespace Dough.Models.Payloads { public string Username { get; set; } public string Password { get; set; } + public string ReturnUrl { get; set; } + public bool Persist { get; set; } } }
\ No newline at end of file diff --git a/src/server/Models/Results/ErrorResult.cs b/src/server/Models/Results/ErrorResult.cs index 8d4504f..edd447c 100644 --- a/src/server/Models/Results/ErrorResult.cs +++ b/src/server/Models/Results/ErrorResult.cs @@ -2,13 +2,13 @@ namespace Dough.Models.Results { public class ErrorResult { - public ErrorResult(string title = default, string message = default) + public ErrorResult(string title = "Something went wrong", string message = "Please try again soon") { Title = title; Message = message; } - public string Title { get; set; } = "En feil oppstod"; - public string Message { get; set; } = "Vennligst prøv igjen snart"; + public string Title { get; set; } + public string Message { get; set; } } -} +}
\ No newline at end of file diff --git a/src/server/Pages/Error.cshtml b/src/server/Pages/Error.cshtml new file mode 100644 index 0000000..6030139 --- /dev/null +++ b/src/server/Pages/Error.cshtml @@ -0,0 +1,19 @@ +@page +@model Dough.Pages.Error + +@{ + Layout = null; +} + +<!DOCTYPE html> + +<html> +<head> + <title></title> +</head> +<body> +<div> + +</div> +</body> +</html>
\ No newline at end of file diff --git a/src/server/Pages/Error.cshtml.cs b/src/server/Pages/Error.cshtml.cs new file mode 100644 index 0000000..5ec2c40 --- /dev/null +++ b/src/server/Pages/Error.cshtml.cs @@ -0,0 +1,12 @@ +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace Dough.Pages +{ + public class Error : PageModel + { + public void OnGet() + { + + } + } +}
\ No newline at end of file diff --git a/src/server/Pages/Login.cshtml b/src/server/Pages/Login.cshtml new file mode 100644 index 0000000..d3829a2 --- /dev/null +++ b/src/server/Pages/Login.cshtml @@ -0,0 +1,95 @@ +@page +@model Dough.Pages.Login + +@{ + Layout = null; +} + +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <title>Login - dough</title> + <style> + form { + display: flex; + flex-direction: column; + max-width: 350px; + margin: 0 auto; + } + + form label { + padding-top: 15px; + } + + button { + margin-top:15px; + } + </style> +</head> +<body> +<div id="error" style="display: none"> + <h2 id="title"></h2> + <p id="message"></p> +</div> +<form onsubmit="return false"> + <label for="username">Username: </label> + <input type="text" id="username" name="username" required autofocus> + <label for="password">Password: </label> + <input type="password" id="password" name="password" required> + <label for="persist">Remeber me: <input type="checkbox" id="persist" name="persist"/></label> + @Html.AntiForgeryToken() + <button>Login</button> +</form> + +<script> + const form = document.querySelector("form"); + const errorEL = document.querySelector("#error"); + const titleEl = document.querySelector("#title"); + const messageEl =document.querySelector("#message"); + form.addEventListener("submit", () => { + const username = document.querySelector("#username").value; + const password = document.querySelector("#password").value; + const returnUrl = new URL(location.href).searchParams.get("ReturnUrl"); + const persist = document.querySelector("#persist").checked; + let data = { + username, + password, + returnUrl, + persist + }; + errorEL.style.diplay = "none"; + + fetch("/account/login", { + method: "POST", + body: JSON.stringify(data), + credentials: "include", + headers: { + "Content-Type": "application/json;charset=utf-8", + "RequestVerificationToken": document.querySelector("input[name='__RequestVerificationToken']").value + } + }).then(response => { + if(response.status === 400) { + response.json().then(res => { + if(res.title && res.message) { + displayError(res.title, res.message); + } + }) + } else { + location.href = returnUrl; + } + }).catch(error => console.error(error)); + }) + + function displayError(title, message) { + const errorEL = document.querySelector("#error"); + const titleEl = document.querySelector("#title"); + const messageEl =document.querySelector("#message"); + titleEl.innerText = title; + messageEl.innerText = message; + errorEL.style.display = "inline-block"; + } +</script> + +</body> +</html>
\ No newline at end of file diff --git a/src/server/Pages/Login.cshtml.cs b/src/server/Pages/Login.cshtml.cs new file mode 100644 index 0000000..02d5ee1 --- /dev/null +++ b/src/server/Pages/Login.cshtml.cs @@ -0,0 +1,12 @@ +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace Dough.Pages +{ + public class Login : PageModel + { + public void OnGet() + { + + } + } +}
\ No newline at end of file diff --git a/src/server/Services/EmailService.cs b/src/server/Services/EmailService.cs index 0d70f0f..9d795d6 100644 --- a/src/server/Services/EmailService.cs +++ b/src/server/Services/EmailService.cs @@ -1,7 +1,38 @@ +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Threading.Tasks; +using Microsoft.Extensions.Configuration; + namespace Dough.Services { public class EmailService { - + private readonly IConfiguration _configuration; + + public EmailService(IConfiguration configuration) + { + _configuration = configuration; + } + + public async Task<bool> Send(string subject, string email) + { + var password = _configuration.GetValue<string>(""); + var emailUser = _configuration.GetValue<string>(""); + var emailHost = _configuration.GetValue<string>(""); + + var httpClient = new HttpClient(); + + var payload = new FormUrlEncodedContent(new[] + { + new KeyValuePair<string, string>("username", emailUser), + new KeyValuePair<string, string>("password", password), + }); + + var requestUri = new Uri(emailHost); + var request = await httpClient.PostAsync(requestUri, payload); + + return request.IsSuccessStatusCode; + } } -}
\ No newline at end of file +} diff --git a/src/server/Startup.cs b/src/server/Startup.cs index f55a761..891965b 100644 --- a/src/server/Startup.cs +++ b/src/server/Startup.cs @@ -1,3 +1,4 @@ +using System.IO;
using Dough.IdentityServer;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
@@ -7,6 +8,9 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting;
using Dough.Models;
using Dough.Models.Database;
+using Dough.Services;
+using IdentityServer4.Configuration;
+using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
@@ -20,7 +24,7 @@ namespace Dough }
public IConfiguration Configuration { get; }
-
+
private const string DefaultCorsPolicy = "DefaultCorsPolicy";
private string GetConnectionStringFromEnvironment()
@@ -35,7 +39,6 @@ namespace Dough public void ConfigureServices(IServiceCollection services)
{
-
services.AddCors(options =>
{
options.AddPolicy(DefaultCorsPolicy, builder =>
@@ -48,25 +51,34 @@ namespace Dough });
});
+ var dataprotectionkeyPath = Path.Combine(Directory.GetCurrentDirectory(), "AppData", "dpkeys");
+ services.AddDataProtection().PersistKeysToFileSystem(new DirectoryInfo(dataprotectionkeyPath));
+
services.AddHealthChecks()
.AddDbContextCheck<MainDbContext>();
- services.AddDbContext<MainDbContext>(options => {
- options.UseMySql(GetConnectionStringFromEnvironment());
- });
-
- services.Configure<ApiBehaviorOptions>(options =>
+ services.AddDbContext<MainDbContext>(options =>
{
- options.SuppressModelStateInvalidFilter = true;
- options.SuppressInferBindingSourcesForParameters = true;
+ options.UseMySql(GetConnectionStringFromEnvironment());
});
- var builder = services.AddIdentityServer()
+
+ services.AddIdentityServer(options =>
+ {
+ options.UserInteraction = new UserInteractionOptions
+ {
+ LoginUrl = "/login",
+ ErrorUrl = "/error",
+ };
+ })
.AddInMemoryIdentityResources(Config.IdentityResources)
.AddInMemoryApiScopes(Config.ApiScopes)
+ .AddDeveloperSigningCredential()
.AddInMemoryClients(Config.Clients);
-
+ services.AddSingleton<EmailService>();
+
services.AddControllers();
+ services.AddRazorPages().AddRazorRuntimeCompilation();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
@@ -78,9 +90,14 @@ namespace Dough app.UseCors(DefaultCorsPolicy);
app.UseHealthChecks("/health");
app.UseStatusCodePages();
- app.UseAuthentication();
+ app.UseIdentityServer();
app.UseAuthorization();
- app.UseEndpoints(endpoints => { endpoints.MapControllers().RequireAuthorization(); });
+ app.UseEndpoints(endpoints =>
+ {
+ endpoints.MapRazorPages();
+ endpoints.MapControllers()
+ .RequireAuthorization();
+ });
}
}
-}
+}
\ No newline at end of file diff --git a/src/server/tempkey.jwk b/src/server/tempkey.jwk new file mode 100644 index 0000000..43ef569 --- /dev/null +++ b/src/server/tempkey.jwk @@ -0,0 +1 @@ +{"alg":"RS256","d":"wWiBhJo4kDImxwFXqpjJNTRgkb8-f0daqMsVghuMyh9U93JI0bcEHvym4tG9yEAEd4dQXXvXG9SMkx-lQ41GKtkd6U6ZDjFa0QQuH-rGt4Q0MCwYAhFIpzeMZwoVtz0811cWg2QattvZW7zaz5SkjX-CaQ5VJFSLHtMlsPzkE5xxqRPddGK83qXkyEAa0EzmOnCqlwVRtXrP2AgvqhkUjJM-vg0wjp_Bd204C_ECvi09EAB04jor2w3a4zXMIqsJFi2LQwy-1tG6HY8msgKeL-zfkE-ilCCLtWbDnsMEmFdFWksnBnqY-T855O7EjoaI7NXnUW6MvX_EEg_Mn21ioQ","dp":"etVHep7VLlt0AMz0dihBayeYUEa3FzpiIB8AqkB94lMWeM5u1xlrRrMt5UzKmXnT80Bn3HGMPxZ22gIbpmBZceQ2rG2fhQwR86en0a5LW2eL9IltmfOILsMYSDGTcUSPYfXjJjxzjhtbVufNtm4UsjgfEUrozAp4vntkWpxBYzE","dq":"gbQ-JWMX1OMpJY8AkzVvpMAysyqO3pb33UqTv6D9Q7vhPzagwpAZDhXu9B2PW3cRUWkjyiSKy7VzlUvNo-t88-lFbKk40k0xs6OSQvS5cWr_gChdytvhZw8HFzwej8oJ2ZKQGI2_nExgxmtlD7JSSI56VnG2JK37GHQXunsCxWE","e":"AQAB","kid":"E8C61E059FD40310181EDCDF860FEFFC","kty":"RSA","n":"zds1h4_ELIOkVouKRe7FJR4IlxHajw_3BSvFv6lRWsLJvP42nT1dkAffvB4JeJHNObtEITY1kKy5mAcWDo_o82ZFkzTAzKnme3F5U9RKJdimuKYBX2fX2zbjFQobKbaG-YWsrOaFzclvw0qn-APgHJ8E-7AAE90bbeDNs7GgGDkYb8pWsdz6xVcGJ4z1gYo-68bCyg4ki60s69b69d-IYlvElvKXX0wRKFokdsfzntHVlb-oXWH58peffOZEDLHlwVgojvUcItXXKTb0_tXTh4XD-Eh3xL7S_s1vPjxK0c8wk_55nQ5ib5EP16rZtCV4ZWpLPk2DSL4prnHDdE2J_w","p":"68iIC3HGTbcL1XpdB5vDJBp6sqhIU4KFooWulIA_mt96rtzFc4wv7cDH0BeiHGBBmff0lEr8c178RP1brOQe6LF_yZDoZqkt9hWmivDJYbElLRqasjF5M5EFYNgnAuydY5G58E-c-_zkQk6GUpj0YVTX1fcq_Hhs4Sdi4ol8Lek","q":"34HGqU-SF-AboSwXD_rZgy0ReEf03GQYee5zvLhcAa1SJCHRNoBY8mxTNgqT7mFyHC4FuunY6DNChcH8Ooq4zpuwBQ1_zu4Kj2HwGsGeQDfm36K4FDRslIT-24MAMBdvfhdT-OCbXohTs2Z_sp1A6GtJl2XcLehOBGkB7dE9f6c","qi":"GxlYgDzfeRT_b99IXLOv4BbVmRe1CmuuJkoY5iP0EENscaaE5Qz5iAh_DDs72TPE7Hwdp957QRkWWr8yDC_T0-ZVpVIBGNdUtl3rB18USmSFYn4aY57cFzY5Qs9O7jKrGp8jbZNiD5BVSExhEKxNUdBbsadEWWJrLKl-YB4yShw"}
\ No newline at end of file |
