From 5e5289cbc8e68c0910d69f3c75ef070871b8a287 Mon Sep 17 00:00:00 2001 From: Robert von Burg Date: Mon, 11 May 2020 17:48:38 +0200 Subject: [PATCH] [New] Implemented keepAlive of strolch sessions --- .../DefaultStrolchPrivilegeHandler.java | 23 ++- .../runtime/privilege/PrivilegeHandler.java | 27 +++- .../handler/DefaultPrivilegeHandler.java | 121 ++++++++++++---- .../privilege/handler/PrivilegeHandler.java | 44 +++++- .../privilege/helper/XmlConstants.java | 5 + .../strolch/privilege/model/Certificate.java | 40 +++-- .../model/internal/UserChallenge.java | 2 +- .../xml/CertificateStubsDomWriter.java | 37 ++--- .../xml/CertificateStubsSaxReader.java | 46 +++--- .../privilege/test/AbstractPrivilegeTest.java | 2 +- .../strolch/privilege/test/PrivilegeTest.java | 4 +- .../privilege/test/SsoHandlerTest.java | 2 +- .../rest/DefaultStrolchSessionHandler.java | 76 ++++++---- .../strolch/rest/StrolchRestfulConstants.java | 1 + .../strolch/rest/StrolchSessionHandler.java | 41 ++++-- .../rest/endpoint/AuthenticationService.java | 137 +++++++++++++++--- .../filters/AuthenticationRequestFilter.java | 2 +- .../li/strolch/rest/model/UserSession.java | 45 +++--- .../li/strolch/service/test/ServiceTest.java | 9 +- 19 files changed, 489 insertions(+), 175 deletions(-) diff --git a/li.strolch.agent/src/main/java/li/strolch/runtime/privilege/DefaultStrolchPrivilegeHandler.java b/li.strolch.agent/src/main/java/li/strolch/runtime/privilege/DefaultStrolchPrivilegeHandler.java index ccfe76051..69ba31b93 100644 --- a/li.strolch.agent/src/main/java/li/strolch/runtime/privilege/DefaultStrolchPrivilegeHandler.java +++ b/li.strolch.agent/src/main/java/li/strolch/runtime/privilege/DefaultStrolchPrivilegeHandler.java @@ -141,15 +141,15 @@ public class DefaultStrolchPrivilegeHandler extends StrolchComponent implements @Override public Certificate authenticate(String username, char[] password) { assertContainerStarted(); - Certificate certificate = this.privilegeHandler.authenticate(username, password); + Certificate certificate = this.privilegeHandler.authenticate(username, password, false); writeAudit(certificate, LOGIN, AccessType.CREATE, username); return certificate; } @Override - public Certificate authenticate(String username, char[] password, String source, Usage usage) { + public Certificate authenticate(String username, char[] password, String source, Usage usage, boolean keepAlive) { assertContainerStarted(); - Certificate certificate = this.privilegeHandler.authenticate(username, password, source, usage); + Certificate certificate = this.privilegeHandler.authenticate(username, password, source, usage, keepAlive); writeAudit(certificate, LOGIN, AccessType.CREATE, username); return certificate; } @@ -157,7 +157,7 @@ public class DefaultStrolchPrivilegeHandler extends StrolchComponent implements @Override public Certificate authenticateSingleSignOn(Object data) { assertContainerStarted(); - Certificate certificate = this.privilegeHandler.authenticateSingleSignOn(data); + Certificate certificate = this.privilegeHandler.authenticateSingleSignOn(data, false); writeAudit(certificate, LOGIN, AccessType.CREATE, certificate.getUsername()); return certificate; } @@ -165,11 +165,24 @@ public class DefaultStrolchPrivilegeHandler extends StrolchComponent implements @Override public Certificate authenticateSingleSignOn(Object data, String source) { assertContainerStarted(); - Certificate certificate = this.privilegeHandler.authenticateSingleSignOn(data, source); + Certificate certificate = this.privilegeHandler.authenticateSingleSignOn(data, source, false); writeAudit(certificate, LOGIN, AccessType.CREATE, certificate.getUsername()); return certificate; } + @Override + public Certificate refreshSession(Certificate certificate, String source) { + assertContainerStarted(); + Certificate refreshedCert = this.privilegeHandler.refresh(certificate, source); + writeAudit(refreshedCert, LOGIN, AccessType.CREATE, refreshedCert.getUsername()); + return refreshedCert; + } + + @Override + public boolean isRefreshAllowed() { + return this.privilegeHandler.isRefreshAllowed(); + } + private void writeAudit(Certificate certificate, String login, AccessType accessType, String username) { StrolchRealm realm = getContainer().getRealm(certificate); try (StrolchTransaction tx = realm.openTx(certificate, login, false).silentThreshold(1, NANOSECONDS)) { diff --git a/li.strolch.agent/src/main/java/li/strolch/runtime/privilege/PrivilegeHandler.java b/li.strolch.agent/src/main/java/li/strolch/runtime/privilege/PrivilegeHandler.java index 0a0d57595..c3d55cd25 100644 --- a/li.strolch.agent/src/main/java/li/strolch/runtime/privilege/PrivilegeHandler.java +++ b/li.strolch.agent/src/main/java/li/strolch/runtime/privilege/PrivilegeHandler.java @@ -45,7 +45,7 @@ public interface PrivilegeHandler { * * @return the certificate * - * @see li.strolch.privilege.handler.PrivilegeHandler#authenticate(String, char[]) + * @see li.strolch.privilege.handler.PrivilegeHandler#authenticate(String, char[], boolean) */ Certificate authenticate(String username, char[] password); @@ -60,12 +60,14 @@ public interface PrivilegeHandler { * the source of the request * @param usage * the usage for this authentication + * @param keepAlive + * should this session be kept alive * * @return the certificate * - * @see li.strolch.privilege.handler.PrivilegeHandler#authenticate(String, char[]) + * @see li.strolch.privilege.handler.PrivilegeHandler#authenticate(String, char[], boolean) */ - Certificate authenticate(String username, char[] password, String source, Usage usage); + Certificate authenticate(String username, char[] password, String source, Usage usage, boolean keepAlive); /** * Authenticates a user on a remote Single Sign On service. This is implemented by the @@ -95,6 +97,25 @@ public interface PrivilegeHandler { */ Certificate authenticateSingleSignOn(Object data, String source) throws PrivilegeException; + /** + * Performs a refresh of the given certificate's session by returning a new certificate + * + * @param certificate + * the certificate to refresh + * @param source + * the source of the request + * + * @return certificate a new certificate + */ + Certificate refreshSession(Certificate certificate, String source); + + /** + * Return true if refreshing sessions is allowed + * + * @return true if refreshing sessions is allowed + */ + boolean isRefreshAllowed(); + /** * Returns the {@link PrivilegeContext} for the given certificate * diff --git a/li.strolch.privilege/src/main/java/li/strolch/privilege/handler/DefaultPrivilegeHandler.java b/li.strolch.privilege/src/main/java/li/strolch/privilege/handler/DefaultPrivilegeHandler.java index 537a7c475..0484f082e 100644 --- a/li.strolch.privilege/src/main/java/li/strolch/privilege/handler/DefaultPrivilegeHandler.java +++ b/li.strolch.privilege/src/main/java/li/strolch/privilege/handler/DefaultPrivilegeHandler.java @@ -22,7 +22,6 @@ import java.io.OutputStream; import java.nio.file.Files; import java.text.MessageFormat; import java.time.LocalDateTime; -import java.time.ZoneId; import java.util.*; import java.util.Map.Entry; import java.util.stream.Collectors; @@ -124,9 +123,19 @@ public class DefaultPrivilegeHandler implements PrivilegeHandler { */ protected SecretKey secretKey; + /** + * flag if session refreshing is allowed + */ + protected boolean allowSessionRefresh; + protected PrivilegeConflictResolution privilegeConflictResolution; private String identifier; + @Override + public boolean isRefreshAllowed() { + return this.allowSessionRefresh; + } + @Override public EncryptionHandler getEncryptionHandler() throws PrivilegeException { return this.encryptionHandler; @@ -1038,7 +1047,7 @@ public class DefaultPrivilegeHandler implements PrivilegeHandler { if (ctx.getUserRep().getUsername().equals(newUser.getUsername())) { Certificate cert = ctx.getCertificate(); cert = buildCertificate(cert.getUsage(), newUser, cert.getAuthToken(), cert.getSessionId(), - cert.getSource(), cert.getLoginTime()); + cert.getSource(), cert.getLoginTime(), cert.isKeepAlive()); PrivilegeContext privilegeContext = buildPrivilegeContext(cert, newUser); this.privilegeContextMap.put(cert.getSessionId(), privilegeContext); } @@ -1063,7 +1072,7 @@ public class DefaultPrivilegeHandler implements PrivilegeHandler { Certificate cert = ctx.getCertificate(); cert = buildCertificate(cert.getUsage(), user, cert.getAuthToken(), cert.getSessionId(), - cert.getSource(), cert.getLoginTime()); + cert.getSource(), cert.getLoginTime(), cert.isKeepAlive()); PrivilegeContext privilegeContext = buildPrivilegeContext(cert, user); this.privilegeContextMap.put(cert.getSessionId(), privilegeContext); } @@ -1128,7 +1137,7 @@ public class DefaultPrivilegeHandler implements PrivilegeHandler { // create a new certificate, with details of the user Usage usage = userChallenge.getUsage(); Certificate certificate = buildCertificate(usage, user, authToken, sessionId, userChallenge.getSource(), - new Date()); + LocalDateTime.now(), false); PrivilegeContext privilegeContext = buildPrivilegeContext(certificate, user); this.privilegeContextMap.put(sessionId, privilegeContext); @@ -1145,12 +1154,12 @@ public class DefaultPrivilegeHandler implements PrivilegeHandler { } @Override - public Certificate authenticate(String username, char[] password) { - return authenticate(username, password, "unknown", Usage.ANY); + public Certificate authenticate(String username, char[] password, boolean keepAlive) { + return authenticate(username, password, "unknown", Usage.ANY, keepAlive); } @Override - public Certificate authenticate(String username, char[] password, String source, Usage usage) { + public Certificate authenticate(String username, char[] password, String source, Usage usage, boolean keepAlive) { DBC.PRE.assertNotEmpty("source must not be empty!", source); try { @@ -1178,7 +1187,8 @@ public class DefaultPrivilegeHandler implements PrivilegeHandler { String sessionId = UUID.randomUUID().toString(); // create a new certificate, with details of the user - Certificate certificate = buildCertificate(usage, user, authToken, sessionId, source, new Date()); + Certificate certificate = buildCertificate(usage, user, authToken, sessionId, source, LocalDateTime.now(), + keepAlive); PrivilegeContext privilegeContext = buildPrivilegeContext(certificate, user); this.privilegeContextMap.put(sessionId, privilegeContext); @@ -1204,12 +1214,13 @@ public class DefaultPrivilegeHandler implements PrivilegeHandler { } @Override - public Certificate authenticateSingleSignOn(Object data) throws PrivilegeException { - return authenticateSingleSignOn(data, "unknown"); + public Certificate authenticateSingleSignOn(Object data, boolean keepAlive) throws PrivilegeException { + return authenticateSingleSignOn(data, "unknown", keepAlive); } @Override - public Certificate authenticateSingleSignOn(Object data, String source) throws PrivilegeException { + public Certificate authenticateSingleSignOn(Object data, String source, boolean keepAlive) + throws PrivilegeException { DBC.PRE.assertNotEmpty("source must not be empty!", source); if (this.ssoHandler == null) throw new IllegalStateException("The SSO Handler is not configured!"); @@ -1234,7 +1245,8 @@ public class DefaultPrivilegeHandler implements PrivilegeHandler { String sessionId = UUID.randomUUID().toString(); // create a new certificate, with details of the user - Certificate certificate = buildCertificate(Usage.ANY, user, authToken, sessionId, source, new Date()); + Certificate certificate = buildCertificate(Usage.ANY, user, authToken, sessionId, source, LocalDateTime.now(), + keepAlive); PrivilegeContext privilegeContext = buildPrivilegeContext(certificate, user); this.privilegeContextMap.put(sessionId, privilegeContext); @@ -1247,12 +1259,69 @@ public class DefaultPrivilegeHandler implements PrivilegeHandler { return certificate; } + @Override + public Certificate refresh(Certificate certificate, String source) throws AccessDeniedException { + DBC.PRE.assertNotNull("certificate must not be null!", certificate); + + try { + // username must be at least 2 characters in length + if (!this.allowSessionRefresh) + throw new AccessDeniedException("Refreshing of sessions not allowed!"); + + validate(certificate); + + if (!certificate.isKeepAlive()) + throw new AccessDeniedException("Refreshing of session not allowed!"); + + if (!certificate.getSource().equals(source)) { + logger.error("Source of existing session {} is not the same as the refresh request's source {}", + certificate.getSource(), source); + } + + // check the password + User user = this.persistenceHandler.getUser(certificate.getUsername()); + + // get 2 auth tokens + String authToken = this.encryptionHandler.nextToken(); + + // get next session id + String sessionId = UUID.randomUUID().toString(); + + // create a new certificate, with details of the user + Certificate refreshedCert = buildCertificate(certificate.getUsage(), user, authToken, sessionId, source, + LocalDateTime.now(), true); + + PrivilegeContext privilegeContext = buildPrivilegeContext(refreshedCert, user); + this.privilegeContextMap.put(sessionId, privilegeContext); + + // invalidate the previous session + invalidate(certificate); + + persistSessions(); + + // log + logger.info(MessageFormat + .format("User {0} refreshed session: {1}", user.getUsername(), refreshedCert)); //$NON-NLS-1$ + + // return the certificate + return refreshedCert; + + } catch (PrivilegeException e) { + throw e; + } catch (RuntimeException e) { + logger.error(e.getMessage(), e); + String msg = "User {0} failed to refresh session: {1}"; //$NON-NLS-1$ + msg = MessageFormat.format(msg, certificate.getUsername(), e.getMessage()); + throw new PrivilegeException(msg, e); + } + } + private Certificate buildCertificate(Usage usage, User user, String authToken, String sessionId, String source, - Date loginTime) { + LocalDateTime loginTime, boolean keepAlive) { DBC.PRE.assertNotEmpty("source must not be empty!", source); Set userRoles = user.getRoles(); return new Certificate(usage, sessionId, user.getUsername(), user.getFirstname(), user.getLastname(), - user.getUserState(), authToken, source, loginTime, user.getLocale(), userRoles, + user.getUserState(), authToken, source, loginTime, keepAlive, user.getLocale(), userRoles, new HashMap<>(user.getProperties())); } @@ -1341,7 +1410,8 @@ public class DefaultPrivilegeHandler implements PrivilegeHandler { } // create a new certificate, with details of the user - Certificate certificate = buildCertificate(usage, user, authToken, sessionId, source, stub.getLoginTime()); + Certificate certificate = buildCertificate(usage, user, authToken, sessionId, source, stub.getLoginTime(), + stub.isKeepAlive()); certificate.setLocale(stub.getLocale()); certificate.setLastAccess(stub.getLastAccess()); @@ -1538,7 +1608,8 @@ public class DefaultPrivilegeHandler implements PrivilegeHandler { PrivilegeContext privilegeContext = this.privilegeContextMap.remove(certificate.getSessionId()); // persist sessions - persistSessions(); + if (privilegeContext != null) + persistSessions(); // return true if object was really removed boolean loggedOut = privilegeContext != null; @@ -1592,7 +1663,7 @@ public class DefaultPrivilegeHandler implements PrivilegeHandler { throw new PrivilegeException(msg); } - certificate.setLastAccess(new Date()); + certificate.setLastAccess(LocalDateTime.now()); if (!certificate.getSource().equals(this.identifier)) throw new IllegalStateException( @@ -1624,15 +1695,14 @@ public class DefaultPrivilegeHandler implements PrivilegeHandler { // validate that challenge certificate is not expired (1 hour only) if (sessionCertificate.getUsage() != Usage.ANY) { - LocalDateTime dateTime = LocalDateTime - .ofInstant(sessionCertificate.getLoginTime().toInstant(), ZoneId.systemDefault()); + LocalDateTime dateTime = sessionCertificate.getLoginTime(); if (dateTime.plusHours(1).isBefore(LocalDateTime.now())) { invalidate(sessionCertificate); throw new NotAuthenticatedException("Certificate has already expired!"); //$NON-NLS-1$ } } - certificate.setLastAccess(new Date()); + certificate.setLastAccess(LocalDateTime.now()); // TODO decide if we want to assert source did not change! if (!source.equals(SOURCE_UNKNOWN) && !certificate.getSource().equals(source)) { @@ -1728,6 +1798,8 @@ public class DefaultPrivilegeHandler implements PrivilegeHandler { handleConflictResolutionParam(parameterMap); handleSecretParams(parameterMap); + this.allowSessionRefresh = Boolean.parseBoolean(parameterMap.get(PARAM_ALLOW_SESSION_REFRESH)); + // validate policies on privileges of Roles for (Role role : persistenceHandler.getAllRoles()) { validatePolicies(role); @@ -1938,11 +2010,8 @@ public class DefaultPrivilegeHandler implements PrivilegeHandler { * the char array containing the passwort which is to be set to zeroes */ private void clearPassword(char[] password) { - if (password != null) { - for (int i = 0; i < password.length; i++) { - password[i] = 0; - } - } + if (password != null) + Arrays.fill(password, (char) 0); } @Override @@ -2054,7 +2123,7 @@ public class DefaultPrivilegeHandler implements PrivilegeHandler { // create a new certificate, with details of the user Certificate systemUserCertificate = buildCertificate(Usage.ANY, user, authToken, sessionId, this.identifier, - new Date()); + LocalDateTime.now(), false); // create and save a new privilege context PrivilegeContext privilegeContext = buildPrivilegeContext(systemUserCertificate, user); diff --git a/li.strolch.privilege/src/main/java/li/strolch/privilege/handler/PrivilegeHandler.java b/li.strolch.privilege/src/main/java/li/strolch/privilege/handler/PrivilegeHandler.java index e31fd1547..441e9b29b 100644 --- a/li.strolch.privilege/src/main/java/li/strolch/privilege/handler/PrivilegeHandler.java +++ b/li.strolch.privilege/src/main/java/li/strolch/privilege/handler/PrivilegeHandler.java @@ -166,6 +166,11 @@ public interface PrivilegeHandler { */ String PARAM_SECRET_KEY = "secretKey"; //$NON-NLS-1$ + /** + * configuration parameter to define if session refreshing is allowed + */ + String PARAM_ALLOW_SESSION_REFRESH = "allowSessionRefresh"; //$NON-NLS-1$ + /** * configuration parameter to define a secret salt */ @@ -608,13 +613,15 @@ public interface PrivilegeHandler { * @param password * the password with which this user is to be authenticated. Null passwords are not accepted and they must meet * the requirements of the {@link #validatePassword(char[])}-method + * @param keepAlive + * should this session be kept alive * * @return a {@link Certificate} with which this user may then perform actions * * @throws AccessDeniedException * if the user credentials are not valid */ - Certificate authenticate(String username, char[] password) throws AccessDeniedException; + Certificate authenticate(String username, char[] password, boolean keepAlive) throws AccessDeniedException; /** * Authenticates a user by validating that a {@link User} for the given username and password exist and then returns @@ -629,26 +636,31 @@ public interface PrivilegeHandler { * the source of the authentication request, i.e. remote IP * @param usage * the usage type for this authentication + * @param keepAlive + * should this session be kept alive * * @return a {@link Certificate} with which this user may then perform actions * * @throws AccessDeniedException * if the user credentials are not valid */ - Certificate authenticate(String username, char[] password, String source, Usage usage) throws AccessDeniedException; + Certificate authenticate(String username, char[] password, String source, Usage usage, boolean keepAlive) + throws AccessDeniedException; /** * Authenticates a user on a remote Single Sign On service. This is implemented by the * * @param data * the data to perform the SSO + * @param keepAlive + * should this session be kept alive * * @return the {@link Certificate} for the user * * @throws PrivilegeException * if something goes wrong with the SSO */ - Certificate authenticateSingleSignOn(Object data) throws PrivilegeException; + Certificate authenticateSingleSignOn(Object data, boolean keepAlive) throws PrivilegeException; /** * Authenticates a user on a remote Single Sign On service. This is implemented by the @@ -657,13 +669,37 @@ public interface PrivilegeHandler { * the data to perform the SSO * @param source * the source of the SSO authentication + * @param keepAlive + * may the certificate be kept alive * * @return the {@link Certificate} for the user * * @throws PrivilegeException * if something goes wrong with the SSO */ - Certificate authenticateSingleSignOn(Object data, String source) throws PrivilegeException; + Certificate authenticateSingleSignOn(Object data, String source, boolean keepAlive) throws PrivilegeException; + + /** + * Refreshes the given certificate's session with a new session, i.e. a new certificate + * + * @param certificate + * the certificate for which to perform a refresh + * @param source + * the source of the refresh request + * + * @return a {@link Certificate} with which this user may then perform actions + * + * @throws AccessDeniedException + * if the certificate is now valid, or refreshing is not allowed + */ + Certificate refresh(Certificate certificate, String source) throws AccessDeniedException; + + /** + * Return true if refreshing sessions is allowed + * + * @return true if refreshing sessions is allowed + */ + boolean isRefreshAllowed(); /** * Invalidates the session for the given {@link Certificate}, effectively logging out the user who was authenticated diff --git a/li.strolch.privilege/src/main/java/li/strolch/privilege/helper/XmlConstants.java b/li.strolch.privilege/src/main/java/li/strolch/privilege/helper/XmlConstants.java index 8aa08f8f0..f2a4ee4b1 100644 --- a/li.strolch.privilege/src/main/java/li/strolch/privilege/helper/XmlConstants.java +++ b/li.strolch.privilege/src/main/java/li/strolch/privilege/helper/XmlConstants.java @@ -183,6 +183,11 @@ public class XmlConstants { */ public static final String XML_ATTR_LOGIN_TIME = "loginTime"; + /** + * XML_ATTR_KEEP_ALIVE = "keepAlive" : + */ + public static final String XML_ATTR_KEEP_ALIVE = "keepAlive"; + /** * XML_ATTR_LAST_ACCESS = "lastAccess" : */ diff --git a/li.strolch.privilege/src/main/java/li/strolch/privilege/model/Certificate.java b/li.strolch.privilege/src/main/java/li/strolch/privilege/model/Certificate.java index c30a6a125..3c6eaeca1 100644 --- a/li.strolch.privilege/src/main/java/li/strolch/privilege/model/Certificate.java +++ b/li.strolch.privilege/src/main/java/li/strolch/privilege/model/Certificate.java @@ -18,7 +18,11 @@ package li.strolch.privilege.model; import static li.strolch.privilege.base.PrivilegeConstants.*; import java.io.Serializable; -import java.util.*; +import java.time.LocalDateTime; +import java.util.Collections; +import java.util.Locale; +import java.util.Map; +import java.util.Set; import li.strolch.privilege.base.PrivilegeConstants; import li.strolch.privilege.base.PrivilegeException; @@ -29,7 +33,7 @@ import li.strolch.utils.helper.StringHelper; /** * The {@link Certificate} is the object a client keeps when accessing a Privilege enabled system. This object is the * instance which is always used when performing an access and is returned when a user performs a login through {@link - * PrivilegeHandler#authenticate(String, char[])} + * PrivilegeHandler#authenticate(String, char[], boolean)} * * @author Robert von Burg */ @@ -43,13 +47,14 @@ public final class Certificate implements Serializable { private final UserState userState; private final String authToken; private final String source; - private final Date loginTime; + private final LocalDateTime loginTime; + private final boolean keepAlive; private final Set userRoles; private final Map propertyMap; private Locale locale; - private Date lastAccess; + private LocalDateTime lastAccess; /** * Default constructor initializing with all information needed for this certificate @@ -65,9 +70,9 @@ public final class Certificate implements Serializable { * the users session id * @param username * the users login name - * @param firstname + * @param firstName * the users first name - * @param lastname + * @param lastName * the users last name * @param authToken * the authentication token defining the users unique session and is a private field of this certificate. @@ -79,9 +84,9 @@ public final class Certificate implements Serializable { * a {@link Map} containing string value pairs of properties for the logged in user. These properties can be * edited and can be used for the user to change settings of this session */ - public Certificate(Usage usage, String sessionId, String username, String firstname, String lastname, - UserState userState, String authToken, String source, Date loginTime, Locale locale, Set userRoles, - Map propertyMap) { + public Certificate(Usage usage, String sessionId, String username, String firstName, String lastName, + UserState userState, String authToken, String source, LocalDateTime loginTime, boolean keepAlive, + Locale locale, Set userRoles, Map propertyMap) { // validate arguments are not null if (StringHelper.isEmpty(sessionId)) { @@ -106,12 +111,13 @@ public final class Certificate implements Serializable { this.usage = usage; this.sessionId = sessionId; this.username = username; - this.firstname = firstname; - this.lastname = lastname; + this.firstname = firstName; + this.lastname = lastName; this.userState = userState; this.authToken = authToken; this.source = source; this.loginTime = loginTime; + this.keepAlive = keepAlive; // if no locale is given, set default if (locale == null) @@ -125,7 +131,7 @@ public final class Certificate implements Serializable { this.propertyMap = Collections.unmodifiableMap(propertyMap); this.userRoles = Collections.unmodifiableSet(userRoles); - this.lastAccess = new Date(); + this.lastAccess = LocalDateTime.now(); } public Usage getUsage() { @@ -224,10 +230,14 @@ public final class Certificate implements Serializable { return userState; } - public Date getLoginTime() { + public LocalDateTime getLoginTime() { return this.loginTime; } + public boolean isKeepAlive() { + return this.keepAlive; + } + public String getAuthToken() { return this.authToken; } @@ -236,11 +246,11 @@ public final class Certificate implements Serializable { return this.source; } - public Date getLastAccess() { + public LocalDateTime getLastAccess() { return this.lastAccess; } - public void setLastAccess(Date lastAccess) { + public void setLastAccess(LocalDateTime lastAccess) { this.lastAccess = lastAccess; } diff --git a/li.strolch.privilege/src/main/java/li/strolch/privilege/model/internal/UserChallenge.java b/li.strolch.privilege/src/main/java/li/strolch/privilege/model/internal/UserChallenge.java index 0aa57fe05..808810df4 100644 --- a/li.strolch.privilege/src/main/java/li/strolch/privilege/model/internal/UserChallenge.java +++ b/li.strolch.privilege/src/main/java/li/strolch/privilege/model/internal/UserChallenge.java @@ -9,8 +9,8 @@ public class UserChallenge { private final String challenge; private final String source; private final LocalDateTime initiated; + private final Usage usage; private boolean fulfilled; - private Usage usage; public UserChallenge(Usage usage, User user, String challenge, String source) { this.usage = usage; diff --git a/li.strolch.privilege/src/main/java/li/strolch/privilege/xml/CertificateStubsDomWriter.java b/li.strolch.privilege/src/main/java/li/strolch/privilege/xml/CertificateStubsDomWriter.java index 7e83cd2fe..293386de4 100644 --- a/li.strolch.privilege/src/main/java/li/strolch/privilege/xml/CertificateStubsDomWriter.java +++ b/li.strolch.privilege/src/main/java/li/strolch/privilege/xml/CertificateStubsDomWriter.java @@ -15,13 +15,15 @@ */ package li.strolch.privilege.xml; +import static java.util.Comparator.comparing; +import static li.strolch.privilege.helper.XmlConstants.*; + import java.io.OutputStream; import java.util.List; -import li.strolch.privilege.helper.XmlConstants; import li.strolch.privilege.model.Certificate; import li.strolch.utils.helper.XmlHelper; -import li.strolch.utils.iso8601.ISO8601FormatFactory; +import li.strolch.utils.iso8601.ISO8601; import org.w3c.dom.Document; import org.w3c.dom.Element; @@ -30,8 +32,8 @@ import org.w3c.dom.Element; */ public class CertificateStubsDomWriter { - private List certificates; - private OutputStream outputStream; + private final List certificates; + private final OutputStream outputStream; public CertificateStubsDomWriter(List certificates, OutputStream outputStream) { this.certificates = certificates; @@ -42,40 +44,41 @@ public class CertificateStubsDomWriter { // create document root Document doc = XmlHelper.createDocument(); - Element rootElement = doc.createElement(XmlConstants.XML_ROOT_CERTIFICATES); + Element rootElement = doc.createElement(XML_ROOT_CERTIFICATES); doc.appendChild(rootElement); - this.certificates.stream().sorted((c1, c2) -> c1.getSessionId().compareTo(c2.getSessionId())).forEach(cert -> { + this.certificates.stream().sorted(comparing(Certificate::getSessionId)).forEach(cert -> { // create the certificate element - Element certElement = doc.createElement(XmlConstants.XML_CERTIFICATE); + Element certElement = doc.createElement(XML_CERTIFICATE); rootElement.appendChild(certElement); // sessionId; - certElement.setAttribute(XmlConstants.XML_ATTR_SESSION_ID, cert.getSessionId()); + certElement.setAttribute(XML_ATTR_SESSION_ID, cert.getSessionId()); // usage; - certElement.setAttribute(XmlConstants.XML_ATTR_USAGE, cert.getUsage().name()); + certElement.setAttribute(XML_ATTR_USAGE, cert.getUsage().name()); // username; - certElement.setAttribute(XmlConstants.XML_ATTR_USERNAME, cert.getUsername()); + certElement.setAttribute(XML_ATTR_USERNAME, cert.getUsername()); // authToken; - certElement.setAttribute(XmlConstants.XML_ATTR_AUTH_TOKEN, cert.getAuthToken()); + certElement.setAttribute(XML_ATTR_AUTH_TOKEN, cert.getAuthToken()); // source; - certElement.setAttribute(XmlConstants.XML_ATTR_SOURCE, cert.getSource()); + certElement.setAttribute(XML_ATTR_SOURCE, cert.getSource()); // locale; - certElement.setAttribute(XmlConstants.XML_ATTR_LOCALE, cert.getLocale().toLanguageTag()); + certElement.setAttribute(XML_ATTR_LOCALE, cert.getLocale().toLanguageTag()); // loginTime; - certElement.setAttribute(XmlConstants.XML_ATTR_LOGIN_TIME, - ISO8601FormatFactory.getInstance().formatDate(cert.getLoginTime())); + certElement.setAttribute(XML_ATTR_LOGIN_TIME, ISO8601.toString(cert.getLoginTime())); // lastAccess; - certElement.setAttribute(XmlConstants.XML_ATTR_LAST_ACCESS, - ISO8601FormatFactory.getInstance().formatDate(cert.getLastAccess())); + certElement.setAttribute(XML_ATTR_LAST_ACCESS, ISO8601.toString(cert.getLastAccess())); + + // keepAlive; + certElement.setAttribute(XML_ATTR_KEEP_ALIVE, String.valueOf(cert.isKeepAlive())); }); // write the container file to disk diff --git a/li.strolch.privilege/src/main/java/li/strolch/privilege/xml/CertificateStubsSaxReader.java b/li.strolch.privilege/src/main/java/li/strolch/privilege/xml/CertificateStubsSaxReader.java index f2dc02e3c..9f4557a40 100644 --- a/li.strolch.privilege/src/main/java/li/strolch/privilege/xml/CertificateStubsSaxReader.java +++ b/li.strolch.privilege/src/main/java/li/strolch/privilege/xml/CertificateStubsSaxReader.java @@ -16,20 +16,20 @@ package li.strolch.privilege.xml; import static li.strolch.privilege.handler.DefaultPrivilegeHandler.SOURCE_UNKNOWN; +import static li.strolch.privilege.helper.XmlConstants.*; import static li.strolch.utils.helper.StringHelper.isEmpty; import java.io.InputStream; +import java.time.LocalDateTime; import java.util.ArrayList; -import java.util.Date; import java.util.List; import java.util.Locale; import li.strolch.privilege.base.PrivilegeException; -import li.strolch.privilege.helper.XmlConstants; import li.strolch.privilege.model.Usage; import li.strolch.utils.dbc.DBC; import li.strolch.utils.helper.XmlHelper; -import li.strolch.utils.iso8601.ISO8601FormatFactory; +import li.strolch.utils.iso8601.ISO8601; import org.xml.sax.Attributes; import org.xml.sax.SAXException; import org.xml.sax.helpers.DefaultHandler; @@ -39,7 +39,7 @@ import org.xml.sax.helpers.DefaultHandler; */ public class CertificateStubsSaxReader extends DefaultHandler { - private InputStream inputStream; + private final InputStream inputStream; private List stubs; public CertificateStubsSaxReader(InputStream inputStream) { @@ -56,21 +56,20 @@ public class CertificateStubsSaxReader extends DefaultHandler { public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException { switch (qName) { - case XmlConstants.XML_ROOT_CERTIFICATES: + case XML_ROOT_CERTIFICATES: break; - case XmlConstants.XML_CERTIFICATE: + case XML_CERTIFICATE: CertificateStub stub = new CertificateStub(); - stub.usage = Usage.valueOf(attributes.getValue(XmlConstants.XML_ATTR_USAGE)); - stub.sessionId = attributes.getValue(XmlConstants.XML_ATTR_SESSION_ID); - stub.username = attributes.getValue(XmlConstants.XML_ATTR_USERNAME); - stub.authToken = attributes.getValue(XmlConstants.XML_ATTR_AUTH_TOKEN); - stub.source = attributes.getValue(XmlConstants.XML_ATTR_SOURCE); - stub.locale = Locale.forLanguageTag(attributes.getValue(XmlConstants.XML_ATTR_LOCALE)); - stub.loginTime = ISO8601FormatFactory.getInstance() - .parseDate(attributes.getValue(XmlConstants.XML_ATTR_LOGIN_TIME)); - stub.lastAccess = ISO8601FormatFactory.getInstance() - .parseDate(attributes.getValue(XmlConstants.XML_ATTR_LAST_ACCESS)); + stub.usage = Usage.valueOf(attributes.getValue(XML_ATTR_USAGE)); + stub.sessionId = attributes.getValue(XML_ATTR_SESSION_ID); + stub.username = attributes.getValue(XML_ATTR_USERNAME); + stub.authToken = attributes.getValue(XML_ATTR_AUTH_TOKEN); + stub.source = attributes.getValue(XML_ATTR_SOURCE); + stub.locale = Locale.forLanguageTag(attributes.getValue(XML_ATTR_LOCALE)); + stub.loginTime = ISO8601.parseToZdt(attributes.getValue(XML_ATTR_LOGIN_TIME)).toLocalDateTime(); + stub.lastAccess = ISO8601.parseToZdt(attributes.getValue(XML_ATTR_LAST_ACCESS)).toLocalDateTime(); + stub.keepAlive = Boolean.parseBoolean(attributes.getValue(XML_ATTR_KEEP_ALIVE)); DBC.INTERIM.assertNotEmpty("sessionId missing on sessions data!", stub.sessionId); DBC.INTERIM.assertNotEmpty("username missing on sessions data!", stub.username); @@ -87,15 +86,16 @@ public class CertificateStubsSaxReader extends DefaultHandler { } } - public class CertificateStub { + public static class CertificateStub { private Usage usage; private String sessionId; private String username; private String authToken; private String source; private Locale locale; - private Date loginTime; - private Date lastAccess; + private LocalDateTime loginTime; + private LocalDateTime lastAccess; + private boolean keepAlive; public Usage getUsage() { return this.usage; @@ -121,12 +121,16 @@ public class CertificateStubsSaxReader extends DefaultHandler { return locale; } - public Date getLoginTime() { + public LocalDateTime getLoginTime() { return loginTime; } - public Date getLastAccess() { + public LocalDateTime getLastAccess() { return lastAccess; } + + public boolean isKeepAlive() { + return this.keepAlive; + } } } diff --git a/li.strolch.privilege/src/test/java/li/strolch/privilege/test/AbstractPrivilegeTest.java b/li.strolch.privilege/src/test/java/li/strolch/privilege/test/AbstractPrivilegeTest.java index 818605f4b..2cf6397a5 100644 --- a/li.strolch.privilege/src/test/java/li/strolch/privilege/test/AbstractPrivilegeTest.java +++ b/li.strolch.privilege/src/test/java/li/strolch/privilege/test/AbstractPrivilegeTest.java @@ -22,7 +22,7 @@ public class AbstractPrivilegeTest { protected PrivilegeContext ctx; protected void login(String username, char[] password) { - Certificate certificate = privilegeHandler.authenticate(username, password); + Certificate certificate = privilegeHandler.authenticate(username, password, false); assertTrue("Certificate is null!", certificate != null); this.ctx = privilegeHandler.validate(certificate); } diff --git a/li.strolch.privilege/src/test/java/li/strolch/privilege/test/PrivilegeTest.java b/li.strolch.privilege/src/test/java/li/strolch/privilege/test/PrivilegeTest.java index a2b8e3108..15607e0d6 100644 --- a/li.strolch.privilege/src/test/java/li/strolch/privilege/test/PrivilegeTest.java +++ b/li.strolch.privilege/src/test/java/li/strolch/privilege/test/PrivilegeTest.java @@ -390,7 +390,7 @@ public class PrivilegeTest extends AbstractPrivilegeTest { } // change password back - certificate = this.privilegeHandler.authenticate(ADMIN, PASS_TED); + certificate = this.privilegeHandler.authenticate(ADMIN, PASS_TED, false); this.privilegeHandler.setUserPassword(certificate, ADMIN, ArraysHelper.copyOf(PASS_ADMIN)); this.privilegeHandler.invalidate(certificate); } @@ -742,7 +742,7 @@ public class PrivilegeTest extends AbstractPrivilegeTest { try { // testFailAuthAsBob // Will fail because user bob is not yet enabled - this.privilegeHandler.authenticate(BOB, ArraysHelper.copyOf(PASS_BOB)); + this.privilegeHandler.authenticate(BOB, ArraysHelper.copyOf(PASS_BOB), false); fail("User Bob may not authenticate because the user is not yet enabled!"); } catch (PrivilegeException e) { String msg = "User bob does not have state ENABLED and can not login!"; diff --git a/li.strolch.privilege/src/test/java/li/strolch/privilege/test/SsoHandlerTest.java b/li.strolch.privilege/src/test/java/li/strolch/privilege/test/SsoHandlerTest.java index 0cbeee57a..f5e7869cb 100644 --- a/li.strolch.privilege/src/test/java/li/strolch/privilege/test/SsoHandlerTest.java +++ b/li.strolch.privilege/src/test/java/li/strolch/privilege/test/SsoHandlerTest.java @@ -42,7 +42,7 @@ public class SsoHandlerTest extends AbstractPrivilegeTest { data.put("roles", "PrivilegeAdmin, AppUser"); // auth - Certificate cert = this.privilegeHandler.authenticateSingleSignOn(data); + Certificate cert = this.privilegeHandler.authenticateSingleSignOn(data, false); this.ctx = this.privilegeHandler.validate(cert); // validate action diff --git a/li.strolch.rest/src/main/java/li/strolch/rest/DefaultStrolchSessionHandler.java b/li.strolch.rest/src/main/java/li/strolch/rest/DefaultStrolchSessionHandler.java index fe2a7dea0..f2a5e4b00 100644 --- a/li.strolch.rest/src/main/java/li/strolch/rest/DefaultStrolchSessionHandler.java +++ b/li.strolch.rest/src/main/java/li/strolch/rest/DefaultStrolchSessionHandler.java @@ -19,9 +19,7 @@ import static li.strolch.runtime.StrolchConstants.StrolchPrivilegeConstants.PRIV import static li.strolch.runtime.StrolchConstants.StrolchPrivilegeConstants.PRIVILEGE_INVALIDATE_SESSION; import java.text.MessageFormat; -import java.time.Instant; import java.time.LocalDateTime; -import java.time.ZoneId; import java.time.temporal.ChronoUnit; import java.util.*; import java.util.concurrent.Future; @@ -51,13 +49,15 @@ import org.slf4j.LoggerFactory; public class DefaultStrolchSessionHandler extends StrolchComponent implements StrolchSessionHandler { public static final String PARAM_SESSION_TTL_MINUTES = "session.ttl.minutes"; //$NON-NLS-1$ + public static final String PARAM_SESSION_MAX_KEEP_ALIVE_MINUTES = "session.maxKeepAlive.minutes"; //$NON-NLS-1$ public static final String PARAM_SESSION_RELOAD_SESSIONS = "session.reload"; //$NON-NLS-1$ private static final Logger logger = LoggerFactory.getLogger(DefaultStrolchSessionHandler.class); private PrivilegeHandler privilegeHandler; private Map certificateMap; private boolean reloadSessions; - private long sessionTtl; + private int sessionTtlMinutes; + private int maxKeepAliveMinutes; private ScheduledFuture validateSessionsTask; private Future persistSessionsTask; @@ -66,9 +66,26 @@ public class DefaultStrolchSessionHandler extends StrolchComponent implements St super(container, componentName); } + @Override + public int getSessionTtlMinutes() { + return this.sessionTtlMinutes; + } + + @Override + public int getSessionMaxKeepAliveMinutes() { + return this.maxKeepAliveMinutes; + } + + @Override + public boolean isRefreshAllowed() { + return this.privilegeHandler.isRefreshAllowed(); + } + @Override public void initialize(ComponentConfiguration configuration) throws Exception { - this.sessionTtl = TimeUnit.MINUTES.toMillis(configuration.getInt(PARAM_SESSION_TTL_MINUTES, 30)); + this.sessionTtlMinutes = configuration.getInt(PARAM_SESSION_TTL_MINUTES, 30); + this.maxKeepAliveMinutes = configuration + .getInt(PARAM_SESSION_MAX_KEEP_ALIVE_MINUTES, Math.max(this.sessionTtlMinutes, 30)); this.reloadSessions = configuration.getBoolean(PARAM_SESSION_RELOAD_SESSIONS, false); super.initialize(configuration); } @@ -133,24 +150,11 @@ public class DefaultStrolchSessionHandler extends StrolchComponent implements St } @Override - public Certificate authenticate(String username, char[] password) { + public Certificate authenticate(String username, char[] password, String source, Usage usage, boolean keepAlive) { DBC.PRE.assertNotEmpty("Username must be set!", username); //$NON-NLS-1$ DBC.PRE.assertNotNull("Passwort must be set", password); //$NON-NLS-1$ - Certificate certificate = this.privilegeHandler.authenticate(username, password); - - this.certificateMap.put(certificate.getAuthToken(), certificate); - logger.info(MessageFormat.format("{0} sessions currently active.", this.certificateMap.size())); //$NON-NLS-1$ - - return certificate; - } - - @Override - public Certificate authenticate(String username, char[] password, String source, Usage usage) { - DBC.PRE.assertNotEmpty("Username must be set!", username); //$NON-NLS-1$ - DBC.PRE.assertNotNull("Passwort must be set", password); //$NON-NLS-1$ - - Certificate certificate = this.privilegeHandler.authenticate(username, password, source, usage); + Certificate certificate = this.privilegeHandler.authenticate(username, password, source, usage, keepAlive); this.certificateMap.put(certificate.getAuthToken(), certificate); logger.info(MessageFormat.format("{0} sessions currently active.", this.certificateMap.size())); //$NON-NLS-1$ @@ -178,6 +182,17 @@ public class DefaultStrolchSessionHandler extends StrolchComponent implements St return certificate; } + @Override + public Certificate refreshSession(Certificate certificate, String source) { + Certificate refreshedSession = this.privilegeHandler.refreshSession(certificate, source); + + invalidate(certificate); + this.certificateMap.put(refreshedSession.getAuthToken(), refreshedSession); + logger.info(MessageFormat.format("{0} sessions currently active.", this.certificateMap.size())); //$NON-NLS-1$ + + return refreshedSession; + } + @Override public Certificate validate(String authToken) throws StrolchNotAuthenticatedException { DBC.PRE.assertNotEmpty("authToken must be set!", authToken); //$NON-NLS-1$ @@ -298,15 +313,24 @@ public class DefaultStrolchSessionHandler extends StrolchComponent implements St certificateMap = new HashMap<>(this.certificateMap); } - LocalDateTime timeOutTime = LocalDateTime.now().minus(sessionTtl, ChronoUnit.MILLIS); - ZoneId systemDefault = ZoneId.systemDefault(); + LocalDateTime maxKeepAliveTime = LocalDateTime.now().minus(this.maxKeepAliveMinutes, ChronoUnit.MINUTES); + LocalDateTime timeOutTime = LocalDateTime.now().minus(this.sessionTtlMinutes, ChronoUnit.MINUTES); for (Certificate certificate : certificateMap.values()) { - Instant lastAccess = certificate.getLastAccess().toInstant(); - if (timeOutTime.isAfter(LocalDateTime.ofInstant(lastAccess, systemDefault))) { - String msg = "Session {0} for user {1} has expired, invalidating session..."; //$NON-NLS-1$ - logger.info(MessageFormat.format(msg, certificate.getSessionId(), certificate.getUsername())); - sessionTimeout(certificate); + if (certificate.isKeepAlive()) { + + if (maxKeepAliveTime.isAfter(certificate.getLoginTime())) { + String msg = "KeepAlive for session {0} for user {1} has expired, invalidating session..."; //$NON-NLS-1$ + logger.info(MessageFormat.format(msg, certificate.getSessionId(), certificate.getUsername())); + sessionTimeout(certificate); + } + + } else { + if (timeOutTime.isAfter(certificate.getLastAccess())) { + String msg = "Session {0} for user {1} has expired, invalidating session..."; //$NON-NLS-1$ + logger.info(MessageFormat.format(msg, certificate.getSessionId(), certificate.getUsername())); + sessionTimeout(certificate); + } } } } diff --git a/li.strolch.rest/src/main/java/li/strolch/rest/StrolchRestfulConstants.java b/li.strolch.rest/src/main/java/li/strolch/rest/StrolchRestfulConstants.java index 58d770870..a18ec92b8 100644 --- a/li.strolch.rest/src/main/java/li/strolch/rest/StrolchRestfulConstants.java +++ b/li.strolch.rest/src/main/java/li/strolch/rest/StrolchRestfulConstants.java @@ -25,6 +25,7 @@ public class StrolchRestfulConstants { public static final String STROLCH_CERTIFICATE = "strolch.certificate"; //$NON-NLS-1$ public static final String STROLCH_REQUEST_SOURCE= "strolch.requestSource"; //$NON-NLS-1$ public static final String STROLCH_AUTHORIZATION = "strolch.authorization"; //$NON-NLS-1$ + public static final String STROLCH_AUTHORIZATION_EXPIRATION_DATE = "strolch.authorization.expirationDate"; //$NON-NLS-1$ public static final String MSG = "msg"; public static final String EXCEPTION_MSG = "exceptionMsg"; diff --git a/li.strolch.rest/src/main/java/li/strolch/rest/StrolchSessionHandler.java b/li.strolch.rest/src/main/java/li/strolch/rest/StrolchSessionHandler.java index 22dc4b2dc..6d2091392 100644 --- a/li.strolch.rest/src/main/java/li/strolch/rest/StrolchSessionHandler.java +++ b/li.strolch.rest/src/main/java/li/strolch/rest/StrolchSessionHandler.java @@ -35,16 +35,25 @@ import li.strolch.rest.model.UserSession; public interface StrolchSessionHandler { /** - * Authenticates a user with the given credentials + * Returns the time to live for a session in minutes * - * @param username - * the username - * @param password - * the password - * - * @return the {@link Certificate} for the logged in user + * @return the time to live for a session in minutes */ - Certificate authenticate(String username, char[] password); + int getSessionTtlMinutes(); + + /** + * Returns the max keep alive for a session in minutes + * + * @return the max keep alive for a session in minutes + */ + int getSessionMaxKeepAliveMinutes(); + + /** + * Return true if refreshing sessions is allowed + * + * @return true if refreshing sessions is allowed + */ + boolean isRefreshAllowed(); /** * Authenticates a user with the given credentials @@ -57,10 +66,12 @@ public interface StrolchSessionHandler { * the source of the request * @param usage * the usage for this authentication + * @param keepAlive + * should the session have a keepAlive * * @return the {@link Certificate} for the logged in user */ - Certificate authenticate(String username, char[] password, String source, Usage usage); + Certificate authenticate(String username, char[] password, String source, Usage usage, boolean keepAlive); /** * Performs a single-sign-on with the given data, if SSO is enabled @@ -84,6 +95,18 @@ public interface StrolchSessionHandler { */ Certificate authenticateSingleSignOn(Object data, String source); + /** + * Performs a refresh of the given certificate's session by returning a new certificate + * + * @param certificate + * the certificate to refresh + * @param source + * the source of the request + * + * @return certificate a new certificate + */ + Certificate refreshSession(Certificate certificate, String source); + /** * Validates that a {@link Certificate} exists with the given auth token and is still valid * diff --git a/li.strolch.rest/src/main/java/li/strolch/rest/endpoint/AuthenticationService.java b/li.strolch.rest/src/main/java/li/strolch/rest/endpoint/AuthenticationService.java index bdd34beee..481a6e6d1 100644 --- a/li.strolch.rest/src/main/java/li/strolch/rest/endpoint/AuthenticationService.java +++ b/li.strolch.rest/src/main/java/li/strolch/rest/endpoint/AuthenticationService.java @@ -15,6 +15,8 @@ */ package li.strolch.rest.endpoint; +import static li.strolch.rest.StrolchRestfulConstants.STROLCH_AUTHORIZATION; +import static li.strolch.rest.StrolchRestfulConstants.STROLCH_AUTHORIZATION_EXPIRATION_DATE; import static li.strolch.rest.filters.AuthenticationRequestFilter.getRemoteIp; import javax.servlet.http.HttpServletRequest; @@ -22,6 +24,7 @@ import javax.ws.rs.*; import javax.ws.rs.core.*; import javax.ws.rs.core.Response.Status; import java.text.MessageFormat; +import java.time.LocalDateTime; import java.util.Base64; import java.util.Set; import java.util.concurrent.TimeUnit; @@ -36,11 +39,11 @@ import li.strolch.privilege.model.IPrivilege; import li.strolch.privilege.model.PrivilegeContext; import li.strolch.privilege.model.Usage; import li.strolch.rest.RestfulStrolchComponent; -import li.strolch.rest.StrolchRestfulConstants; import li.strolch.rest.StrolchSessionHandler; import li.strolch.rest.helper.ResponseUtil; import li.strolch.runtime.privilege.PrivilegeHandler; import li.strolch.utils.helper.ExceptionHelper; +import li.strolch.utils.iso8601.ISO8601; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -58,12 +61,12 @@ public class AuthenticationService { public Response authenticate(@Context HttpServletRequest request, @Context HttpHeaders headers, String data) { JsonObject login = new JsonParser().parse(data).getAsJsonObject(); - JsonObject loginResult = new JsonObject(); try { if (!login.has("username") || login.get("username").getAsString().length() < 2) { logger.error("Authentication failed: Username was not given or is too short!"); + JsonObject loginResult = new JsonObject(); loginResult.addProperty("msg", MessageFormat.format("Could not log in due to: {0}", "Username was not given or is too short!")); //$NON-NLS-2$ return Response.status(Status.BAD_REQUEST).entity(loginResult.toString()).build(); @@ -71,6 +74,7 @@ public class AuthenticationService { if (!login.has("password") || login.get("password").getAsString().length() < 3) { logger.error("Authentication failed: Password was not given or is too short!"); + JsonObject loginResult = new JsonObject(); loginResult.addProperty("msg", MessageFormat.format("Could not log in due to: {0}", "Password was not given or is too short!")); //$NON-NLS-2$ return Response.status(Status.BAD_REQUEST).entity(loginResult.toString()).build(); @@ -78,12 +82,14 @@ public class AuthenticationService { String username = login.get("username").getAsString(); String passwordEncoded = login.get("password").getAsString(); + boolean keepAlive = login.has("keepAlive") && login.get("keepAlive").getAsBoolean(); byte[] decode = Base64.getDecoder().decode(passwordEncoded); char[] password = new String(decode).toCharArray(); if (password.length < 3) { logger.error("Authentication failed: Password was not given or is too short!"); + JsonObject loginResult = new JsonObject(); loginResult.addProperty("msg", MessageFormat.format("Could not log in due to: {0}", "Password was not given or is too short!")); //$NON-NLS-2$ return Response.status(Status.BAD_REQUEST).entity(loginResult.toString()).build(); @@ -91,27 +97,31 @@ public class AuthenticationService { StrolchSessionHandler sessionHandler = RestfulStrolchComponent.getInstance().getSessionHandler(); String source = getRemoteIp(request); - Certificate certificate = sessionHandler.authenticate(username, password, source, Usage.ANY); + Certificate certificate = sessionHandler.authenticate(username, password, source, Usage.ANY, keepAlive); - return getAuthenticationResponse(request, loginResult, certificate, source); + return getAuthenticationResponse(request, certificate, source, true); } catch (InvalidCredentialsException e) { logger.error("Authentication failed due to: " + e.getMessage()); + JsonObject loginResult = new JsonObject(); loginResult.addProperty("msg", "Could not log in as the given credentials are invalid"); //$NON-NLS-1$ return Response.status(Status.UNAUTHORIZED).entity(loginResult.toString()).build(); } catch (AccessDeniedException e) { logger.error("Authentication failed due to: " + e.getMessage()); + JsonObject loginResult = new JsonObject(); loginResult.addProperty("msg", MessageFormat.format("Could not log in due to: {0}", e.getMessage())); //$NON-NLS-2$ return Response.status(Status.UNAUTHORIZED).entity(loginResult.toString()).build(); } catch (StrolchException | PrivilegeException e) { logger.error(e.getMessage(), e); + JsonObject loginResult = new JsonObject(); loginResult.addProperty("msg", MessageFormat.format("Could not log in due to: {0}", e.getMessage())); //$NON-NLS-2$ return Response.status(Status.FORBIDDEN).entity(loginResult.toString()).build(); } catch (Exception e) { logger.error(e.getMessage(), e); String msg = e.getMessage(); + JsonObject loginResult = new JsonObject(); loginResult.addProperty("msg", MessageFormat.format("{0}: {1}", e.getClass().getName(), msg)); //$NON-NLS-1$ return Response.serverError().entity(loginResult.toString()).build(); } @@ -122,33 +132,35 @@ public class AuthenticationService { @Path("sso") public Response authenticateSingleSignOn(@Context HttpServletRequest request, @Context HttpHeaders headers) { - JsonObject loginResult = new JsonObject(); - try { StrolchSessionHandler sessionHandler = RestfulStrolchComponent.getInstance().getSessionHandler(); String source = getRemoteIp(request); Certificate certificate = sessionHandler.authenticateSingleSignOn(request.getUserPrincipal(), source); - return getAuthenticationResponse(request, loginResult, certificate, source); + return getAuthenticationResponse(request, certificate, source, true); } catch (InvalidCredentialsException e) { logger.error("Authentication failed due to: " + e.getMessage()); + JsonObject loginResult = new JsonObject(); loginResult.addProperty("msg", "Could not log in as the given credentials are invalid"); //$NON-NLS-1$ return Response.status(Status.UNAUTHORIZED).entity(loginResult.toString()).build(); } catch (AccessDeniedException e) { logger.error("Authentication failed due to: " + e.getMessage()); + JsonObject loginResult = new JsonObject(); loginResult.addProperty("msg", MessageFormat.format("Could not log in due to: {0}", e.getMessage())); //$NON-NLS-2$ return Response.status(Status.UNAUTHORIZED).entity(loginResult.toString()).build(); } catch (StrolchException | PrivilegeException e) { logger.error(e.getMessage(), e); + JsonObject loginResult = new JsonObject(); loginResult.addProperty("msg", MessageFormat.format("Could not log in due to: {0}", e.getMessage())); //$NON-NLS-2$ return Response.status(Status.FORBIDDEN).entity(loginResult.toString()).build(); } catch (Exception e) { logger.error(e.getMessage(), e); String msg = e.getMessage(); + JsonObject loginResult = new JsonObject(); loginResult.addProperty("msg", MessageFormat.format("{0}: {1}", e.getClass().getName(), msg)); //$NON-NLS-1$ return Response.serverError().entity(loginResult.toString()).build(); } @@ -219,6 +231,68 @@ public class AuthenticationService { } } + @GET + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + @Path("{authToken}") + public Response getValidatedSession(@Context HttpServletRequest request, @PathParam("authToken") String authToken) { + + try { + + StrolchSessionHandler sessionHandler = RestfulStrolchComponent.getInstance().getSessionHandler(); + String source = getRemoteIp(request); + Certificate certificate = sessionHandler.validate(authToken, source); + + return getAuthenticationResponse(request, certificate, source, false); + + } catch (StrolchException | PrivilegeException e) { + logger.error("Session validation failed: " + e.getMessage()); + JsonObject root = new JsonObject(); + root.addProperty("msg", MessageFormat.format("Session invalid: {0}", e.getMessage())); + String json = new Gson().toJson(root); + return Response.status(Status.UNAUTHORIZED).entity(json).build(); + } catch (Exception e) { + logger.error(e.getMessage(), e); + String msg = e.getMessage(); + JsonObject root = new JsonObject(); + root.addProperty("msg", MessageFormat.format("Session invalid: {0}: {1}", e.getClass().getName(), msg)); + String json = new Gson().toJson(root); + return Response.serverError().entity(json).build(); + } + } + + @PUT + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + @Path("{authToken}") + public Response refreshSession(@Context HttpServletRequest request, @PathParam("authToken") String authToken) { + + try { + + StrolchSessionHandler sessionHandler = RestfulStrolchComponent.getInstance().getSessionHandler(); + String source = getRemoteIp(request); + Certificate certificate = sessionHandler.validate(authToken, source); + + Certificate refreshedCert = sessionHandler.refreshSession(certificate, source); + + return getAuthenticationResponse(request, refreshedCert, source, true); + + } catch (StrolchException | PrivilegeException e) { + logger.error("Session validation failed: " + e.getMessage()); + JsonObject root = new JsonObject(); + root.addProperty("msg", MessageFormat.format("Session invalid: {0}", e.getMessage())); + String json = new Gson().toJson(root); + return Response.status(Status.UNAUTHORIZED).entity(json).build(); + } catch (Exception e) { + logger.error(e.getMessage(), e); + String msg = e.getMessage(); + JsonObject root = new JsonObject(); + root.addProperty("msg", MessageFormat.format("Session invalid: {0}: {1}", e.getClass().getName(), msg)); + String json = new Gson().toJson(root); + return Response.serverError().entity(json).build(); + } + } + @POST @Produces(MediaType.APPLICATION_JSON) @Path("challenge") @@ -268,8 +342,8 @@ public class AuthenticationService { logger.warn(msg); } - NewCookie cookie = new NewCookie(StrolchRestfulConstants.STROLCH_AUTHORIZATION, certificate.getAuthToken(), - "/", null, "Authorization header", (int) TimeUnit.DAYS.toSeconds(1), secureCookie); + NewCookie cookie = new NewCookie(STROLCH_AUTHORIZATION, certificate.getAuthToken(), "/", null, + "Authorization header", (int) TimeUnit.DAYS.toSeconds(1), secureCookie); return Response.ok().entity(jsonObject.toString())// .header(HttpHeaders.AUTHORIZATION, certificate.getAuthToken()).cookie(cookie).build(); @@ -283,17 +357,37 @@ public class AuthenticationService { } } - private Response getAuthenticationResponse(HttpServletRequest request, JsonObject loginResult, - Certificate certificate, String source) { + private Response getAuthenticationResponse(HttpServletRequest request, Certificate certificate, String source, + boolean setCookies) { + + StrolchSessionHandler sessionHandler = RestfulStrolchComponent.getInstance().getSessionHandler(); + int sessionMaxKeepAliveMinutes = sessionHandler.getSessionMaxKeepAliveMinutes(); + int cookieMaxAge; + if (certificate.isKeepAlive()) { + cookieMaxAge = (int) TimeUnit.MINUTES.toSeconds(sessionMaxKeepAliveMinutes); + } else { + cookieMaxAge = RestfulStrolchComponent.getInstance().getCookieMaxAge(); + } + + LocalDateTime expirationDate = LocalDateTime.now().plusSeconds(cookieMaxAge); + String expirationDateS = ISO8601.toString(expirationDate); + + JsonObject loginResult = new JsonObject(); PrivilegeHandler privilegeHandler = RestfulStrolchComponent.getInstance().getContainer().getPrivilegeHandler(); PrivilegeContext privilegeContext = privilegeHandler.validate(certificate, source); loginResult.addProperty("sessionId", certificate.getSessionId()); - loginResult.addProperty("authToken", certificate.getAuthToken()); + String authToken = certificate.getAuthToken(); + loginResult.addProperty("authToken", authToken); loginResult.addProperty("username", certificate.getUsername()); loginResult.addProperty("firstname", certificate.getFirstname()); loginResult.addProperty("lastname", certificate.getLastname()); loginResult.addProperty("locale", certificate.getLocale().toLanguageTag()); + loginResult.addProperty("keepAlive", certificate.isKeepAlive()); + loginResult.addProperty("keepAliveMinutes", sessionMaxKeepAliveMinutes); + loginResult.addProperty("cookieMaxAge", cookieMaxAge); + loginResult.addProperty("authorizationExpiration", expirationDateS); + loginResult.addProperty("refreshAllowed", sessionHandler.isRefreshAllowed()); if (!certificate.getPropertyMap().isEmpty()) { JsonObject propObj = new JsonObject(); @@ -336,15 +430,24 @@ public class AuthenticationService { } boolean secureCookie = RestfulStrolchComponent.getInstance().isSecureCookie(); - int cookieMaxAge = RestfulStrolchComponent.getInstance().getCookieMaxAge(); if (secureCookie && !request.getScheme().equals("https")) { logger.error( "Authorization cookie is secure, but connection is not secure! Cookie won't be passed to client!"); } - NewCookie cookie = new NewCookie(StrolchRestfulConstants.STROLCH_AUTHORIZATION, certificate.getAuthToken(), "/", - null, "Authorization header", cookieMaxAge, secureCookie); - return Response.ok().entity(loginResult.toString())// - .header(HttpHeaders.AUTHORIZATION, certificate.getAuthToken()).cookie(cookie).build(); + if (setCookies) { + NewCookie authCookie = new NewCookie(STROLCH_AUTHORIZATION, authToken, "/", null, "Authorization header", + cookieMaxAge, secureCookie); + NewCookie authExpirationCookie = new NewCookie(STROLCH_AUTHORIZATION_EXPIRATION_DATE, expirationDateS, "/", + null, "Authorization Expiration Date", cookieMaxAge, secureCookie); + + return Response.ok().entity(loginResult.toString()) // + .header(HttpHeaders.AUTHORIZATION, authToken) // + .cookie(authCookie) // + .cookie(authExpirationCookie) // + .build(); + } + + return Response.ok().entity(loginResult.toString()).header(HttpHeaders.AUTHORIZATION, authToken).build(); } } diff --git a/li.strolch.rest/src/main/java/li/strolch/rest/filters/AuthenticationRequestFilter.java b/li.strolch.rest/src/main/java/li/strolch/rest/filters/AuthenticationRequestFilter.java index d8a9688ae..bbdc7c10c 100644 --- a/li.strolch.rest/src/main/java/li/strolch/rest/filters/AuthenticationRequestFilter.java +++ b/li.strolch.rest/src/main/java/li/strolch/rest/filters/AuthenticationRequestFilter.java @@ -205,7 +205,7 @@ public class AuthenticationRequestFilter implements ContainerRequestFilter { logger.info("Performing basic auth for user " + parts[0] + "..."); StrolchSessionHandler sessionHandler = RestfulStrolchComponent.getInstance().getSessionHandler(); - Certificate certificate = sessionHandler.authenticate(parts[0], parts[1].toCharArray(), remoteIp, Usage.SINGLE); + Certificate certificate = sessionHandler.authenticate(parts[0], parts[1].toCharArray(), remoteIp, Usage.SINGLE, false); requestContext.setProperty(STROLCH_CERTIFICATE, certificate); requestContext.setProperty(STROLCH_REQUEST_SOURCE, remoteIp); diff --git a/li.strolch.rest/src/main/java/li/strolch/rest/model/UserSession.java b/li.strolch.rest/src/main/java/li/strolch/rest/model/UserSession.java index dce39b42b..8c43c7d2d 100644 --- a/li.strolch.rest/src/main/java/li/strolch/rest/model/UserSession.java +++ b/li.strolch.rest/src/main/java/li/strolch/rest/model/UserSession.java @@ -15,36 +15,38 @@ */ package li.strolch.rest.model; -import java.util.Date; +import java.time.LocalDateTime; import java.util.Locale; import java.util.Set; import com.google.gson.JsonArray; import com.google.gson.JsonObject; import li.strolch.privilege.model.Certificate; -import li.strolch.utils.iso8601.ISO8601FormatFactory; +import li.strolch.utils.iso8601.ISO8601; public class UserSession { - private String sessionId; - private Date loginTime; - private String username; - private String firstname; - private String lastname; - private String source; - private Set userRoles; - private Locale locale; - private Date lastAccess; + private final boolean keepAlive; + private final String sessionId; + private final LocalDateTime loginTime; + private final String username; + private final String firstName; + private final String lastName; + private final String source; + private final Set userRoles; + private final Locale locale; + private final LocalDateTime lastAccess; public UserSession(Certificate certificate) { this.sessionId = certificate.getSessionId(); this.loginTime = certificate.getLoginTime(); this.username = certificate.getUsername(); - this.firstname = certificate.getFirstname(); - this.lastname = certificate.getLastname(); + this.firstName = certificate.getFirstname(); + this.lastName = certificate.getLastname(); this.source = certificate.getSource(); this.userRoles = certificate.getUserRoles(); this.locale = certificate.getLocale(); + this.keepAlive = certificate.isKeepAlive(); this.lastAccess = certificate.getLastAccess(); } @@ -52,7 +54,7 @@ public class UserSession { return locale; } - public Date getLastAccess() { + public LocalDateTime getLastAccess() { return lastAccess; } @@ -60,7 +62,7 @@ public class UserSession { return sessionId; } - public Date getLoginTime() { + public LocalDateTime getLoginTime() { return loginTime; } @@ -69,11 +71,11 @@ public class UserSession { } public String getFirstname() { - return firstname; + return firstName; } public String getLastname() { - return lastname; + return lastName; } public String getSource() { @@ -88,14 +90,15 @@ public class UserSession { JsonObject jsonObject = new JsonObject(); jsonObject.addProperty("sessionId", this.sessionId); - jsonObject.addProperty("loginTime", ISO8601FormatFactory.getInstance().formatDate(this.loginTime)); + jsonObject.addProperty("loginTime", ISO8601.toString(this.loginTime)); jsonObject.addProperty("username", this.username); - jsonObject.addProperty("firstname", this.firstname); - jsonObject.addProperty("lastname", this.lastname); + jsonObject.addProperty("firstname", this.firstName); + jsonObject.addProperty("lastname", this.lastName); jsonObject.addProperty("source", this.source); + jsonObject.addProperty("keepAlive", this.keepAlive); jsonObject.addProperty("locale", this.locale.toString()); - jsonObject.addProperty("lastAccess", ISO8601FormatFactory.getInstance().formatDate(this.lastAccess)); + jsonObject.addProperty("lastAccess", ISO8601.toString(this.lastAccess)); JsonArray rolesJ = new JsonArray(); for (String role : this.userRoles) { diff --git a/li.strolch.service/src/test/java/li/strolch/service/test/ServiceTest.java b/li.strolch.service/src/test/java/li/strolch/service/test/ServiceTest.java index 657714c28..581db708b 100644 --- a/li.strolch.service/src/test/java/li/strolch/service/test/ServiceTest.java +++ b/li.strolch.service/src/test/java/li/strolch/service/test/ServiceTest.java @@ -18,7 +18,7 @@ package li.strolch.service.test; import static org.hamcrest.Matchers.*; import static org.junit.Assert.assertThat; -import java.util.Date; +import java.time.LocalDateTime; import java.util.HashSet; import li.strolch.privilege.base.AccessDeniedException; @@ -49,16 +49,15 @@ public class ServiceTest extends AbstractServiceTest { this.thrown.expect(PrivilegeException.class); TestService testService = new TestService(); getServiceHandler().doService( - new Certificate(null, null, null, null, null, null, null, null, new Date(), null, new HashSet<>(), - null), testService); + new Certificate(null, null, null, null, null, null, null, null, LocalDateTime.now(), false, null, + new HashSet<>(), null), testService); } @Test public void shouldFailInvalidCertificate2() { TestService testService = new TestService(); Certificate badCert = new Certificate(Usage.ANY, "1", "bob", "Bob", "Brown", UserState.ENABLED, "dsdf", "asd", - //$NON-NLS-1$//$NON-NLS-2$//$NON-NLS-3$ - new Date(), null, new HashSet<>(), null); + LocalDateTime.now(), false, null, new HashSet<>(), null); ServiceResult svcResult = getServiceHandler().doService(badCert, testService); assertThat(svcResult.getThrowable(), instanceOf(NotAuthenticatedException.class)); }