Browse Source

bugfixes in token handling, added session duration property to user

Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
sqlite
Stephan Richter 4 months ago
parent
commit
5057b54bef
  1. 1
      de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/Constants.java
  2. 5
      de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/UserService.java
  3. 19
      de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/data/User.java
  4. 2
      de.srsoftware.oidc.backend/src/main/java/de/srsoftware/oidc/backend/TokenController.java
  5. 4
      de.srsoftware.oidc.backend/src/main/java/de/srsoftware/oidc/backend/UserController.java
  6. 51
      de.srsoftware.oidc.datastore.file/src/main/java/de/srsoftware/oidc/datastore/file/FileStore.java
  7. 41
      de.srsoftware.oidc.web/src/main/resources/en/scripts/settings.js
  8. 20
      de.srsoftware.oidc.web/src/main/resources/en/settings.html
  9. 1
      de.srsoftware.oidc.web/src/main/resources/en/todo.html

1
de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/Constants.java

@ -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";

5
de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/UserService.java

@ -13,8 +13,9 @@ public interface UserService {
* @param user * @param user
* @return * @return
*/ */
public AccessToken accessToken(User user); public AccessToken accessToken(User user);
public UserService delete(User user); public Optional<User> consumeToken(String accessToken);
public UserService delete(User user);
/** /**
* return the user identified by its access token * return the user identified by its access token

19
de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/data/User.java

@ -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;
@ -14,7 +17,8 @@ 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;
}
} }

2
de.srsoftware.oidc.backend/src/main/java/de/srsoftware/oidc/backend/TokenController.java

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

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

@ -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));
} }

51
de.srsoftware.oidc.datastore.file/src/main/java/de/srsoftware/oidc/datastore/file/FileStore.java

@ -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,10 +36,9 @@ 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<>();
private Authenticator auth; private Authenticator auth;
public FileStore(File storage, PasswordHasher<String> passwordHasher) throws IOException { public FileStore(File storage, PasswordHasher<String> passwordHasher) throws IOException {
@ -58,33 +55,33 @@ 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;
} }

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

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

20
de.srsoftware.oidc.web/src/main/resources/en/settings.html

@ -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>

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

@ -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>

Loading…
Cancel
Save