bugfixes in token handling, added session duration property to user
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
This commit is contained in:
@@ -37,6 +37,7 @@ public class Constants {
|
|||||||
public static final String RESPONSE_TYPE = "response_type";
|
public static final String RESPONSE_TYPE = "response_type";
|
||||||
public static final String SCOPE = "scope";
|
public static final String SCOPE = "scope";
|
||||||
public static final String SECRET = "secret";
|
public static final String SECRET = "secret";
|
||||||
|
public static final String SESSION_DURATION = "session_duration";
|
||||||
public static final String SMTP_USER = "smtp_user";
|
public static final String SMTP_USER = "smtp_user";
|
||||||
public static final String SMTP_PASSWORD = "smtp_pass";
|
public static final String SMTP_PASSWORD = "smtp_pass";
|
||||||
public static final String SMTP_AUTH = "smtp_auth";
|
public static final String SMTP_AUTH = "smtp_auth";
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ public interface UserService {
|
|||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
public AccessToken accessToken(User user);
|
public AccessToken accessToken(User user);
|
||||||
|
public Optional<User> consumeToken(String accessToken);
|
||||||
public UserService delete(User user);
|
public UserService delete(User user);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
/* © SRSoftware 2024 */
|
/* © SRSoftware 2024 */
|
||||||
package de.srsoftware.oidc.api.data;
|
package de.srsoftware.oidc.api.data;
|
||||||
|
|
||||||
|
import static de.srsoftware.oidc.api.Constants.SESSION_DURATION;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import org.json.JSONObject;
|
import org.json.JSONObject;
|
||||||
|
|
||||||
@@ -15,6 +18,7 @@ public final class User {
|
|||||||
private final Set<Permission> permissions = new HashSet<>();
|
private final Set<Permission> permissions = new HashSet<>();
|
||||||
|
|
||||||
private String email, hashedPassword, realName, uuid, username;
|
private String email, hashedPassword, realName, uuid, username;
|
||||||
|
private Duration sessionDuration = Duration.ofMinutes(10);
|
||||||
|
|
||||||
public User(String username, String hashedPassword, String realName, String email, String uuid) {
|
public User(String username, String hashedPassword, String realName, String email, String uuid) {
|
||||||
this.username = username;
|
this.username = username;
|
||||||
@@ -66,7 +70,9 @@ public final class User {
|
|||||||
|
|
||||||
|
|
||||||
public Map<String, Object> map(boolean includePassword) {
|
public Map<String, Object> map(boolean includePassword) {
|
||||||
return includePassword ? Map.of(USERNAME, username, REALNAME, realName, EMAIL, email, PERMISSIONS, permissions, UUID, uuid, PASSWORD, hashedPassword) : Map.of(USERNAME, username, REALNAME, realName, EMAIL, email, PERMISSIONS, permissions, UUID, uuid);
|
return includePassword //
|
||||||
|
? Map.of(USERNAME, username, REALNAME, realName, EMAIL, email, PERMISSIONS, permissions, UUID, uuid, SESSION_DURATION, sessionDuration.toMinutes(), PASSWORD, hashedPassword)
|
||||||
|
: Map.of(USERNAME, username, REALNAME, realName, EMAIL, email, PERMISSIONS, permissions, UUID, uuid, SESSION_DURATION, sessionDuration.toMinutes());
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Optional<User> of(JSONObject json, String userId) {
|
public static Optional<User> of(JSONObject json, String userId) {
|
||||||
@@ -81,6 +87,7 @@ public final class User {
|
|||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (json.has(SESSION_DURATION)) user.sessionDuration(Duration.ofMinutes(json.getInt(SESSION_DURATION)));
|
||||||
return Optional.of(user);
|
return Optional.of(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,4 +122,12 @@ public final class User {
|
|||||||
public String uuid() {
|
public String uuid() {
|
||||||
return uuid;
|
return uuid;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void sessionDuration(Duration newVal) {
|
||||||
|
sessionDuration = newVal;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Duration sessionDuration() {
|
||||||
|
return sessionDuration;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -111,7 +111,7 @@ public class TokenController extends PathHandler {
|
|||||||
String jwToken = createJWT(client, user.get());
|
String jwToken = createJWT(client, user.get());
|
||||||
ex.getResponseHeaders().add("Cache-Control", "no-store");
|
ex.getResponseHeaders().add("Cache-Control", "no-store");
|
||||||
JSONObject response = new JSONObject();
|
JSONObject response = new JSONObject();
|
||||||
response.put(ACCESS_TOKEN, users.accessToken(user.get()));
|
response.put(ACCESS_TOKEN, users.accessToken(user.get()).id());
|
||||||
response.put(TOKEN_TYPE, BEARER);
|
response.put(TOKEN_TYPE, BEARER);
|
||||||
response.put(EXPIRES_IN, 3600);
|
response.put(EXPIRES_IN, 3600);
|
||||||
response.put(ID_TOKEN, jwToken);
|
response.put(ID_TOKEN, jwToken);
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import de.srsoftware.oidc.api.data.User;
|
|||||||
import jakarta.mail.*;
|
import jakarta.mail.*;
|
||||||
import jakarta.mail.internet.*;
|
import jakarta.mail.internet.*;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.time.Duration;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
@@ -130,7 +131,7 @@ public class UserController extends Controller {
|
|||||||
}
|
}
|
||||||
if (!strong(newPass1)) return sendContent(ex, HTTP_BAD_REQUEST, "weak password");
|
if (!strong(newPass1)) return sendContent(ex, HTTP_BAD_REQUEST, "weak password");
|
||||||
var token = data.getString(TOKEN);
|
var token = data.getString(TOKEN);
|
||||||
var optUser = users.forToken(token);
|
var optUser = users.consumeToken(token);
|
||||||
if (optUser.isEmpty()) return sendContent(ex, HTTP_UNAUTHORIZED, "invalid token");
|
if (optUser.isEmpty()) return sendContent(ex, HTTP_UNAUTHORIZED, "invalid token");
|
||||||
var user = optUser.get();
|
var user = optUser.get();
|
||||||
users.updatePassword(user, newPass1);
|
users.updatePassword(user, newPass1);
|
||||||
@@ -256,6 +257,7 @@ public class UserController extends Controller {
|
|||||||
user.username(json.getString(USERNAME));
|
user.username(json.getString(USERNAME));
|
||||||
user.email(json.getString(EMAIL));
|
user.email(json.getString(EMAIL));
|
||||||
user.realName(json.getString(REALNAME));
|
user.realName(json.getString(REALNAME));
|
||||||
|
user.sessionDuration(Duration.ofMinutes(json.getInt(SESSION_DURATION)));
|
||||||
users.save(user);
|
users.save(user);
|
||||||
return sendContent(ex, user.map(false));
|
return sendContent(ex, user.map(false));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,8 +21,6 @@ import java.time.Instant;
|
|||||||
import java.time.temporal.ChronoUnit;
|
import java.time.temporal.ChronoUnit;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import java.util.stream.Stream;
|
|
||||||
|
|
||||||
import org.json.JSONObject;
|
import org.json.JSONObject;
|
||||||
|
|
||||||
public class FileStore implements AuthorizationService, ClientService, SessionService, UserService, MailConfig {
|
public class FileStore implements AuthorizationService, ClientService, SessionService, UserService, MailConfig {
|
||||||
@@ -38,7 +36,6 @@ public class FileStore implements AuthorizationService, ClientService, SessionSe
|
|||||||
private final Path storageFile;
|
private final Path storageFile;
|
||||||
private final JSONObject json;
|
private final JSONObject json;
|
||||||
private final PasswordHasher<String> passwordHasher;
|
private final PasswordHasher<String> passwordHasher;
|
||||||
private Duration sessionDuration = Duration.of(10, ChronoUnit.MINUTES);
|
|
||||||
private Map<String, Client> clients = new HashMap<>();
|
private Map<String, Client> clients = new HashMap<>();
|
||||||
private Map<String, AccessToken> accessTokens = new HashMap<>();
|
private Map<String, AccessToken> accessTokens = new HashMap<>();
|
||||||
private Map<String, Authorization> authCodes = new HashMap<>();
|
private Map<String, Authorization> authCodes = new HashMap<>();
|
||||||
@@ -59,32 +56,32 @@ public class FileStore implements AuthorizationService, ClientService, SessionSe
|
|||||||
|
|
||||||
private void cleanUp() {
|
private void cleanUp() {
|
||||||
var now = Instant.now();
|
var now = Instant.now();
|
||||||
var sessions = json.getJSONObject(SESSIONS);
|
var sessions = sessions();
|
||||||
LOG.log(DEBUG,"cleaning up sessions…");
|
LOG.log(DEBUG, "cleaning up sessions…");
|
||||||
var sessionIds = Set.copyOf(sessions.keySet());
|
var sessionIds = Set.copyOf(sessions.keySet());
|
||||||
for (var sessionId : sessionIds) {
|
for (var sessionId : sessionIds) {
|
||||||
var session = sessions.getJSONObject(sessionId);
|
var session = sessions.getJSONObject(sessionId);
|
||||||
var expiration = Instant.ofEpochSecond(session.getLong(EXPIRATION));
|
var expiration = Instant.ofEpochSecond(session.getLong(EXPIRATION));
|
||||||
if (expiration.isBefore(now)) {
|
if (expiration.isBefore(now)) {
|
||||||
sessions.remove(sessionId);
|
sessions.remove(sessionId);
|
||||||
LOG.log(DEBUG,"removed old session {0}.",sessionId);
|
LOG.log(DEBUG, "removed old session {0}.", sessionId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var authorizations = json.getJSONObject(AUTHORIZATIONS);
|
var authorizations = json.getJSONObject(AUTHORIZATIONS);
|
||||||
var authorizationUsers = Set.copyOf(authorizations.keySet());
|
var authorizationUsers = Set.copyOf(authorizations.keySet());
|
||||||
var userIds = list().stream().map(User::uuid).collect(Collectors.toSet());
|
var userIds = list().stream().map(User::uuid).collect(Collectors.toSet());
|
||||||
for (var userId : authorizationUsers){
|
for (var userId : authorizationUsers) {
|
||||||
if (!userIds.contains(userId)) {
|
if (!userIds.contains(userId)) {
|
||||||
authorizations.remove(userId);
|
authorizations.remove(userId);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
var clients = authorizations.getJSONObject(userId);
|
var clients = authorizations.getJSONObject(userId);
|
||||||
var clientIds = Set.copyOf(clients.keySet());
|
var clientIds = Set.copyOf(clients.keySet());
|
||||||
for (var clientId : clientIds){
|
for (var clientId : clientIds) {
|
||||||
var client = clients.getJSONObject(clientId);
|
var client = clients.getJSONObject(clientId);
|
||||||
var scopes = Set.copyOf(client.keySet());
|
var scopes = Set.copyOf(client.keySet());
|
||||||
for (var scope : scopes){
|
for (var scope : scopes) {
|
||||||
var expiration = Instant.ofEpochSecond(client.getLong(scope));
|
var expiration = Instant.ofEpochSecond(client.getLong(scope));
|
||||||
if (expiration.isBefore(now)) {
|
if (expiration.isBefore(now)) {
|
||||||
client.remove(scope);
|
client.remove(scope);
|
||||||
@@ -116,6 +113,12 @@ public class FileStore implements AuthorizationService, ClientService, SessionSe
|
|||||||
return token;
|
return token;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Optional<User> consumeToken(String id) {
|
||||||
|
var user = forToken(id);
|
||||||
|
accessTokens.remove(id);
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public UserService delete(User user) {
|
public UserService delete(User user) {
|
||||||
@@ -126,9 +129,10 @@ public class FileStore implements AuthorizationService, ClientService, SessionSe
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Optional<User> forToken(String id) {
|
public Optional<User> forToken(String id) {
|
||||||
AccessToken token = accessTokens.remove(id);
|
AccessToken token = accessTokens.get(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(id);
|
||||||
return empty();
|
return empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -223,13 +227,13 @@ public class FileStore implements AuthorizationService, ClientService, SessionSe
|
|||||||
@Override
|
@Override
|
||||||
public Session createSession(User user) {
|
public Session createSession(User user) {
|
||||||
var now = Instant.now();
|
var now = Instant.now();
|
||||||
var endOfSession = now.plus(sessionDuration);
|
var endOfSession = now.plus(user.sessionDuration());
|
||||||
return save(new Session(user, endOfSession, uuid().toString()));
|
return save(new Session(user, endOfSession, uuid().toString()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public SessionService dropSession(String sessionId) {
|
public SessionService dropSession(String sessionId) {
|
||||||
json.getJSONObject(SESSIONS).remove(sessionId);
|
sessions().remove(sessionId);
|
||||||
save();
|
save();
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
@@ -239,11 +243,14 @@ public class FileStore implements AuthorizationService, ClientService, SessionSe
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private JSONObject sessions() {
|
||||||
|
return json.getJSONObject(SESSIONS);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Optional<Session> retrieve(String sessionId) {
|
public Optional<Session> retrieve(String sessionId) {
|
||||||
var sessions = json.getJSONObject(SESSIONS);
|
|
||||||
try {
|
try {
|
||||||
var session = sessions.getJSONObject(sessionId);
|
var session = sessions().getJSONObject(sessionId);
|
||||||
var userId = session.getString(USER);
|
var userId = session.getString(USER);
|
||||||
var expiration = Instant.ofEpochSecond(session.getLong(EXPIRATION));
|
var expiration = Instant.ofEpochSecond(session.getLong(EXPIRATION));
|
||||||
if (expiration.isAfter(Instant.now())) {
|
if (expiration.isAfter(Instant.now())) {
|
||||||
@@ -256,7 +263,7 @@ public class FileStore implements AuthorizationService, ClientService, SessionSe
|
|||||||
}
|
}
|
||||||
|
|
||||||
private Session save(Session session) {
|
private Session save(Session session) {
|
||||||
json.getJSONObject(SESSIONS).put(session.id(), Map.of(USER, session.user().uuid(), EXPIRATION, session.expiration().getEpochSecond()));
|
sessions().put(session.id(), Map.of(USER, session.user().uuid(), EXPIRATION, session.expiration().getEpochSecond()));
|
||||||
save();
|
save();
|
||||||
return session;
|
return session;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
var session_duration_minutes = 10;
|
||||||
|
|
||||||
function fillForm(){
|
function fillForm(){
|
||||||
if (user == null){
|
if (user == null){
|
||||||
setTimeout(fillForm,100);
|
setTimeout(fillForm,100);
|
||||||
@@ -7,6 +9,9 @@ function fillForm(){
|
|||||||
setValue('email',user.email);
|
setValue('email',user.email);
|
||||||
setValue('uuid', user.uuid);
|
setValue('uuid', user.uuid);
|
||||||
setValue('realname', user.realname);
|
setValue('realname', user.realname);
|
||||||
|
session_duration_minutes = user.session_duration | 10;
|
||||||
|
displayDuration();
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,6 +69,7 @@ async function handleSettings(response){
|
|||||||
console.log('handleSettings(…)',response);
|
console.log('handleSettings(…)',response);
|
||||||
if (response.ok){
|
if (response.ok){
|
||||||
var json = await response.json();
|
var json = await response.json();
|
||||||
|
console.log("json: ",json);
|
||||||
for (var key in json){
|
for (var key in json){
|
||||||
setValue(key,json[key]);
|
setValue(key,json[key]);
|
||||||
}
|
}
|
||||||
@@ -125,7 +131,8 @@ function update(){
|
|||||||
username : getValue('username'),
|
username : getValue('username'),
|
||||||
email : getValue('email'),
|
email : getValue('email'),
|
||||||
realname : getValue('realname'),
|
realname : getValue('realname'),
|
||||||
uuid : getValue('uuid')
|
uuid : getValue('uuid'),
|
||||||
|
session_duration : session_duration_minutes
|
||||||
}
|
}
|
||||||
fetch(user_controller+'/update',{
|
fetch(user_controller+'/update',{
|
||||||
method : 'POST',
|
method : 'POST',
|
||||||
@@ -137,5 +144,37 @@ function update(){
|
|||||||
setText('updateBtn','sent…');
|
setText('updateBtn','sent…');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function displayDuration(){
|
||||||
|
var mins = session_duration_minutes;
|
||||||
|
hrs = Math.floor(mins/60);
|
||||||
|
mins-=60*hrs;
|
||||||
|
days = Math.floor(hrs/24);
|
||||||
|
hrs-=24*days;
|
||||||
|
setText('days',days);
|
||||||
|
setText('hours',hrs);
|
||||||
|
setText('minutes',mins);
|
||||||
|
}
|
||||||
|
|
||||||
|
function durationUpdate(){
|
||||||
|
var raw = getValue('session_duration');
|
||||||
|
console.log(raw);
|
||||||
|
var mins = 0;
|
||||||
|
var hrs = 0;
|
||||||
|
var days = 0;
|
||||||
|
if (raw<30){
|
||||||
|
mins = raw;
|
||||||
|
} else if(raw<37) {
|
||||||
|
mins=5*(raw-24);
|
||||||
|
} else if(raw<57){
|
||||||
|
mins=15*(raw-32);
|
||||||
|
} else if(raw<75){
|
||||||
|
mins=60*(raw-50);
|
||||||
|
} else {
|
||||||
|
mins=60*24*(raw-73);
|
||||||
|
}
|
||||||
|
session_duration_minutes = mins;
|
||||||
|
displayDuration();
|
||||||
|
}
|
||||||
|
|
||||||
setTimeout(fillForm,100);
|
setTimeout(fillForm,100);
|
||||||
fetch("/api/email/settings").then(handleSettings);
|
fetch("/api/email/settings").then(handleSettings);
|
||||||
|
|||||||
@@ -30,12 +30,12 @@
|
|||||||
<td><input id="email" type="email"></td>
|
<td><input id="email" type="email"></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th>ID</th>
|
<th>Session duration</th>
|
||||||
<td><input id="uuid" type="text" disabled="true"></td>
|
<td>
|
||||||
</tr>
|
<input id="session_duration" type="range" value="50" min="1" max="103" oninput="durationUpdate()"/>
|
||||||
<tr id="update_error" style="display: none">
|
<br/>
|
||||||
<th>Error</th>
|
<span id="days"></span> days, <span id="hours"></span> hours, <span id="minutes"></span> minutes
|
||||||
<td class="warning">Failed to update settings!</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td></td>
|
<td></td>
|
||||||
@@ -53,8 +53,12 @@
|
|||||||
<td><input id="oldpass" type="password"></td>
|
<td><input id="oldpass" type="password"></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th></th>
|
<th>ID</th>
|
||||||
<td><input type="text" style="visibility: hidden"></td>
|
<td><input id="uuid" type="text" disabled="true"></td>
|
||||||
|
</tr>
|
||||||
|
<tr id="update_error" style="display: none">
|
||||||
|
<th>Error</th>
|
||||||
|
<td class="warning">Failed to update settings!</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th>New Password</th>
|
<th>New Password</th>
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
<div id="content">
|
<div id="content">
|
||||||
<h1>to do…</h1>
|
<h1>to do…</h1>
|
||||||
<ul>
|
<ul>
|
||||||
|
<li>Separates Email-Konto</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>
|
||||||
|
|||||||
Reference in New Issue
Block a user