implemented password reset flow
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
This commit is contained in:
@@ -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<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 {
|
||||
ex.sendResponseHeaders(statusCode, 0);
|
||||
return false;
|
||||
|
||||
@@ -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<User> 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);
|
||||
|
||||
|
||||
@@ -81,10 +81,9 @@ public class FileStore implements AuthorizationService, ClientService, SessionSe
|
||||
|
||||
@Override
|
||||
public Optional<User> 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();
|
||||
}
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<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>
|
||||
</table>
|
||||
</fieldset>
|
||||
|
||||
53
de.srsoftware.oidc.web/src/main/resources/en/reset.html
Normal file
53
de.srsoftware.oidc.web/src/main/resources/en/reset.html
Normal file
@@ -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>
|
||||
@@ -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');
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
@@ -138,4 +138,4 @@ function update(){
|
||||
}
|
||||
|
||||
setTimeout(fillForm,100);
|
||||
fetch("/api/email/settings").then(handleSettings);
|
||||
fetch("/api/email/settings").then(handleSettings);
|
||||
|
||||
@@ -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);
|
||||
fetch(user_controller+"/list",{method:'POST'}).then(handleUsers);
|
||||
|
||||
@@ -13,14 +13,13 @@
|
||||
<h1>to do…</h1>
|
||||
<ul>
|
||||
<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>at_hash in ID Token</li>
|
||||
<li>drop outdated sessions</li>
|
||||
<li>invalidate tokens</li>
|
||||
<li>implement token refresh</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>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
Reference in New Issue
Block a user