reintegration authentication patterns
diff --git a/logs/audit.log b/logs/audit.log
deleted file mode 100644
index 591c4a4..0000000
--- a/logs/audit.log
+++ /dev/null
@@ -1,48 +0,0 @@
-SERVICE AUDIT :
-MichaelHanl@Tue Aug 11 15:50:35 CEST 2015
-Status 105; Args None;
-SERVICE AUDIT :
-MichaelHanl@Tue Aug 11 15:50:35 CEST 2015
-Status 105; Args None;
-SERVICE AUDIT :
-MichaelHanl@Tue Aug 11 15:50:35 CEST 2015
-Status 105; Args None;
-SERVICE AUDIT :
-MichaelHanl@Tue Aug 11 15:50:35 CEST 2015
-Status 105; Args None;
-SERVICE AUDIT :
-MichaelHanl@Tue Aug 11 15:50:35 CEST 2015
-Status 105; Args None;
-SERVICE AUDIT :
-MichaelHanl@Tue Aug 11 15:50:35 CEST 2015
-Status 105; Args None;
-SERVICE AUDIT :
-MichaelHanl@Tue Aug 11 15:50:35 CEST 2015
-Status 105; Args None;
-SERVICE AUDIT :
-MichaelHanl@Tue Aug 11 15:50:35 CEST 2015
-Status 105; Args None;
-SERVICE AUDIT :
-MichaelHanl@Tue Aug 11 15:50:35 CEST 2015
-Status 105; Args None;
-SERVICE AUDIT :
-MichaelHanl@Tue Aug 11 15:50:35 CEST 2015
-Status 105; Args None;
-SERVICE AUDIT :
-MichaelHanl@Tue Aug 11 15:50:35 CEST 2015
-Status 105; Args None;
-SERVICE AUDIT :
-MichaelHanl@Tue Aug 11 15:50:35 CEST 2015
-Status 105; Args None;
-SERVICE AUDIT :
-MichaelHanl@Tue Aug 11 15:50:35 CEST 2015
-Status 105; Args None;
-SERVICE AUDIT :
-MichaelHanl@Tue Aug 11 15:50:35 CEST 2015
-Status 105; Args None;
-SERVICE AUDIT :
-MichaelHanl@Tue Aug 11 15:50:35 CEST 2015
-Status 105; Args None;
-SERVICE AUDIT :
-MichaelHanl@Tue Aug 11 15:50:35 CEST 2015
-Status 105; Args None;
diff --git a/logs/default_audit.log b/logs/default_audit.log
deleted file mode 100644
index e69de29..0000000
--- a/logs/default_audit.log
+++ /dev/null
diff --git a/src/main/java/de/ids_mannheim/korap/config/JWTSigner.java b/src/main/java/de/ids_mannheim/korap/config/JWTSigner.java
new file mode 100644
index 0000000..be9e9ce
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/config/JWTSigner.java
@@ -0,0 +1,170 @@
+package de.ids_mannheim.korap.config;
+
+import com.nimbusds.jose.*;
+import com.nimbusds.jose.crypto.MACSigner;
+import com.nimbusds.jose.crypto.MACVerifier;
+import com.nimbusds.jwt.JWTClaimsSet;
+import com.nimbusds.jwt.SignedJWT;
+import de.ids_mannheim.korap.exceptions.KustvaktException;
+import de.ids_mannheim.korap.exceptions.StatusCodes;
+import de.ids_mannheim.korap.user.Attributes;
+import de.ids_mannheim.korap.user.TokenContext;
+import de.ids_mannheim.korap.user.User;
+import de.ids_mannheim.korap.utils.TimeUtils;
+import org.joda.time.DateTime;
+
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.text.ParseException;
+import java.util.Map;
+
+/**
+ * @author hanl
+ * @date 19/05/2014
+ */
+public class JWTSigner {
+
+ private URL issuer;
+ private JWSSigner signer;
+ private JWSVerifier verifier;
+ private final int defaultttl;
+
+ public JWTSigner(final byte[] secret, URL issuer, final int defaultttl) {
+ this.issuer = issuer;
+ this.signer = new MACSigner(secret);
+ this.verifier = new MACVerifier(secret);
+ this.defaultttl = defaultttl;
+ }
+
+ public JWTSigner(final byte[] secret, String issuer)
+ throws MalformedURLException {
+ this(secret, new URL(issuer), 72 * 60 * 60);
+ }
+
+ public SignedJWT createJWT(User user, Map<String, Object> attr) {
+ return signContent(user, attr, defaultttl);
+ }
+
+ public SignedJWT signContent(User user, Map<String, Object> attr, int ttl) {
+ String scopes;
+
+ JWTClaimsSet cs = new JWTClaimsSet();
+ cs.setIssuerClaim(this.issuer.toString());
+
+ if ((scopes = (String) attr.get(Attributes.SCOPES)) != null) {
+ Map<String, Object> claims = Scopes
+ .mapOpenIDConnectScopes(scopes, user.getDetails());
+ cs.setCustomClaims(claims);
+ cs.setCustomClaim(Attributes.SCOPES,
+ ((String) attr.get(Attributes.SCOPES)).toLowerCase());
+ }
+
+ cs.setSubjectClaim(user.getUsername());
+ if (attr.get(Attributes.CLIENT_ID) != null)
+ cs.setAudienceClaim(
+ new String[] { (String) attr.get(Attributes.CLIENT_ID) });
+ cs.setExpirationTimeClaim(
+ TimeUtils.getNow().plusSeconds(ttl).getMillis());
+ SignedJWT signedJWT = new SignedJWT(new JWSHeader(JWSAlgorithm.HS256),
+ cs);
+ try {
+ signedJWT.sign(signer);
+ }catch (JOSEException e) {
+ return null;
+ }
+ return signedJWT;
+ }
+
+ /**
+ * @param username
+ * @param json
+ * @return
+ */
+ public SignedJWT signContent(String username, String userclient,
+ String json, int ttl) {
+ JWTClaimsSet cs = new JWTClaimsSet();
+ cs.setSubjectClaim(username);
+ if (!json.isEmpty())
+ cs.setCustomClaim("data", json);
+ cs.setExpirationTimeClaim(
+ TimeUtils.getNow().plusSeconds(ttl).getMillis());
+ cs.setIssuerClaim(this.issuer.toString());
+
+ if (!userclient.isEmpty())
+ cs.setCustomClaim("userip", userclient);
+
+ SignedJWT signedJWT = new SignedJWT(new JWSHeader(JWSAlgorithm.HS256),
+ cs);
+ try {
+ signedJWT.sign(signer);
+ }catch (JOSEException e) {
+ return null;
+ }
+ return signedJWT;
+ }
+
+ public SignedJWT signContent(String username, String userclient,
+ String json) {
+ return signContent(username, userclient, json, defaultttl);
+ }
+
+ public SignedJWT createSignedToken(String username) {
+ return createSignedToken(username, defaultttl);
+ }
+
+ // add client info
+ public SignedJWT createSignedToken(String username, int ttl) {
+ return signContent(username, "", "", ttl);
+ }
+
+ private SignedJWT verifyToken(String token) throws KustvaktException {
+ SignedJWT client;
+ try {
+ client = SignedJWT.parse(token);
+ if (!client.verify(verifier))
+ throw new KustvaktException(StatusCodes.REQUEST_INVALID);
+
+ if (!new DateTime(client.getJWTClaimsSet().getExpirationTimeClaim())
+ .isAfterNow())
+ throw new KustvaktException(StatusCodes.EXPIRED,
+ "authentication token is expired", token);
+ }catch (ParseException | JOSEException e) {
+ //todo: message or entity, how to treat??!
+ throw new KustvaktException(StatusCodes.ILLEGAL_ARGUMENT,
+ "token could not be verified", token);
+ }
+ return client;
+ }
+
+ // does not care about expiration times
+ public String retrieveContent(String signedContent) throws
+ KustvaktException {
+ SignedJWT jwt;
+ try {
+ jwt = SignedJWT.parse(signedContent);
+ if (!jwt.verify(verifier))
+ throw new KustvaktException(StatusCodes.REQUEST_INVALID,
+ "token invalid", signedContent);
+ return (String) jwt.getJWTClaimsSet().getCustomClaim("data");
+ }catch (ParseException | JOSEException e) {
+ return null;
+ }
+ }
+
+ public TokenContext getTokenContext(String idtoken)
+ throws ParseException, JOSEException, KustvaktException {
+ SignedJWT signedJWT = verifyToken(idtoken);
+
+ TokenContext c = new TokenContext(
+ signedJWT.getJWTClaimsSet().getSubjectClaim());
+ if (signedJWT.getJWTClaimsSet().getAudienceClaim() != null)
+ c.addContextParameter(Attributes.CLIENT_ID,
+ signedJWT.getJWTClaimsSet().getAudienceClaim()[0]);
+ c.setExpirationTime(
+ signedJWT.getJWTClaimsSet().getExpirationTimeClaim());
+ c.setToken(idtoken);
+ c.setParameters(signedJWT.getJWTClaimsSet().getCustomClaims());
+ return c;
+ }
+
+}
diff --git a/src/main/java/de/ids_mannheim/korap/config/URIParam.java b/src/main/java/de/ids_mannheim/korap/config/URIParam.java
new file mode 100644
index 0000000..3ab3724
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/config/URIParam.java
@@ -0,0 +1,26 @@
+package de.ids_mannheim.korap.config;
+
+import lombok.Getter;
+
+/**
+ * @author hanl
+ * @date 15/07/15
+ */
+@Getter
+public class URIParam extends ParamFields.Param {
+
+ private final String uriFragment;
+ private final Long uriExpiration;
+
+ public URIParam(String uri, Long expire) {
+ this.uriFragment = uri;
+ this.uriExpiration = expire;
+ }
+
+ @Override
+ public boolean hasValues() {
+ return this.uriFragment != null && !this.uriFragment.isEmpty()
+ && this.uriExpiration != null;
+ }
+
+}
diff --git a/src/main/java/de/ids_mannheim/korap/security/auth/APIAuthentication.java b/src/main/java/de/ids_mannheim/korap/security/auth/APIAuthentication.java
new file mode 100644
index 0000000..df9f1b8
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/security/auth/APIAuthentication.java
@@ -0,0 +1,86 @@
+package de.ids_mannheim.korap.security.auth;
+
+import com.nimbusds.jose.JOSEException;
+import com.nimbusds.jwt.SignedJWT;
+import de.ids_mannheim.korap.config.JWTSigner;
+import de.ids_mannheim.korap.config.KustvaktConfiguration;
+import de.ids_mannheim.korap.exceptions.KustvaktException;
+import de.ids_mannheim.korap.exceptions.StatusCodes;
+import de.ids_mannheim.korap.interfaces.AuthenticationIface;
+import de.ids_mannheim.korap.user.Attributes;
+import de.ids_mannheim.korap.user.TokenContext;
+import de.ids_mannheim.korap.user.User;
+import de.ids_mannheim.korap.utils.StringUtils;
+import lombok.AccessLevel;
+import net.sf.ehcache.CacheManager;
+import net.sf.ehcache.Element;
+import org.springframework.cache.annotation.CacheEvict;
+import org.springframework.cache.annotation.Cacheable;
+
+import java.text.ParseException;
+import java.util.Map;
+
+/**
+ * Created by hanl on 5/23/14.
+ */
+public class APIAuthentication implements AuthenticationIface {
+
+ private JWTSigner signedToken;
+
+ public APIAuthentication(KustvaktConfiguration bconfig) {
+ KustvaktConfiguration config = bconfig;
+ this.signedToken = new JWTSigner(config.getSharedSecret(),
+ config.getIssuer(), config.getTokenTTL());
+ }
+
+ @Cacheable(value = "id_tokens", key = "#authToken")
+ @Override
+ public TokenContext getUserStatus(String authToken)
+ throws KustvaktException {
+ try {
+ authToken = StringUtils.stripTokenType(authToken);
+ TokenContext c = signedToken.getTokenContext(authToken);
+ c.setTokenType(Attributes.API_AUTHENTICATION);
+ return c;
+ }catch (JOSEException | ParseException e) {
+ throw new KustvaktException(StatusCodes.ILLEGAL_ARGUMENT);
+ }
+ }
+
+ @Override
+ public TokenContext createUserSession(User user, Map<String, Object> attr)
+ throws KustvaktException {
+ TokenContext c = new TokenContext(user.getUsername());
+ SignedJWT jwt = signedToken.createJWT(user, attr);
+ try {
+ c.setExpirationTime(jwt.getJWTClaimsSet().getExpirationTimeClaim());
+ }catch (ParseException e) {
+ throw new KustvaktException(StatusCodes.ILLEGAL_ARGUMENT);
+ }
+ c.setTokenType(Attributes.API_AUTHENTICATION);
+ c.setToken(jwt.serialize());
+ CacheManager.getInstance().getCache("id_tokens")
+ .put(new Element(c.getToken(), c));
+
+ return c;
+ }
+
+ // todo: cache and set expiration to token expiration. if token in that cache, it is not to be used anymore!
+ @CacheEvict(value = "id_tokens", key = "#token")
+ @Override
+ public void removeUserSession(String token) throws KustvaktException {
+ // invalidate token!
+ }
+
+ @Override
+ public TokenContext refresh(TokenContext context) throws KustvaktException {
+ return null;
+ }
+
+
+ @Override
+ public String getIdentifier() {
+ return Attributes.API_AUTHENTICATION;
+ }
+
+}
diff --git a/src/main/java/de/ids_mannheim/korap/security/auth/BasicHttpAuth.java b/src/main/java/de/ids_mannheim/korap/security/auth/BasicHttpAuth.java
new file mode 100644
index 0000000..07e8ace
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/security/auth/BasicHttpAuth.java
@@ -0,0 +1,75 @@
+package de.ids_mannheim.korap.security.auth;
+
+import de.ids_mannheim.korap.exceptions.KustvaktException;
+import de.ids_mannheim.korap.interfaces.AuthenticationIface;
+import de.ids_mannheim.korap.user.Attributes;
+import de.ids_mannheim.korap.user.TokenContext;
+import de.ids_mannheim.korap.user.User;
+import de.ids_mannheim.korap.utils.StringUtils;
+import org.apache.commons.codec.binary.Base64;
+
+import java.util.Map;
+
+/**
+ * @author hanl
+ * @date 28/04/2015
+ */
+public class BasicHttpAuth implements AuthenticationIface {
+
+ public static String[] decode(String token) {
+ if (StringUtils.getTokenType(token)
+ .equals(Attributes.BASIC_AUTHENTICATION)) {
+ token = StringUtils.stripTokenType(token);
+ String[] sp = new String(Base64.decodeBase64(token)).split(":", 2);
+ sp[0].replaceAll(" ", "");
+ sp[1].replaceAll(" ", "");
+ return sp;
+ }
+ return null;
+ }
+
+ public static String encode(String user, String pass) {
+ String s = user + ":" + pass;
+ return new String(Base64.encodeBase64(s.getBytes()));
+ }
+
+ @Override
+ public TokenContext getUserStatus(String authToken) throws
+ KustvaktException {
+ authToken = StringUtils.stripTokenType(authToken);
+ String[] values = decode(authToken);
+ if (values != null) {
+ TokenContext c = new TokenContext(values[0]);
+ c.setTokenType(Attributes.BASIC_AUTHENTICATION);
+ c.setSecureRequired(true);
+ c.setToken(authToken);
+ // fixme: you can make queries, but user sensitive data is off limits?!
+ // c.addContextParameter(Attributes.SCOPES,
+ // Scopes.Scope.search.toString());
+ return c;
+ }
+ return null;
+ }
+
+ // not supported!
+ @Override
+ public TokenContext createUserSession(User user, Map<String, Object> attr)
+ throws KustvaktException {
+ return null;
+ }
+
+ @Override
+ public void removeUserSession(String token) throws KustvaktException {
+ }
+
+ @Override
+ public TokenContext refresh(TokenContext context) throws KustvaktException {
+ return null;
+ }
+
+
+ @Override
+ public String getIdentifier() {
+ return Attributes.BASIC_AUTHENTICATION;
+ }
+}
diff --git a/src/main/java/de/ids_mannheim/korap/security/auth/KustvaktAuthenticationManager.java b/src/main/java/de/ids_mannheim/korap/security/auth/KustvaktAuthenticationManager.java
new file mode 100644
index 0000000..a703e39
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/security/auth/KustvaktAuthenticationManager.java
@@ -0,0 +1,691 @@
+package de.ids_mannheim.korap.security.auth;
+
+import de.ids_mannheim.korap.auditing.AuditRecord;
+import de.ids_mannheim.korap.config.BeanConfiguration;
+import de.ids_mannheim.korap.config.KustvaktConfiguration;
+import de.ids_mannheim.korap.config.URIParam;
+import de.ids_mannheim.korap.exceptions.*;
+import de.ids_mannheim.korap.interfaces.*;
+import de.ids_mannheim.korap.user.*;
+import de.ids_mannheim.korap.utils.KustvaktLogger;
+import de.ids_mannheim.korap.utils.StringUtils;
+import de.ids_mannheim.korap.utils.TimeUtils;
+import net.sf.ehcache.Cache;
+import net.sf.ehcache.CacheManager;
+import net.sf.ehcache.Element;
+import org.joda.time.DateTime;
+import org.slf4j.Logger;
+import org.springframework.cache.annotation.CachePut;
+
+import java.io.UnsupportedEncodingException;
+import java.security.NoSuchAlgorithmException;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * contains the logic to authentication and registration processes. Uses
+ * interface implementations (AuthenticationIface) for different databases and handlers
+ *
+ * @author hanl
+ */
+public class KustvaktAuthenticationManager extends AuthenticationManagerIface {
+
+ private static String KEY = "kustvakt:key";
+ private static Logger jlog = KustvaktLogger
+ .initiate(KustvaktAuthenticationManager.class);
+ private EncryptionIface crypto;
+ private EntityHandlerIface entHandler;
+ private AuditingIface auditing;
+ private final LoginCounter counter;
+ private Cache user_cache;
+
+ public KustvaktAuthenticationManager(EntityHandlerIface userdb,
+ EncryptionIface crypto, KustvaktConfiguration config,
+ AuditingIface auditer) {
+ this.entHandler = userdb;
+ this.crypto = crypto;
+ this.auditing = auditer;
+ this.counter = new LoginCounter(config);
+ this.user_cache = CacheManager.getInstance().getCache("users");
+ }
+
+ /**
+ * get session object if token was a session token
+ *
+ * @param token
+ * @param host
+ * @param useragent
+ * @return
+ * @throws KustvaktException
+ */
+ public TokenContext getTokenStatus(String token, String host,
+ String useragent) throws KustvaktException {
+ jlog.info("getting session status of token '{}'", token);
+ AuthenticationIface provider = getProvider(
+ StringUtils.getTokenType(token));
+ TokenContext context = provider.getUserStatus(token);
+ if (!matchStatus(host, useragent, context))
+ provider.removeUserSession(token);
+ return context;
+ }
+
+ public User getUser(String username) throws KustvaktException {
+ User user;
+ String key = cache_key(username);
+ Element e = user_cache.get(key);
+
+ if (e != null) {
+ Map map = (Map) e.getObjectValue();
+ user = User.UserFactory.toUser(map);
+ }else {
+ try {
+ user = entHandler.getAccount(username);
+ user_cache.put(new Element(key, user.toCache()));
+ // todo: not valid. for the duration of the session, the host should not change!
+ }catch (EmptyResultException e1) {
+ // do nothing
+ return null;
+ }
+ }
+ //todo:
+ // user.addField(Attributes.HOST, context.getHostAddress());
+ // user.addField(Attributes.USER_AGENT, context.getUserAgent());
+ return user;
+ }
+
+ public TokenContext refresh(TokenContext context) throws KustvaktException {
+ AuthenticationIface provider = getProvider(context.getTokenType());
+ try {
+ provider.removeUserSession(context.getToken());
+ User user = getUser(context.getUsername());
+ return provider.createUserSession(user, context.getParameters());
+ }catch (KustvaktException e) {
+ throw new WrappedException(e, StatusCodes.LOGIN_FAILED);
+ }
+ }
+
+ /**
+ * @param type
+ * @param attributes contains username and password to authenticate the user.
+ * Depending of the authentication schema, may contain other values as well
+ * @return User
+ * @throws KustvaktException
+ */
+
+ public User authenticate(int type, String username, String password,
+ Map<String, Object> attributes) throws KustvaktException {
+ User user;
+ switch (type) {
+ case 1:
+ // todo:
+ user = authenticateShib(attributes);
+ break;
+ default:
+ user = authenticate(username, password, attributes);
+ break;
+ }
+ auditing.audit(AuditRecord
+ .serviceRecord(user.getId(), StatusCodes.LOGIN_SUCCESSFUL,
+ user.toString()));
+ return user;
+ }
+
+ @CachePut(value = "users", key = "#user.getUsername()")
+ public TokenContext createTokenContext(User user, Map<String, Object> attr,
+ String provider_key) throws KustvaktException {
+ AuthenticationIface provider = getProvider(provider_key);
+
+ if (attr.get(Attributes.SCOPES) != null)
+ this.getUserDetails(user);
+
+ TokenContext context = provider.createUserSession(user, attr);
+ if (context == null)
+ throw new KustvaktException(StatusCodes.NOT_SUPPORTED);
+ context.setUserAgent((String) attr.get(Attributes.USER_AGENT));
+ context.setHostAddress(Attributes.HOST);
+ return context;
+ }
+
+ //todo: test
+ private boolean matchStatus(String host, String useragent,
+ TokenContext context) {
+ if (host.equals(context.getHostAddress())) {
+ if (useragent.equals(context.getUserAgent()))
+ return true;
+ }
+ return false;
+ }
+
+ private User authenticateShib(Map<String, Object> attributes)
+ throws KustvaktException {
+ // todo use persistent id, since eppn is not unique
+ String eppn = (String) attributes.get(Attributes.EPPN);
+
+ if (eppn == null || eppn.isEmpty())
+ throw new KustvaktException(StatusCodes.REQUEST_INVALID);
+
+ if (!attributes.containsKey(Attributes.EMAIL)
+ && crypto.validateEmail(eppn) != null)
+ attributes.put(Attributes.EMAIL, eppn);
+
+ // fixme?!
+ User user = isRegistered(eppn);
+ if (user == null)
+ user = createShibbUserAccount(attributes);
+ return user;
+ }
+
+ //todo: what if attributes null?
+ private User authenticate(String username, String password,
+ Map<String, Object> attr) throws KustvaktException {
+ Map<String, Object> attributes = crypto.validateMap(attr);
+ String uPassword, safeUS;
+ User unknown;
+ // just to make sure that the plain password does not appear anywhere in the logs!
+
+ try {
+ safeUS = crypto.validateString(username);
+ }catch (KustvaktException e) {
+ throw new WrappedException(e, StatusCodes.LOGIN_FAILED, username);
+ }
+
+ if (safeUS == null || safeUS.isEmpty())
+ throw new WrappedException(new KustvaktException(username,
+ StatusCodes.BAD_CREDENTIALS), StatusCodes.LOGIN_FAILED);
+ else {
+ try {
+ unknown = entHandler.getAccount(safeUS);
+ unknown.setSettings(
+ entHandler.getUserSettings(unknown.getId()));
+ }catch (EmptyResultException e) {
+ // mask exception to disable user guessing in possible attacks
+ throw new WrappedException(new KustvaktException(username,
+ StatusCodes.BAD_CREDENTIALS), StatusCodes.LOGIN_FAILED,
+ username);
+ }catch (KustvaktException e) {
+ throw new WrappedException(e, StatusCodes.LOGIN_FAILED,
+ attributes.toString());
+ }
+ }
+ jlog.trace("Authentication: found user under name " + unknown
+ .getUsername());
+ if (unknown instanceof KorAPUser) {
+ if (password == null || password.isEmpty())
+ throw new WrappedException(
+ new KustvaktException(unknown.getId(),
+ StatusCodes.BAD_CREDENTIALS),
+ StatusCodes.LOGIN_FAILED, username);
+
+ KorAPUser user = (KorAPUser) unknown;
+ boolean check = crypto.checkHash(password, user.getPassword());
+
+ if (!check) {
+ // the fail counter only applies for wrong password
+ jlog.warn("Wrong Password!");
+ processLoginFail(unknown);
+ throw new WrappedException(new KustvaktException(user.getId(),
+ StatusCodes.BAD_CREDENTIALS), StatusCodes.LOGIN_FAILED,
+ username);
+ }
+
+ // bad credentials error has presedence over account locked or unconfirmed codes
+ // since latter can lead to account guessing of third parties
+ if (user.isAccountLocked()) {
+ URIParam param = (URIParam) user.getField(URIParam.class);
+
+ if (param.hasValues()) {
+ jlog.debug("Account is not yet activated for user '{}'",
+ user.getUsername());
+ if (TimeUtils.getNow().isAfter(param.getUriExpiration())) {
+ KustvaktLogger.ERROR_LOGGER
+ .error("URI token is expired. Deleting account for user {}",
+ user.getUsername());
+ deleteAccount(user);
+ throw new WrappedException(
+ new KustvaktException(unknown.getId(),
+ StatusCodes.EXPIRED,
+ "account confirmation uri has expired",
+ param.getUriFragment()),
+ StatusCodes.LOGIN_FAILED, username);
+ }
+ throw new WrappedException(
+ new KustvaktException(unknown.getId(),
+ StatusCodes.UNCONFIRMED_ACCOUNT),
+ StatusCodes.LOGIN_FAILED, username);
+ }
+ KustvaktLogger.ERROR_LOGGER
+ .error("ACCESS DENIED: account not active for '{}'",
+ unknown.getUsername());
+ throw new WrappedException(
+ new KustvaktException(unknown.getId(),
+ StatusCodes.ACCOUNT_DEACTIVATED),
+ StatusCodes.LOGIN_FAILED, username);
+ }
+
+ }else if (unknown instanceof ShibUser) {
+ //todo
+ }
+ jlog.debug("Authentication done: " + safeUS);
+ return unknown;
+ }
+
+ public User isRegistered(String username) throws KustvaktException {
+ User user;
+ if (username == null || username.isEmpty())
+ throw new KustvaktException(username, StatusCodes.ILLEGAL_ARGUMENT,
+ "username must be set", username);
+
+ try {
+ user = entHandler.getAccount(username);
+ }catch (EmptyResultException e) {
+ jlog.debug("user does not exist ({})", username);
+ return null;
+
+ }catch (KustvaktException e) {
+ KustvaktLogger.ERROR_LOGGER.error("KorAPException", e);
+ throw new KustvaktException(username, StatusCodes.ILLEGAL_ARGUMENT,
+ "username invalid", username);
+ }
+ return user;
+ }
+
+ public void logout(TokenContext context) throws KustvaktException {
+ String key = cache_key(context.getUsername());
+ try {
+ AuthenticationIface provider = getProvider(context.getTokenType());
+ provider.removeUserSession(context.getToken());
+ }catch (KustvaktException e) {
+ throw new WrappedException(e, StatusCodes.LOGOUT_FAILED,
+ context.toString());
+ }
+ auditing.audit(AuditRecord.serviceRecord(context.getUsername(),
+ StatusCodes.LOGOUT_SUCCESSFUL, context.toString()));
+ user_cache.remove(key);
+ }
+
+ private void processLoginFail(User user) throws KustvaktException {
+ counter.registerFail(user.getUsername());
+ if (!counter.validate(user.getUsername())) {
+ try {
+ this.lockAccount(user);
+ }catch (KustvaktException e) {
+ KustvaktLogger.ERROR_LOGGER
+ .error("user account could not be locked!", e);
+ throw new WrappedException(e,
+ StatusCodes.UPDATE_ACCOUNT_FAILED);
+ }
+ throw new WrappedException(new KustvaktException(user.getId(),
+ StatusCodes.ACCOUNT_DEACTIVATED), StatusCodes.LOGIN_FAILED);
+ }
+ }
+
+ public void lockAccount(User user) throws KustvaktException {
+ if (!(user instanceof KorAPUser))
+ throw new KustvaktException(StatusCodes.REQUEST_INVALID);
+
+ KorAPUser u = (KorAPUser) user;
+ u.setAccountLocked(true);
+ jlog.info("locking account for user: {}", user.getUsername());
+ entHandler.updateAccount(u);
+ }
+
+ public KorAPUser checkPasswordAllowance(KorAPUser user, String oldPassword,
+ String newPassword) throws KustvaktException {
+ String dbPassword = user.getPassword();
+
+ if (oldPassword.trim().equals(newPassword.trim())) {
+ // TODO: special error StatusCodes for this?
+ throw new WrappedException(new KustvaktException(user.getId(),
+ StatusCodes.ILLEGAL_ARGUMENT),
+ StatusCodes.PASSWORD_RESET_FAILED, newPassword);
+ }
+
+ boolean check = crypto.checkHash(oldPassword, dbPassword);
+
+ if (!check)
+ throw new WrappedException(new KustvaktException(user.getId(),
+ StatusCodes.BAD_CREDENTIALS),
+ StatusCodes.PASSWORD_RESET_FAILED);
+
+ try {
+ user.setPassword(crypto.produceSecureHash(newPassword));
+ }catch (NoSuchAlgorithmException | UnsupportedEncodingException e) {
+ // throw new KorAPException(StatusCodes.ILLEGAL_ARGUMENT,
+ // "Creating password hash failed!", "password");
+ throw new WrappedException(new KustvaktException(user.getId(),
+ StatusCodes.ILLEGAL_ARGUMENT, "password invalid",
+ newPassword), StatusCodes.PASSWORD_RESET_FAILED,
+ user.toString(), newPassword);
+ }
+ return user;
+ }
+
+ //fixme: use clientinfo for logging/auditing?! = from where did he access the reset function?
+ @Override
+ public void resetPassword(String uriFragment, String username,
+ String newPassphrase) throws KustvaktException {
+ String safeUser, safePass;
+
+ try {
+ safeUser = crypto.validateString(username);
+ safePass = crypto.validatePassphrase(newPassphrase);
+ }catch (KustvaktException e) {
+ KustvaktLogger.ERROR_LOGGER.error("Error", e);
+ throw new WrappedException(new KustvaktException(username,
+ StatusCodes.ILLEGAL_ARGUMENT, "password invalid",
+ newPassphrase), StatusCodes.PASSWORD_RESET_FAILED, username,
+ newPassphrase);
+ }
+
+ try {
+ safePass = crypto.produceSecureHash(safePass);
+ }catch (NoSuchAlgorithmException | UnsupportedEncodingException e) {
+ KustvaktLogger.ERROR_LOGGER.error("Encoding/Algorithm Error", e);
+ throw new WrappedException(new KustvaktException(username,
+ StatusCodes.ILLEGAL_ARGUMENT, "password invalid",
+ newPassphrase), StatusCodes.PASSWORD_RESET_FAILED, username,
+ uriFragment, newPassphrase);
+ }
+ int result = entHandler
+ .resetPassphrase(safeUser, uriFragment, safePass);
+
+ if (result == 0)
+ throw new WrappedException(
+ new KustvaktException(username, StatusCodes.EXPIRED,
+ "URI fragment expired", uriFragment),
+ StatusCodes.PASSWORD_RESET_FAILED, username, uriFragment);
+ else if (result == 1)
+ jlog.info("successfully reset password for user {}", safeUser);
+ }
+
+ public void confirmRegistration(String uriFragment, String username)
+ throws KustvaktException {
+ String safeUser;
+ try {
+ safeUser = crypto.validateString(username);
+ }catch (KustvaktException e) {
+ KustvaktLogger.ERROR_LOGGER.error("error", e);
+ throw new WrappedException(e,
+ StatusCodes.ACCOUNT_CONFIRMATION_FAILED, username,
+ uriFragment);
+ }
+ int r = entHandler.activateAccount(safeUser, uriFragment);
+ if (r == 0) {
+ User user;
+ try {
+ user = entHandler.getAccount(username);
+ }catch (EmptyResultException e) {
+ throw new WrappedException(new KustvaktException(username,
+ StatusCodes.BAD_CREDENTIALS),
+ StatusCodes.ACCOUNT_CONFIRMATION_FAILED, username,
+ uriFragment);
+ }
+ entHandler.deleteAccount(user.getId());
+ throw new WrappedException(
+ new KustvaktException(user.getId(), StatusCodes.EXPIRED),
+ StatusCodes.ACCOUNT_CONFIRMATION_FAILED, username,
+ uriFragment);
+ }else if (r == 1)
+ jlog.info("successfully confirmed user registration for user {}",
+ safeUser);
+ // register successful audit!
+ }
+
+ /**
+ * @param attributes
+ * @return
+ * @throws KustvaktException
+ */
+ //fixme: remove clientinfo object (not needed), use json representation to get stuff
+ public User createUserAccount(Map<String, Object> attributes)
+ throws KustvaktException {
+ Map<String, Object> safeMap = crypto.validateMap(attributes);
+ if (safeMap.get(Attributes.USERNAME) == null || ((String) safeMap
+ .get(Attributes.USERNAME)).isEmpty())
+ throw new KustvaktException(StatusCodes.ILLEGAL_ARGUMENT,
+ "username must be set", "username");
+ if (safeMap.get(Attributes.PASSWORD) == null || ((String) safeMap
+ .get(Attributes.PASSWORD)).isEmpty())
+ throw new KustvaktException(safeMap.get(Attributes.USERNAME),
+ StatusCodes.ILLEGAL_ARGUMENT, "password must be set",
+ "password");
+
+ String safePass = crypto
+ .validatePassphrase((String) safeMap.get(Attributes.PASSWORD));
+ String hash;
+ try {
+ hash = crypto.produceSecureHash(safePass);
+ }catch (UnsupportedEncodingException | NoSuchAlgorithmException e) {
+ KustvaktLogger.ERROR_LOGGER.error("Encryption error", e);
+ throw new KustvaktException(StatusCodes.ILLEGAL_ARGUMENT);
+ }
+
+ KorAPUser user = User.UserFactory
+ .getUser((String) safeMap.get(Attributes.USERNAME));
+ UserDetails det = UserDetails.newDetailsIterator(safeMap);
+ user.setDetails(det);
+ user.setSettings(new UserSettings());
+ user.setAccountLocked(true);
+ URIParam param = new URIParam(crypto.createToken(), TimeUtils
+ .plusSeconds(BeanConfiguration.getBeans().getConfiguration()
+ .getShortTokenTTL()).getMillis());
+ user.addField(param);
+ user.setPassword(hash);
+ try {
+ entHandler.createAccount(user);
+ }catch (KustvaktException e) {
+ throw new WrappedException(e, StatusCodes.CREATE_ACCOUNT_FAILED,
+ user.toString());
+ }
+
+ auditing.audit(AuditRecord.serviceRecord(user.getUsername(),
+ StatusCodes.CREATE_ACCOUNT_SUCCESSFUL));
+ return user;
+ }
+
+ //todo:
+ private ShibUser createShibbUserAccount(Map<String, Object> attributes)
+ throws KustvaktException {
+ jlog.debug("creating shibboleth user account for user attr: {}",
+ attributes);
+ Map<String, Object> safeMap = crypto.validateMap(attributes);
+
+ //todo eppn non-unique.join with idp or use persistent_id as username identifier
+ ShibUser user = User.UserFactory
+ .getShibInstance((String) safeMap.get(Attributes.EPPN),
+ (String) safeMap.get(Attributes.MAIL),
+ (String) safeMap.get(Attributes.CN));
+ user.setAffiliation((String) safeMap.get(Attributes.EDU_AFFIL));
+ UserDetails det = UserDetails
+ .newDetailsIterator(new HashMap<String, Object>());
+ user.setDetails(det);
+ user.setSettings(new UserSettings());
+ user.setAccountCreation(TimeUtils.getNow().getMillis());
+ entHandler.createAccount(user);
+ return user;
+ }
+
+ /**
+ * link shibboleth and korap user account to one another.
+ *
+ * @param current currently logged in user
+ * @param for_name foreign user name the current account should be linked to
+ * @param transstrat transfer status of user data (details, settings, user queries)
+ * 0 = the currently logged in data should be kept
+ * 1 = the foreign account data should be kept
+ * @throws NotAuthorizedException
+ * @throws KustvaktException
+ */
+ // todo:
+ public void accountLink(User current, String for_name, int transstrat)
+ throws KustvaktException {
+ // User foreign = entHandler.getAccount(for_name);
+
+ // if (current.getAccountLink() == null && current.getAccountLink()
+ // .isEmpty()) {
+ // if (current instanceof KorAPUser && foreign instanceof ShibUser) {
+ // if (transstrat == 1)
+ // current.transfer(foreign);
+ //// foreign.setAccountLink(current.getUsername());
+ //// current.setAccountLink(foreign.getUsername());
+ // // entHandler.purgeDetails(foreign);
+ // // entHandler.purgeSettings(foreign);
+ // }else if (foreign instanceof KorAPUser
+ // && current instanceof ShibUser) {
+ // if (transstrat == 0)
+ // foreign.transfer(current);
+ //// current.setAccountLink(foreign.getUsername());
+ // // entHandler.purgeDetails(current);
+ // // entHandler.purgeSettings(current);
+ // // entHandler.purgeSettings(current);
+ // }
+ // entHandler.updateAccount(current);
+ // entHandler.updateAccount(foreign);
+ // }
+ }
+
+ @Override
+ public boolean updateAccount(User user) throws KustvaktException {
+ boolean result;
+ String key = cache_key(user.getUsername());
+ if (user instanceof DemoUser)
+ throw new KustvaktException(user.getId(),
+ StatusCodes.REQUEST_INVALID,
+ "account not updateable for demo user", user.getUsername());
+ else {
+ crypto.validate(user);
+ try {
+ result = entHandler.updateAccount(user) > 0;
+ }catch (KustvaktException e) {
+ KustvaktLogger.ERROR_LOGGER.error("Error ", e);
+ throw new WrappedException(e,
+ StatusCodes.UPDATE_ACCOUNT_FAILED);
+ }
+ }
+ if (result) {
+ user_cache.remove(key);
+ auditing.audit(AuditRecord.serviceRecord(user.getId(),
+ StatusCodes.UPDATE_ACCOUNT_SUCCESSFUL, user.toString()));
+ }
+ return result;
+ }
+
+ public boolean deleteAccount(User user) throws KustvaktException {
+ boolean result;
+ String key = cache_key(user.getUsername());
+ if (user instanceof DemoUser)
+ return true;
+ else {
+ try {
+ result = entHandler.deleteAccount(user.getId()) > 0;
+ }catch (KustvaktException e) {
+ KustvaktLogger.ERROR_LOGGER.error("Error ", e);
+ throw new WrappedException(e,
+ StatusCodes.DELETE_ACCOUNT_FAILED);
+ }
+ }
+ if (result) {
+ user_cache.remove(key);
+ auditing.audit(AuditRecord.serviceRecord(user.getUsername(),
+ StatusCodes.DELETE_ACCOUNT_SUCCESSFUL, user.toString()));
+ }
+ return result;
+ }
+
+ public Object[] validateResetPasswordRequest(String username, String email)
+ throws KustvaktException {
+ String mail, uritoken;
+ mail = crypto.validateEmail(email);
+ User ident;
+ try {
+ ident = entHandler.getAccount(username);
+ if (ident instanceof DemoUser)
+ // throw new NotAuthorizedException(StatusCodes.PERMISSION_DENIED,
+ // "password reset now allowed for DemoUser", "");
+ throw new WrappedException(username,
+ StatusCodes.PASSWORD_RESET_FAILED, username);
+ }catch (EmptyResultException e) {
+ throw new WrappedException(new KustvaktException(username,
+ StatusCodes.ILLEGAL_ARGUMENT, "username not found",
+ username), StatusCodes.PASSWORD_RESET_FAILED, username);
+ }
+
+ getUserDetails(ident);
+ KorAPUser user = (KorAPUser) ident;
+ if (!mail.equals(user.getDetails().getEmail()))
+ // throw new NotAuthorizedException(StatusCodes.ILLEGAL_ARGUMENT,
+ // "invalid parameter: email", "email");
+ throw new WrappedException(new KustvaktException(user.getId(),
+ StatusCodes.ILLEGAL_ARGUMENT, "email invalid", email),
+ StatusCodes.PASSWORD_RESET_FAILED, email);
+ uritoken = crypto.encodeBase();
+ URIParam param = new URIParam(uritoken,
+ TimeUtils.plusHours(24).getMillis());
+ user.addField(param);
+
+ try {
+ entHandler.updateAccount(user);
+ }catch (KustvaktException e) {
+ KustvaktLogger.ERROR_LOGGER.error("Error ", e);
+ throw new WrappedException(e, StatusCodes.PASSWORD_RESET_FAILED);
+ }
+ return new Object[] { uritoken,
+ new DateTime(param.getUriExpiration()) };
+ }
+
+ public void updateUserSettings(User user, UserSettings settings)
+ throws KustvaktException {
+ if (user instanceof DemoUser)
+ return;
+ else {
+ crypto.validate(settings);
+ try {
+ entHandler.updateSettings(settings);
+ }catch (KustvaktException e) {
+ KustvaktLogger.ERROR_LOGGER.error("Error ", e);
+ throw new WrappedException(e,
+ StatusCodes.UPDATE_ACCOUNT_FAILED);
+ }
+ }
+ }
+
+ public void updateUserDetails(User user, UserDetails details)
+ throws KustvaktException {
+ if (user instanceof DemoUser)
+ return;
+ else {
+ crypto.validate(details);
+ try {
+ entHandler.updateUserDetails(details);
+ }catch (KustvaktException e) {
+ KustvaktLogger.ERROR_LOGGER.error("Error ", e);
+ throw new WrappedException(e,
+ StatusCodes.UPDATE_ACCOUNT_FAILED);
+ }
+ }
+ }
+
+ public UserDetails getUserDetails(User user) throws KustvaktException {
+ try {
+ if (user.getDetails() == null)
+ user.setDetails(entHandler.getUserDetails(user.getId()));
+ }catch (KustvaktException e) {
+ throw new WrappedException(e, StatusCodes.GET_ACCOUNT_FAILED);
+ }
+ return user.getDetails();
+ }
+
+ public UserSettings getUserSettings(User user) throws KustvaktException {
+ try {
+ if (user.getSettings() == null)
+ user.setSettings(entHandler.getUserSettings(user.getId()));
+ }catch (KustvaktException e) {
+ throw new WrappedException(e, StatusCodes.GET_ACCOUNT_FAILED);
+ }
+ return user.getSettings();
+ }
+
+ private String cache_key(String input) {
+ return crypto.hash(KEY + "@" + input);
+ }
+}
diff --git a/src/main/java/de/ids_mannheim/korap/security/auth/LoginCounter.java b/src/main/java/de/ids_mannheim/korap/security/auth/LoginCounter.java
new file mode 100644
index 0000000..c38bc02
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/security/auth/LoginCounter.java
@@ -0,0 +1,62 @@
+package de.ids_mannheim.korap.security.auth;
+
+import de.ids_mannheim.korap.config.KustvaktConfiguration;
+import de.ids_mannheim.korap.utils.KustvaktLogger;
+import de.ids_mannheim.korap.utils.TimeUtils;
+import org.slf4j.Logger;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * @author hanl
+ * @date 11/11/2014
+ */
+public class LoginCounter {
+
+ private static Logger jlog = KustvaktLogger.initiate(LoginCounter.class);
+ private final Map<String, Long[]> failedLogins;
+ private KustvaktConfiguration config;
+
+ public LoginCounter(KustvaktConfiguration config) {
+ jlog.debug("init login counter for authentication management");
+ this.config = config;
+ this.failedLogins = new HashMap<>();
+ }
+
+ public void resetFailedCounter(String username) {
+ failedLogins.remove(username);
+ }
+
+ public void registerFail(String username) {
+ long expires = TimeUtils.plusSeconds(config.getLoginAttemptTTL())
+ .getMillis();
+ long fail = 1;
+ Long[] set = failedLogins.get(username);
+ if (set != null)
+ fail = set[0] + 1;
+ else
+ set = new Long[2];
+ set[0] = fail;
+ set[1] = expires;
+
+ failedLogins.put(username, set);
+ jlog.warn("user failed to login ({}) ",
+ Arrays.asList(failedLogins.get(username)));
+ }
+
+ public boolean validate(String username) {
+ Long[] set = failedLogins.get(username);
+ if (set != null) {
+ if (TimeUtils.isPassed(set[1])) {
+ failedLogins.remove(username);
+ return true;
+ }else if (set[0] < config.getLoginAttemptNum())
+ return true;
+ return false;
+ }
+ return true;
+ }
+
+}
diff --git a/src/main/java/de/ids_mannheim/korap/security/auth/OpenIDconnectAuthentication.java b/src/main/java/de/ids_mannheim/korap/security/auth/OpenIDconnectAuthentication.java
new file mode 100644
index 0000000..cae5e2d
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/security/auth/OpenIDconnectAuthentication.java
@@ -0,0 +1,81 @@
+package de.ids_mannheim.korap.security.auth;
+
+import com.nimbusds.jwt.SignedJWT;
+import de.ids_mannheim.korap.config.JWTSigner;
+import de.ids_mannheim.korap.config.KustvaktConfiguration;
+import de.ids_mannheim.korap.exceptions.KustvaktException;
+import de.ids_mannheim.korap.exceptions.StatusCodes;
+import de.ids_mannheim.korap.handlers.OAuthDb;
+import de.ids_mannheim.korap.interfaces.AuthenticationIface;
+import de.ids_mannheim.korap.interfaces.PersistenceClient;
+import de.ids_mannheim.korap.user.Attributes;
+import de.ids_mannheim.korap.user.TokenContext;
+import de.ids_mannheim.korap.user.User;
+import de.ids_mannheim.korap.utils.StringUtils;
+import net.sf.ehcache.CacheManager;
+import net.sf.ehcache.Element;
+import org.springframework.cache.annotation.CacheEvict;
+import org.springframework.cache.annotation.Cacheable;
+
+import java.text.ParseException;
+import java.util.Map;
+
+/**
+ * @author hanl
+ * @date 12/11/2014
+ */
+public class OpenIDconnectAuthentication implements AuthenticationIface {
+
+ private OAuthDb database;
+ private KustvaktConfiguration config;
+
+ public OpenIDconnectAuthentication(KustvaktConfiguration config,
+ PersistenceClient client) {
+ this.database = new OAuthDb(client);
+ this.config = config;
+ }
+
+ @Cacheable(value = "id_tokens", key = "#authToken")
+ @Override
+ public TokenContext getUserStatus(String authToken)
+ throws KustvaktException {
+ authToken = StringUtils.stripTokenType(authToken);
+ return this.database.getContext(authToken);
+ }
+
+ @Override
+ public TokenContext createUserSession(User user, Map<String, Object> attr)
+ throws KustvaktException {
+ JWTSigner signer = new JWTSigner(
+ ((String) attr.get(Attributes.CLIENT_SECRET)).getBytes(),
+ config.getIssuer(), config.getTokenTTL());
+ TokenContext c = new TokenContext(user.getUsername());
+ SignedJWT jwt = signer.createJWT(user, attr);
+ try {
+ c.setExpirationTime(jwt.getJWTClaimsSet().getExpirationTimeClaim());
+ }catch (ParseException e) {
+ throw new KustvaktException(StatusCodes.ILLEGAL_ARGUMENT);
+ }
+ c.setTokenType(Attributes.OPENID_AUTHENTICATION);
+ c.setToken(jwt.serialize());
+ CacheManager.getInstance().getCache("id_tokens")
+ .put(new Element(c.getToken(), c));
+ return c;
+ }
+
+ @CacheEvict(value = "id_tokens", key = "#token")
+ @Override
+ public void removeUserSession(String token) throws KustvaktException {
+ // emit token from cache only
+ }
+
+ @Override
+ public TokenContext refresh(TokenContext context) throws KustvaktException {
+ throw new UnsupportedOperationException("method not supported");
+ }
+
+ @Override
+ public String getIdentifier() {
+ return Attributes.OPENID_AUTHENTICATION;
+ }
+}
diff --git a/src/main/java/de/ids_mannheim/korap/security/auth/SessionAuthentication.java b/src/main/java/de/ids_mannheim/korap/security/auth/SessionAuthentication.java
new file mode 100644
index 0000000..99ebd89
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/security/auth/SessionAuthentication.java
@@ -0,0 +1,93 @@
+package de.ids_mannheim.korap.security.auth;
+
+import de.ids_mannheim.korap.config.KustvaktConfiguration;
+import de.ids_mannheim.korap.exceptions.KustvaktException;
+import de.ids_mannheim.korap.exceptions.StatusCodes;
+import de.ids_mannheim.korap.interfaces.AuthenticationIface;
+import de.ids_mannheim.korap.interfaces.EncryptionIface;
+import de.ids_mannheim.korap.user.Attributes;
+import de.ids_mannheim.korap.user.TokenContext;
+import de.ids_mannheim.korap.user.User;
+import de.ids_mannheim.korap.utils.KustvaktLogger;
+import de.ids_mannheim.korap.utils.TimeUtils;
+import org.joda.time.DateTime;
+import org.slf4j.Logger;
+
+import java.util.Map;
+import java.util.concurrent.ScheduledThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * implementation of the AuthenticationIface to handle korap authentication
+ * internals
+ *
+ * @author hanl
+ */
+public class SessionAuthentication implements AuthenticationIface {
+
+ private static Logger jlog = KustvaktLogger
+ .initiate(SessionAuthentication.class);
+ private SessionFactory sessions;
+ private ScheduledThreadPoolExecutor scheduled;
+ private EncryptionIface crypto;
+ private KustvaktConfiguration config;
+
+ public SessionAuthentication(KustvaktConfiguration config,
+ EncryptionIface crypto) {
+ jlog.info("initialize session authentication handler");
+ this.crypto = crypto;
+ this.config = config;
+ this.scheduled = new ScheduledThreadPoolExecutor(1);
+ this.sessions = new SessionFactory(this.config.isAllowMultiLogIn(),
+ this.config.getInactiveTime());
+ this.scheduled.scheduleAtFixedRate(this.sessions,
+ this.config.getInactiveTime() / 2,
+ this.config.getInactiveTime(), TimeUnit.SECONDS);
+ }
+
+ @Override
+ public TokenContext getUserStatus(String authenticationToken)
+ throws KustvaktException {
+ jlog.debug("retrieving user session for user '{}'",
+ authenticationToken);
+ if (authenticationToken == null)
+ throw new KustvaktException(StatusCodes.PERMISSION_DENIED);
+ return this.sessions.getSession(authenticationToken);
+ }
+
+ @Override
+ public TokenContext createUserSession(User user, Map attr)
+ throws KustvaktException {
+ DateTime now = TimeUtils.getNow();
+ DateTime ex = TimeUtils
+ .getExpiration(now.getMillis(), config.getExpiration());
+ String token = crypto
+ .createToken(true, user.getUsername(), now.getMillis());
+ TokenContext ctx = new TokenContext(user.getUsername());
+ ctx.setUsername(user.getUsername());
+ ctx.setTokenType(Attributes.SESSION_AUTHENTICATION);
+ ctx.setToken(token);
+ ctx.setExpirationTime(ex.getMillis());
+ ctx.setHostAddress(attr.get(Attributes.HOST).toString());
+ ctx.setUserAgent(attr.get(Attributes.USER_AGENT).toString());
+ this.sessions.putSession(token, ctx);
+ jlog.info("create session for user: " + user.getUsername());
+ return ctx;
+ }
+
+ @Override
+ public void removeUserSession(String token) {
+ this.sessions.removeSession(token);
+ }
+
+ @Override
+ public TokenContext refresh(TokenContext context) throws KustvaktException {
+ throw new UnsupportedOperationException("method not supported");
+ }
+
+ @Override
+ public String getIdentifier() {
+ return Attributes.SESSION_AUTHENTICATION;
+ }
+
+}
diff --git a/src/main/java/de/ids_mannheim/korap/security/auth/SessionFactory.java b/src/main/java/de/ids_mannheim/korap/security/auth/SessionFactory.java
new file mode 100644
index 0000000..26e316a
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/security/auth/SessionFactory.java
@@ -0,0 +1,171 @@
+package de.ids_mannheim.korap.security.auth;
+
+import de.ids_mannheim.korap.exceptions.KustvaktException;
+import de.ids_mannheim.korap.exceptions.StatusCodes;
+import de.ids_mannheim.korap.user.DemoUser;
+import de.ids_mannheim.korap.user.TokenContext;
+import de.ids_mannheim.korap.utils.ConcurrentMultiMap;
+import de.ids_mannheim.korap.utils.KustvaktLogger;
+import de.ids_mannheim.korap.utils.TimeUtils;
+import org.joda.time.DateTime;
+import org.slf4j.Logger;
+import org.springframework.cache.annotation.CacheEvict;
+import org.springframework.cache.annotation.Cacheable;
+
+import java.util.HashSet;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+
+/**
+ * session object to hold current user sessions and track inactive time to close
+ * unused sessions. Inactive sessions are not enforced until user makes a
+ * request through thrift
+ *
+ * @author hanl
+ */
+public class SessionFactory implements Runnable {
+
+ private static Logger jlog = KustvaktLogger.initiate(SessionFactory.class);
+
+ private final ConcurrentMap<String, TokenContext> sessionsObject;
+ private final ConcurrentMap<String, DateTime> timeCheck;
+ private final ConcurrentMultiMap<String, String> loggedInRecord;
+ // private final ConcurrentMultiMap<String, Long> failedLogins;
+ private final boolean multipleEnabled;
+ private final int inactive;
+
+ public SessionFactory(boolean multipleEnabled, int inactive) {
+ jlog.debug("allow multiple sessions per user: '{}'", multipleEnabled);
+ this.multipleEnabled = multipleEnabled;
+ this.inactive = inactive;
+ this.sessionsObject = new ConcurrentHashMap<>();
+ this.timeCheck = new ConcurrentHashMap<>();
+ this.loggedInRecord = new ConcurrentMultiMap<>();
+ }
+
+ public boolean hasSession(TokenContext context) {
+ if (context.getUsername().equalsIgnoreCase(DemoUser.DEMOUSER_NAME))
+ return false;
+ if (loggedInRecord.containsKey(context.getUsername()) && !loggedInRecord
+ .get(context.getUsername()).isEmpty())
+ return true;
+ return false;
+ }
+
+ @Cacheable("session")
+ public TokenContext getSession(String token) throws KustvaktException {
+ jlog.debug("logged in users: {}", loggedInRecord);
+ TokenContext context = sessionsObject.get(token);
+ if (context != null) {
+ if (isUserSessionValid(token)) {
+ resetInterval(token);
+ return context;
+ }else
+ throw new KustvaktException(StatusCodes.EXPIRED);
+
+ }else
+ throw new KustvaktException(StatusCodes.PERMISSION_DENIED);
+ }
+
+ //todo: ?!
+ @CacheEvict(value = "session", key = "#session.token")
+ public void putSession(final String token, final TokenContext activeUser)
+ throws KustvaktException {
+ if (!hasSession(activeUser) | multipleEnabled) {
+ loggedInRecord.put(activeUser.getUsername(), token);
+ sessionsObject.put(token, activeUser);
+ timeCheck.put(token, TimeUtils.getNow());
+ }else {
+ removeAll(activeUser);
+ throw new KustvaktException(StatusCodes.ALREADY_LOGGED_IN);
+ }
+ }
+
+ public void removeAll(final TokenContext activeUser) {
+ for (String existing : loggedInRecord.get(activeUser.getUsername())) {
+ timeCheck.remove(existing);
+ sessionsObject.remove(existing);
+ }
+ loggedInRecord.remove(activeUser.getUsername());
+ }
+
+ @CacheEvict(value = "session", key = "#session.token")
+ public void removeSession(String token) {
+ String username = sessionsObject.get(token).getUsername();
+ loggedInRecord.remove(username, token);
+ if (loggedInRecord.get(username).isEmpty())
+ loggedInRecord.remove(username);
+ timeCheck.remove(token);
+ sessionsObject.remove(token);
+ }
+
+ /**
+ * reset inactive time interval to 0
+ *
+ * @param token
+ */
+ private void resetInterval(String token) {
+ timeCheck.put(token, TimeUtils.getNow());
+ }
+
+ /**
+ * if user possesses a valid non-expired session token
+ *
+ * @param token
+ * @return validity of user to request a backend function
+ */
+ private boolean isUserSessionValid(String token) {
+ if (timeCheck.containsKey(token)) {
+ if (TimeUtils.plusSeconds(timeCheck.get(token).getMillis(),
+ inactive).isAfterNow()) {
+ jlog.debug("user has session");
+ return true;
+ }else
+ jlog.debug("user with token {} has an invalid session", token);
+ }
+ return false;
+ }
+
+ /**
+ * clean inactive sessions from session object
+ * TODO: persist userdata to database when session times out!
+ */
+ private void timeoutMaintenance() {
+ jlog.debug("running session cleanup thread");
+ Set<String> inactive = new HashSet<>();
+ for (Entry<String, DateTime> entry : timeCheck.entrySet()) {
+ if (!isUserSessionValid(entry.getKey())) {
+ TokenContext user = sessionsObject.get(entry.getKey());
+ jlog.debug("removing user session for user {}",
+ user.getUsername());
+ inactive.add(user.getUsername());
+ removeSession(entry.getKey());
+ }
+ }
+ if (inactive.size() > 0)
+ jlog.debug("removing inactive user session for users '{}' ",
+ inactive);
+
+ // keys:
+ // for (String key : failedLogins.getKeySet()) {
+ // DateTime d = new DateTime(failedLogins.get(key).get(1));
+ // if (d.isBeforeNow()) {
+ // failedLogins.remove(key);
+ // jlog.info("removed failed login counts due to expiration for user {}", key);
+ // continue keys;
+ // }
+ // }
+ }
+
+ /**
+ * run cleanup-thread
+ */
+ @Override
+ public void run() {
+ timeoutMaintenance();
+ jlog.debug("logged users: {}", loggedInRecord.toString());
+
+ }
+}