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 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;
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
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;
|
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');
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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);
|
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){
|
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);
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user