aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--src/browser/package-lock.json31
-rw-r--r--src/browser/package.json3
-rw-r--r--src/browser/src/api/account.js178
-rw-r--r--src/browser/src/constants.js15
-rw-r--r--src/browser/src/main.js5
-rw-r--r--src/browser/src/router.js27
-rw-r--r--src/browser/src/store.js14
-rw-r--r--src/browser/src/views/Login.vue9
-rw-r--r--src/browser/src/views/OidcCallback.vue8
-rw-r--r--src/server/AppData/dpkeys/key-75aa5a6c-406f-4e39-b1df-58a8d3e6af7a.xml16
-rw-r--r--src/server/Controllers/AccountController.cs71
-rw-r--r--src/server/Controllers/BaseController.cs2
-rw-r--r--src/server/Dough.csproj11
-rw-r--r--src/server/IdentityServer/Config.cs8
-rw-r--r--src/server/Models/Constants.cs8
-rw-r--r--src/server/Models/Payloads/LoginPayload.cs2
-rw-r--r--src/server/Models/Results/ErrorResult.cs8
-rw-r--r--src/server/Pages/Error.cshtml19
-rw-r--r--src/server/Pages/Error.cshtml.cs12
-rw-r--r--src/server/Pages/Login.cshtml95
-rw-r--r--src/server/Pages/Login.cshtml.cs12
-rw-r--r--src/server/Services/EmailService.cs35
-rw-r--r--src/server/Startup.cs45
-rw-r--r--src/server/tempkey.jwk1
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