From 62c85410a99f9b143bf86ec6b4ec9babc28ddc48 Mon Sep 17 00:00:00 2001 From: Stephan Richter Date: Fri, 9 Aug 2024 23:56:40 +0200 Subject: [PATCH] implemented password reset flow Signed-off-by: Stephan Richter --- .../java/de/srsoftware/http/PathHandler.java | 8 +++ .../oidc/backend/UserController.java | 41 ++++++++++---- .../oidc/datastore/file/FileStore.java | 3 +- .../src/main/resources/en/login.html | 2 +- .../src/main/resources/en/reset.html | 53 +++++++++++++++++++ .../src/main/resources/en/scripts/login.js | 9 ++-- .../src/main/resources/en/scripts/reset.js | 53 +++++++++++++++++++ .../src/main/resources/en/scripts/settings.js | 2 +- .../src/main/resources/en/scripts/users.js | 7 +-- .../src/main/resources/en/todo.html | 3 +- 10 files changed, 155 insertions(+), 26 deletions(-) create mode 100644 de.srsoftware.oidc.web/src/main/resources/en/reset.html create mode 100644 de.srsoftware.oidc.web/src/main/resources/en/scripts/reset.js diff --git a/de.srsoftware.http/src/main/java/de/srsoftware/http/PathHandler.java b/de.srsoftware.http/src/main/java/de/srsoftware/http/PathHandler.java index 0a71049..20ffb4d 100644 --- a/de.srsoftware.http/src/main/java/de/srsoftware/http/PathHandler.java +++ b/de.srsoftware.http/src/main/java/de/srsoftware/http/PathHandler.java @@ -12,6 +12,7 @@ import com.sun.net.httpserver.HttpHandler; import com.sun.net.httpserver.HttpServer; import java.io.IOException; import java.util.*; +import java.util.stream.Collectors; import java.util.stream.Stream; import org.json.JSONObject; @@ -145,6 +146,13 @@ public abstract class PathHandler implements HttpHandler { return sendEmptyResponse(HTTP_NOT_FOUND, ex); } + public Map queryParam(HttpExchange ex) { + return Arrays + .stream(ex.getRequestURI().getQuery().split("&")) // + .map(s -> s.split("=", 2)) + .collect(Collectors.toMap(arr -> arr[0], arr -> arr[1])); + } + public static boolean sendEmptyResponse(int statusCode, HttpExchange ex) throws IOException { ex.sendResponseHeaders(statusCode, 0); return false; diff --git a/de.srsoftware.oidc.backend/src/main/java/de/srsoftware/oidc/backend/UserController.java b/de.srsoftware.oidc.backend/src/main/java/de/srsoftware/oidc/backend/UserController.java index 0e70f5f..aaa043c 100644 --- a/de.srsoftware.oidc.backend/src/main/java/de/srsoftware/oidc/backend/UserController.java +++ b/de.srsoftware.oidc.backend/src/main/java/de/srsoftware/oidc/backend/UserController.java @@ -2,6 +2,7 @@ package de.srsoftware.oidc.backend; import static de.srsoftware.oidc.api.Constants.APP_NAME; +import static de.srsoftware.oidc.api.Constants.TOKEN; import static de.srsoftware.oidc.api.data.Permission.MANAGE_USERS; import static de.srsoftware.oidc.api.data.User.*; import static de.srsoftware.utils.Strings.uuid; @@ -48,9 +49,8 @@ public class UserController extends Controller { switch (path) { case "/info": return userInfo(ex); - case "/reset": - return checkResetLink(ex); + return generateResetLink(ex); } var optSession = getSession(ex); if (optSession.isEmpty()) return sendEmptyResponse(HTTP_UNAUTHORIZED, ex); @@ -94,9 +94,27 @@ public class UserController extends Controller { return notFound(ex); } - private boolean checkResetLink(HttpExchange ex) { - // TODO - throw new RuntimeException("not implemented"); + private boolean resetPassword(HttpExchange ex) throws IOException { + var data = json(ex); + if (!data.has(TOKEN)) return sendContent(ex, HTTP_UNAUTHORIZED, "token missing"); + var newpass = data.getJSONArray("newpass"); + var newPass1 = newpass.getString(0); + if (!newPass1.equals(newpass.getString(1))) { + return badRequest(ex, "password mismatch"); + } + if (!strong(newPass1)) return sendContent(ex, HTTP_BAD_REQUEST, "weak password"); + var token = data.getString(TOKEN); + var optUser = users.forToken(token); + if (optUser.isEmpty()) return sendContent(ex, HTTP_UNAUTHORIZED, "invalid token"); + var user = optUser.get(); + users.updatePassword(user, newPass1); + var session = sessions.createSession(user); + new SessionToken(session.id()).addTo(ex); + return sendRedirect(ex, "/"); + } + + private boolean strong(String pass) { + return pass.length() > 10; // TODO } private boolean list(HttpExchange ex, Session session) throws IOException { @@ -124,9 +142,12 @@ public class UserController extends Controller { return sendEmptyResponse(HTTP_OK, ex); } - private boolean resetPassword(HttpExchange ex) throws IOException { - var idOrEmail = body(ex); - var url = url(ex); + private boolean generateResetLink(HttpExchange ex) throws IOException { + var idOrEmail = queryParam(ex).get("user"); + var url = url(ex) // + .replace("/api/user/", "/web/") + .split("\\?")[0] + + ".html"; Set matchingUsers = users.find(idOrEmail); if (!matchingUsers.isEmpty()) { resourceLoader // @@ -134,13 +155,13 @@ public class UserController extends Controller { .map(ResourceLoader.Resource::content) .map(bytes -> new String(bytes, UTF_8)) .ifPresent(template -> { // - matchingUsers.forEach(user -> senPasswordLink(user, template, url)); + matchingUsers.forEach(user -> sendResetLink(user, template, url)); }); } return sendEmptyResponse(HTTP_OK, ex); } - private void senPasswordLink(User user, String template, String url) { + private void sendResetLink(User user, String template, String url) { LOG.log(WARNING, "Sending password link to {0}", user.email()); var token = users.accessToken(user); diff --git a/de.srsoftware.oidc.datastore.file/src/main/java/de/srsoftware/oidc/datastore/file/FileStore.java b/de.srsoftware.oidc.datastore.file/src/main/java/de/srsoftware/oidc/datastore/file/FileStore.java index ecab28c..e3ed137 100644 --- a/de.srsoftware.oidc.datastore.file/src/main/java/de/srsoftware/oidc/datastore/file/FileStore.java +++ b/de.srsoftware.oidc.datastore.file/src/main/java/de/srsoftware/oidc/datastore/file/FileStore.java @@ -81,10 +81,9 @@ public class FileStore implements AuthorizationService, ClientService, SessionSe @Override public Optional forToken(String id) { - AccessToken token = accessTokens.get(id); + AccessToken token = accessTokens.remove(id); if (token == null) return empty(); if (token.valid()) return Optional.of(token.user()); - accessTokens.remove(token.id()); return empty(); } diff --git a/de.srsoftware.oidc.web/src/main/resources/en/login.html b/de.srsoftware.oidc.web/src/main/resources/en/login.html index a3a4583..69633fd 100644 --- a/de.srsoftware.oidc.web/src/main/resources/en/login.html +++ b/de.srsoftware.oidc.web/src/main/resources/en/login.html @@ -33,7 +33,7 @@ - + diff --git a/de.srsoftware.oidc.web/src/main/resources/en/reset.html b/de.srsoftware.oidc.web/src/main/resources/en/reset.html new file mode 100644 index 0000000..3510f69 --- /dev/null +++ b/de.srsoftware.oidc.web/src/main/resources/en/reset.html @@ -0,0 +1,53 @@ + + + + Light OIDC + + + + + + +
+

Reset password

+
Welcome! You may now reset your password!
+
+
+ + Password + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
New Password
Repeat Password
+
+
+
+ + \ No newline at end of file diff --git a/de.srsoftware.oidc.web/src/main/resources/en/scripts/login.js b/de.srsoftware.oidc.web/src/main/resources/en/scripts/login.js index 128d3c4..14badb1 100644 --- a/de.srsoftware.oidc.web/src/main/resources/en/scripts/login.js +++ b/de.srsoftware.oidc.web/src/main/resources/en/scripts/login.js @@ -1,4 +1,4 @@ -function doRedirect(){ + function doRedirect(){ let params = new URL(document.location.toString()).searchParams; redirect( params.get("return_to") || 'index.html'); return false; @@ -25,10 +25,9 @@ function resetPw(){ return; } hide('bubble'); - fetch(user_controller+"/reset",{ - method: 'POST', - body:user - }).then(() => { + disable('resetBtn'); + setText('resetBtn','sending…'); + fetch(user_controller+"/reset?user="+user).then(() => { hide('login'); show('sent'); }); diff --git a/de.srsoftware.oidc.web/src/main/resources/en/scripts/reset.js b/de.srsoftware.oidc.web/src/main/resources/en/scripts/reset.js new file mode 100644 index 0000000..3ec11eb --- /dev/null +++ b/de.srsoftware.oidc.web/src/main/resources/en/scripts/reset.js @@ -0,0 +1,53 @@ +const urlParams = new URLSearchParams(window.location.search); +const token = urlParams.get('token'); + +async function handlePasswordResponse(response){ + if (response.ok){ + console.log(response); + setText('passBtn', 'saved.'); + if (response.redirected){ + redirect(response.url); + } + } else { + setText('passBtn', 'Update failed!'); + var text = await response.text(); + if (text == 'invalid token') show('invalid_token'); + if (text == 'token missing') show('missing_token'); + if (text == 'password mismatch') show('password_mismatch'); + if (text == 'weak password') show('weak_password'); + } + enable('passBtn'); + setTimeout(function(){ + setText('passBtn','Update'); + },10000); +} + +function passKeyDown(ev){ + if (event.keyCode == 13) updatePass(); +} + +function updatePass(){ + disable('passBtn'); + hide('missing_token'); + hide('invalid_token'); + hide('password_mismatch'); + hide('weak_password'); + var newData = { + newpass : [getValue('newpass1'),getValue('newpass2')], + token : token + } + fetch(user_controller+'/reset',{ + method : 'POST', + headers : { + 'Content-Type': 'application/json' + }, + body : JSON.stringify(newData) + }).then(handlePasswordResponse); + setText('passBtn','sent…'); +} + +function missingToken(){ + show('missing_token'); + disable('passBtn'); +} +if (!token) setTimeout(missingToken,100); diff --git a/de.srsoftware.oidc.web/src/main/resources/en/scripts/settings.js b/de.srsoftware.oidc.web/src/main/resources/en/scripts/settings.js index e10591e..e7c24e4 100644 --- a/de.srsoftware.oidc.web/src/main/resources/en/scripts/settings.js +++ b/de.srsoftware.oidc.web/src/main/resources/en/scripts/settings.js @@ -138,4 +138,4 @@ function update(){ } setTimeout(fillForm,100); -fetch("/api/email/settings").then(handleSettings); \ No newline at end of file +fetch("/api/email/settings").then(handleSettings); diff --git a/de.srsoftware.oidc.web/src/main/resources/en/scripts/users.js b/de.srsoftware.oidc.web/src/main/resources/en/scripts/users.js index 1dd7d91..98451fe 100644 --- a/de.srsoftware.oidc.web/src/main/resources/en/scripts/users.js +++ b/de.srsoftware.oidc.web/src/main/resources/en/scripts/users.js @@ -57,12 +57,9 @@ function remove(userId){ } function reset_password(userid){ - fetch(user_controller+"/reset",{ - method: 'POST', - body:userid - }).then(() => { + fetch(user_controller+"/reset?user="+userid).then(() => { disable('reset-'+userid); }); } -fetch(user_controller+"/list",{method:'POST'}).then(handleUsers); \ No newline at end of file +fetch(user_controller+"/list",{method:'POST'}).then(handleUsers); diff --git a/de.srsoftware.oidc.web/src/main/resources/en/todo.html b/de.srsoftware.oidc.web/src/main/resources/en/todo.html index 426622e..700a417 100644 --- a/de.srsoftware.oidc.web/src/main/resources/en/todo.html +++ b/de.srsoftware.oidc.web/src/main/resources/en/todo.html @@ -13,14 +13,13 @@

to do…