diff options
24 files changed, 537 insertions, 98 deletions
diff --git a/src/browser/package-lock.json b/src/browser/package-lock.json index 6e840a1..2e5511f 100644 --- a/src/browser/package-lock.json +++ b/src/browser/package-lock.json @@ -463,6 +463,11 @@ "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", "dev": true }, + "base64-js": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz", + "integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==" + }, "big.js": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", @@ -721,6 +726,11 @@ } } }, + "core-js": { + "version": "2.6.11", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.11.tgz", + "integrity": "sha512-5wjnpaT/3dV+XB4borEsnAYQchn00XSgTAWKDkEqv+K8KevjbzmofK6hfJ9TZIlpj2N0xQpazy7PiRQiWHqzWg==" + }, "cosmiconfig": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-5.2.1.tgz", @@ -744,6 +754,11 @@ "which": "^2.0.1" } }, + "crypto-js": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-3.3.0.tgz", + "integrity": "sha512-DIT51nX0dCfKltpRiXV+/TVZq+Qq2NgF4644+K7Ttnla7zEzqc+kjJyiB96BHNyUTBxyjzRcZYpUdZa+QAqi6Q==" + }, "css-select": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/css-select/-/css-select-2.1.0.tgz", @@ -1963,6 +1978,17 @@ "has": "^1.0.3" } }, + "oidc-client": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/oidc-client/-/oidc-client-1.10.1.tgz", + "integrity": "sha512-/QB5Nl7c9GmT9ir1E+OVY3+yZZnuk7Qa9ZEAJqSvDq0bAyAU9KAgeKipTEfKjGdGLTeOLy9FRWuNpULMkfZydQ==", + "requires": { + "base64-js": "^1.3.0", + "core-js": "^2.6.4", + "crypto-js": "^3.1.9-1", + "uuid": "^3.3.2" + } + }, "on-finished": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", @@ -2743,6 +2769,11 @@ "object.getownpropertydescriptors": "^2.1.0" } }, + "uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==" + }, "vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", diff --git a/src/browser/package.json b/src/browser/package.json index a6d2e0f..baec9c1 100644 --- a/src/browser/package.json +++ b/src/browser/package.json @@ -7,8 +7,9 @@ "build": "vite build" }, "dependencies": { - "vue": "^3.0.0-rc.1", + "oidc-client": "^1.10.1", "vue-router": "^4.0.0-0", + "vue": "^3.0.0-rc.1", "vuex": "^4.0.0-0" }, "devDependencies": { diff --git a/src/browser/src/api/account.js b/src/browser/src/api/account.js index 9ba609b..665eb3f 100644 --- a/src/browser/src/api/account.js +++ b/src/browser/src/api/account.js @@ -1,37 +1,171 @@ import constants from "../constants"; +import Oidc from "oidc-client"; +import store from "../store"; + +const userManager = new Oidc.UserManager({ + authority: constants.api_address, + client_id: "browser", + redirect_uri: `${constants.client_address}/oidc-callback`, + post_logout_redirect_uri: constants.client_address, + response_type: "code", + scope: "openid profile main_api offline_access", + accessTokenExpiringNotificationTime: 10, + automaticSilentRenew: true, + filterProtocolClaims: true, + loadUserInfo: true, + revokeAccessTokenOnSignout: true, +}); + +Oidc.Log.logger = console; +Oidc.Log.level = Oidc.Log.INFO; +let currentUser; +let signedIn = false; + +userManager.events.addUserLoaded(function (user) { + store.commit("setProfileData", user); + console.log("New user:", arguments); + console.log("Access_token: ", user.access_token); +}); + +userManager.events.addAccessTokenExpiring(function () { + console.log("AccessToken Expiring", arguments); +}); + +userManager.events.addAccessTokenExpired(function () { + console.log("AccessToken Expired", arguments); + userManager + .signoutRedirect() + .then(function (resp) { + console.log("signed out", resp); + }) + .catch(function (err) { + console.log(err); + }); +}); + +userManager.events.addSilentRenewError(function () { + console.error("Silent Renew Error:", arguments); +}); + +userManager.events.addUserSignedOut(function () { + alert("Logout"); + console.log("UserSignedOut:", arguments); + //userManager.removeUser(); + userManager + .signoutRedirect() + .then(function (resp) { + console.log("signed out", resp); + }) + .catch(function (err) { + console.log(err); + }); +}); export default { - async loginAsync(username, password) { - let response = await fetch(constants.API_ADDRESS + "/account/login", { - method: "POST", - credentials: "include", - headers: { - "Content-Type": "application/json;charset=utf-8", - }, - body: JSON.stringify({ - username, - password, - }), + signIn() { + userManager.signinRedirect().catch(function (err) { + console.log(err); }); + }, - return response; + signinRedirectCallback() { + userManager.signinRedirectCallback().then( + () => { + console.log("Logged in"); + }, + (error) => { + console.error(error); + } + ); }, - async getProfileAsync() { - let response = await fetch(constants.API_ADDRESS + "/account/me", { - method: "GET", - credentials: "include", + + signOut() { + var self = this; + userManager + .signoutRedirect() + .then(function (resp) { + self.signedIn = false; + console.log("signed out", resp); + }) + .catch(function (err) { + console.log(err); + }); + }, + + showTokens() { + userManager.getUser().then(function (user) { + if (user) { + console.log("Profile", user.profile); + console.log("Role", user.profile.role); + } else { + self.signIn(); + } }); + }, - return response; + getUser() { + let self = this; + return new Promise((resolve, reject) => { + userManager + .getUser() + .then(function (user) { + if (user == null) { + self.signIn(); + return resolve(null); + } else { + return resolve(user); + } + }) + .catch(function (err) { + console.log(err); + return reject(err); + }); + }); }, - async logoutAsync() { - let response = await fetch(constants.API_ADDRESS + "/account/logout", { - method: "GET", - credentials: "include", + + getSignedIn() { + let self = this; + return new Promise((resolve, reject) => { + userManager + .getUser() + .then(function (user) { + if (user == null) { + self.signIn(); + return resolve(false); + } else { + currentUser = user; + signedIn = true; + return resolve(signedIn); + } + }) + .catch(function (err) { + console.log(err); + return reject(err); + }); }); + }, - return response; + getRole() { + let self = this; + return new Promise((resolve, reject) => { + userManager + .getUser() + .then(function (user) { + if (user == null) { + self.signIn(); + return resolve(false); + } else { + currentUser = user; + return resolve(user.profile.role); + } + }) + .catch(function (err) { + console.log(err); + return reject(err); + }); + }); }, + async sendResetPasswordMailAsync(username) { let response = await fetch(constants.API_ADDRESS + "/account/forgot", { method: "GET", diff --git a/src/browser/src/constants.js b/src/browser/src/constants.js index ddeebe8..471cc92 100644 --- a/src/browser/src/constants.js +++ b/src/browser/src/constants.js @@ -1,13 +1,12 @@ -const constants = { - API_ADDRESS: "http://localhost:5001/api", +export default { + api_address: "http://localhost:5001", + client_address: "http://localhost:3000", storageKeys: { - COOKIE_LAST_SEEN: "cookie-last-seen", + cookie_last_seen: "cookie-last-seen", }, types: { - SET_PROFILE_DATA: "set-profile-data", - LOGIN_ASYNC: "login-async", - DONWLOAD_PROFILE_DATA: "download-profile-data-async", + set_profile_data: "set-profile-data", + login_async: "login-async", + download_profile_data: "download-profile-data-async", }, }; - -export default constants; diff --git a/src/browser/src/main.js b/src/browser/src/main.js index db7debc..89ba0ea 100644 --- a/src/browser/src/main.js +++ b/src/browser/src/main.js @@ -3,7 +3,4 @@ import App from "./App.vue"; import router from "./router"; import store from "./store"; -createApp(App) - .use(store) - .use(router) - .mount("#app"); +createApp(App).use(store).use(router).mount("#app"); diff --git a/src/browser/src/router.js b/src/browser/src/router.js index 4b25557..0fd5462 100644 --- a/src/browser/src/router.js +++ b/src/browser/src/router.js @@ -1,4 +1,5 @@ import { createRouter, createWebHistory } from "vue-router"; + import store from "./store"; import Home from "./views/Home.vue"; @@ -9,48 +10,54 @@ import Privacy from "./views/Privacy.vue"; import Transactions from "./views/Transactions.vue"; import Settings from "./views/Settings.vue"; import Account from "./views/Account.vue"; +import OidcCallback from "./views/OidcCallback.vue"; const routes = [ { path: "/", - allowUnauthenticated: false, + isPublic: false, component: Home, }, { path: "/transactions", - allowUnauthenticated: false, + isPublic: false, component: Transactions, }, { path: "/settings", - allowUnauthenticated: false, + isPublic: false, component: Settings, }, { path: "/account", - allowUnauthenticated: false, + isPublic: false, component: Account, }, { path: "/login", - allowUnauthenticated: true, + isPublic: true, component: Login, }, { path: "/forgot", - allowUnauthenticated: true, + isPublic: true, component: Forgot, }, { path: "/signup", - allowUnauthenticated: true, + isPublic: true, component: Signup, }, { path: "/privacy", - allowUnauthenticated: true, + isPublic: true, component: Privacy, }, + { + path: "/oidc-callback", + isPublic: true, + component: OidcCallback, + }, ]; const router = createRouter({ @@ -60,8 +67,8 @@ const router = createRouter({ router.beforeEach((to, from, next) => { console.log("store.state.profile.isAuthenticated: " + store.state.profile.isAuthenticated); - const unRestrictedPaths = routes.filter((r) => r.allowUnauthenticated); - if (unRestrictedPaths.every((c) => c.path !== to.path) && !store.state.profile.isAuthenticated) + const publicPaths = routes.filter((r) => r.isPublic); + if (publicPaths.every((c) => c.path !== to.path) && !store.state.profile.isAuthenticated) next("/login"); else next(); }); diff --git a/src/browser/src/store.js b/src/browser/src/store.js index 0dd3195..c775ed8 100644 --- a/src/browser/src/store.js +++ b/src/browser/src/store.js @@ -3,21 +3,11 @@ import { createStore } from "vuex"; export default createStore({ strict: true, state: { - profile: { - id: "", - username: "", - sessionStart: "", - isAuthenticated: false, - }, + profile: {}, }, mutations: { setProfileData(state, profile) { - state.profile = { - id: profile.id, - username: profile.username, - sessionStart: profile.sessionStart, - isAuthenticated: profile.id !== "", - }; + state.profile = profile; }, }, actions: {}, diff --git a/src/browser/src/views/Login.vue b/src/browser/src/views/Login.vue index 07b5b31..a94ea9e 100644 --- a/src/browser/src/views/Login.vue +++ b/src/browser/src/views/Login.vue @@ -70,6 +70,7 @@ import store from "../store"; import router from "../router"; import account from "../api/account"; + export default { components: { Alert, @@ -89,6 +90,12 @@ export default { }, }); + account.signIn() + .then((user) => console.log(user)) + .catch((err) => console.error(err)); + + function submitForm() {} + /* async function submitForm() { model.isLoading = true; return; @@ -122,7 +129,7 @@ export default { } } } - +*/ const forgotPassword = () => router.replace("/forgot"); const signup = () => router.replace("/signup"); diff --git a/src/browser/src/views/OidcCallback.vue b/src/browser/src/views/OidcCallback.vue new file mode 100644 index 0000000..10ccd52 --- /dev/null +++ b/src/browser/src/views/OidcCallback.vue @@ -0,0 +1,8 @@ +<template> + <h1>callback</h1> +</template> + +<script> +import account from "../api/account"; +account.signinRedirectCallback(); +</script> 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 |
