Browse Source

implemented password reset flow

Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
sqlite
Stephan Richter 3 months ago
parent
commit
62c85410a9
  1. 8
      de.srsoftware.http/src/main/java/de/srsoftware/http/PathHandler.java
  2. 41
      de.srsoftware.oidc.backend/src/main/java/de/srsoftware/oidc/backend/UserController.java
  3. 3
      de.srsoftware.oidc.datastore.file/src/main/java/de/srsoftware/oidc/datastore/file/FileStore.java
  4. 2
      de.srsoftware.oidc.web/src/main/resources/en/login.html
  5. 53
      de.srsoftware.oidc.web/src/main/resources/en/reset.html
  6. 9
      de.srsoftware.oidc.web/src/main/resources/en/scripts/login.js
  7. 53
      de.srsoftware.oidc.web/src/main/resources/en/scripts/reset.js
  8. 2
      de.srsoftware.oidc.web/src/main/resources/en/scripts/settings.js
  9. 7
      de.srsoftware.oidc.web/src/main/resources/en/scripts/users.js
  10. 3
      de.srsoftware.oidc.web/src/main/resources/en/todo.html

8
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 com.sun.net.httpserver.HttpServer;
import java.io.IOException; import java.io.IOException;
import java.util.*; import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream; import java.util.stream.Stream;
import org.json.JSONObject; import org.json.JSONObject;
@ -145,6 +146,13 @@ public abstract class PathHandler implements HttpHandler {
return sendEmptyResponse(HTTP_NOT_FOUND, ex); return sendEmptyResponse(HTTP_NOT_FOUND, ex);
} }
public Map<String, String> 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 { public static boolean sendEmptyResponse(int statusCode, HttpExchange ex) throws IOException {
ex.sendResponseHeaders(statusCode, 0); ex.sendResponseHeaders(statusCode, 0);
return false; return false;

41
de.srsoftware.oidc.backend/src/main/java/de/srsoftware/oidc/backend/UserController.java

@ -2,6 +2,7 @@
package de.srsoftware.oidc.backend; package de.srsoftware.oidc.backend;
import static de.srsoftware.oidc.api.Constants.APP_NAME; 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.Permission.MANAGE_USERS;
import static de.srsoftware.oidc.api.data.User.*; import static de.srsoftware.oidc.api.data.User.*;
import static de.srsoftware.utils.Strings.uuid; import static de.srsoftware.utils.Strings.uuid;
@ -48,9 +49,8 @@ public class UserController extends Controller {
switch (path) { switch (path) {
case "/info": case "/info":
return userInfo(ex); return userInfo(ex);
case "/reset": case "/reset":
return checkResetLink(ex); return generateResetLink(ex);
} }
var optSession = getSession(ex); var optSession = getSession(ex);
if (optSession.isEmpty()) return sendEmptyResponse(HTTP_UNAUTHORIZED, ex); if (optSession.isEmpty()) return sendEmptyResponse(HTTP_UNAUTHORIZED, ex);
@ -94,9 +94,27 @@ public class UserController extends Controller {
return notFound(ex); return notFound(ex);
} }
private boolean checkResetLink(HttpExchange ex) { private boolean resetPassword(HttpExchange ex) throws IOException {
// TODO var data = json(ex);
throw new RuntimeException("not implemented"); 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 { private boolean list(HttpExchange ex, Session session) throws IOException {
@ -124,9 +142,12 @@ public class UserController extends Controller {
return sendEmptyResponse(HTTP_OK, ex); return sendEmptyResponse(HTTP_OK, ex);
} }
private boolean resetPassword(HttpExchange ex) throws IOException { private boolean generateResetLink(HttpExchange ex) throws IOException {
var idOrEmail = body(ex); var idOrEmail = queryParam(ex).get("user");
var url = url(ex); var url = url(ex) //
.replace("/api/user/", "/web/")
.split("\\?")[0] +
".html";
Set<User> matchingUsers = users.find(idOrEmail); Set<User> matchingUsers = users.find(idOrEmail);
if (!matchingUsers.isEmpty()) { if (!matchingUsers.isEmpty()) {
resourceLoader // resourceLoader //
@ -134,13 +155,13 @@ public class UserController extends Controller {
.map(ResourceLoader.Resource::content) .map(ResourceLoader.Resource::content)
.map(bytes -> new String(bytes, UTF_8)) .map(bytes -> new String(bytes, UTF_8))
.ifPresent(template -> { // .ifPresent(template -> { //
matchingUsers.forEach(user -> senPasswordLink(user, template, url)); matchingUsers.forEach(user -> sendResetLink(user, template, url));
}); });
} }
return sendEmptyResponse(HTTP_OK, ex); 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()); LOG.log(WARNING, "Sending password link to {0}", user.email());
var token = users.accessToken(user); var token = users.accessToken(user);

3
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 @Override
public Optional<User> forToken(String id) { public Optional<User> forToken(String id) {
AccessToken token = accessTokens.get(id); AccessToken token = accessTokens.remove(id);
if (token == null) return empty(); if (token == null) return empty();
if (token.valid()) return Optional.of(token.user()); if (token.valid()) return Optional.of(token.user());
accessTokens.remove(token.id());
return empty(); return empty();
} }

2
de.srsoftware.oidc.web/src/main/resources/en/login.html

@ -33,7 +33,7 @@
</tr> </tr>
<tr> <tr>
<td></td> <td></td>
<td><button type="button" class="light" onClick="resetPw()">reset password?</button></td> <td><button type="button" id="resetBtn" class="light" onClick="resetPw()">reset password?</button></td>
</tr> </tr>
</table> </table>
</fieldset> </fieldset>

53
de.srsoftware.oidc.web/src/main/resources/en/reset.html

@ -0,0 +1,53 @@
<html>
<head>
<meta charset="utf-8">
<title>Light OIDC</title>
<script src="scripts/common.js"></script>
<script src="scripts/reset.js"></script>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<nav></nav>
<div id="content">
<h1>Reset password</h1>
<div>Welcome! You may now reset your password!</div>
<form>
<fieldset>
<legend>
Password
</legend>
<table>
<tr>
<th>New Password</th>
<td><input id="newpass1" type="password"></td>
</tr>
<tr>
<th>Repeat Password</th>
<td><input id="newpass2" type="password" onkeydown="passKeyDown()"></td>
</tr>
<tr id="password_mismatch" style="display: none">
<th>Error</th>
<td class="warning">Mismatch between new password and repetition!</td>
</tr>
<tr id="weak_password" style="display: none">
<th>Error</th>
<td class="warning">Your password is too weak!</td>
</tr>
<tr id="missing_token" style="display: none">
<th>Error</th>
<td class="warning">Access token missing!</td>
</tr>
<tr id="invalid_token" style="display: none">
<th>Error</th>
<td class="warning">I received an access token, but it is invalid!</td>
</tr>
<tr>
<td></td>
<td><button id="passBtn" type="button" onClick="updatePass()">Update</button></td>
</tr>
</table>
</fieldset>
</form>
</div>
</body>
</html>

9
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; let params = new URL(document.location.toString()).searchParams;
redirect( params.get("return_to") || 'index.html'); redirect( params.get("return_to") || 'index.html');
return false; return false;
@ -25,10 +25,9 @@ function resetPw(){
return; return;
} }
hide('bubble'); hide('bubble');
fetch(user_controller+"/reset",{ disable('resetBtn');
method: 'POST', setText('resetBtn','sending…');
body:user fetch(user_controller+"/reset?user="+user).then(() => {
}).then(() => {
hide('login'); hide('login');
show('sent'); show('sent');
}); });

53
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);

2
de.srsoftware.oidc.web/src/main/resources/en/scripts/settings.js

@ -138,4 +138,4 @@ function update(){
} }
setTimeout(fillForm,100); setTimeout(fillForm,100);
fetch("/api/email/settings").then(handleSettings); fetch("/api/email/settings").then(handleSettings);

7
de.srsoftware.oidc.web/src/main/resources/en/scripts/users.js

@ -57,12 +57,9 @@ function remove(userId){
} }
function reset_password(userid){ function reset_password(userid){
fetch(user_controller+"/reset",{ fetch(user_controller+"/reset?user="+userid).then(() => {
method: 'POST',
body:userid
}).then(() => {
disable('reset-'+userid); disable('reset-'+userid);
}); });
} }
fetch(user_controller+"/list",{method:'POST'}).then(handleUsers); fetch(user_controller+"/list",{method:'POST'}).then(handleUsers);

3
de.srsoftware.oidc.web/src/main/resources/en/todo.html

@ -13,14 +13,13 @@
<h1>to do…</h1> <h1>to do…</h1>
<ul> <ul>
<li><a href="users.html">Users: remove</a></li> <li><a href="users.html">Users: remove</a></li>
<li><a href="users.html">Users: send password reset link</a></li>
<li><a href="login.html">Login: send password reset link</a></li>
<li><a href="login.html">Login: "remember me" option</a></li> <li><a href="login.html">Login: "remember me" option</a></li>
<li>at_hash in ID Token</li> <li>at_hash in ID Token</li>
<li>drop outdated sessions</li> <li>drop outdated sessions</li>
<li>invalidate tokens</li> <li>invalidate tokens</li>
<li>implement token refresh</li> <li>implement token refresh</li>
<li>handle https correctly in PathHandler.hostname</li> <li>handle https correctly in PathHandler.hostname</li>
<li>bessere Implementierung für UserController.stron(pass), anwendung überall da wo passworte geändert werden können</li>
</ul> </ul>
</div> </div>
</body> </body>

Loading…
Cancel
Save