Added a config properties for a long-time access token expiry and
excluded refresh tokens for public clients in OAuth2 token responses.

Change-Id: Ie1cbf65bc605ab93202642030db9a1893a1cc9a8
diff --git a/full/Changes b/full/Changes
index cda6540..ff981f6 100644
--- a/full/Changes
+++ b/full/Changes
@@ -3,6 +3,10 @@
    - Removed salt from config and updated config files (margaretha)
 03/02/2020
    - Added an admin API for clearing access token cache (margaretha)
+05/02/2020
+   - Added a config properties for a long-time access token expiry 
+     and excluded refresh tokens for public clients in OAuth2 token 
+     responses (margaretha) 
    
 # version 0.62.3
 03/12/2019
diff --git a/full/src/main/java/de/ids_mannheim/korap/config/FullConfiguration.java b/full/src/main/java/de/ids_mannheim/korap/config/FullConfiguration.java
index 8590db1..6da8f2a 100644
--- a/full/src/main/java/de/ids_mannheim/korap/config/FullConfiguration.java
+++ b/full/src/main/java/de/ids_mannheim/korap/config/FullConfiguration.java
@@ -70,6 +70,7 @@
     private Set<String> clientCredentialsScopes;
     private int maxAuthenticationAttempts;
 
+    private int accessTokenLongExpiry;
     private int accessTokenExpiry;
     private int refreshTokenExpiry;
     private int authorizationCodeExpiry;
@@ -251,6 +252,9 @@
                 properties.getProperty("oauth2.refresh.token.expiry", "90D"));
         authorizationCodeExpiry = TimeUtils.convertTimeToSeconds(properties
                 .getProperty("oauth2.authorization.code.expiry", "10M"));
+        
+        setAccessTokenLongExpiry(TimeUtils.convertTimeToSeconds(
+                properties.getProperty("oauth2.access.token.long.expiry", "365D")));
     }
 
     private void setMailConfiguration (Properties properties) {
@@ -636,4 +640,12 @@
     public void setNamedVCPath (String namedVCPath) {
         this.namedVCPath = namedVCPath;
     }
+
+    public int getAccessTokenLongExpiry () {
+        return accessTokenLongExpiry;
+    }
+
+    public void setAccessTokenLongExpiry (int accessTokenLongExpiry) {
+        this.accessTokenLongExpiry = accessTokenLongExpiry;
+    }
 }
diff --git a/full/src/main/java/de/ids_mannheim/korap/oauth2/dao/AccessTokenDao.java b/full/src/main/java/de/ids_mannheim/korap/oauth2/dao/AccessTokenDao.java
index 1b1be95..b82819d 100644
--- a/full/src/main/java/de/ids_mannheim/korap/oauth2/dao/AccessTokenDao.java
+++ b/full/src/main/java/de/ids_mannheim/korap/oauth2/dao/AccessTokenDao.java
@@ -12,6 +12,7 @@
 import javax.persistence.TypedQuery;
 import javax.persistence.criteria.CriteriaBuilder;
 import javax.persistence.criteria.CriteriaQuery;
+import javax.persistence.criteria.Predicate;
 import javax.persistence.criteria.Root;
 
 import org.springframework.beans.factory.annotation.Autowired;
@@ -27,6 +28,7 @@
 import de.ids_mannheim.korap.oauth2.entity.AccessScope;
 import de.ids_mannheim.korap.oauth2.entity.AccessToken;
 import de.ids_mannheim.korap.oauth2.entity.AccessToken_;
+import de.ids_mannheim.korap.oauth2.entity.OAuth2Client;
 import de.ids_mannheim.korap.oauth2.entity.RefreshToken;
 import de.ids_mannheim.korap.utils.ParameterChecker;
 
@@ -44,6 +46,8 @@
     private EntityManager entityManager;
     @Autowired
     private FullConfiguration config;
+    @Autowired
+    private OAuth2ClientDao clientDao;
 
     public AccessTokenDao () {
         super("access_token", "key:access_token");
@@ -53,7 +57,7 @@
             Set<AccessScope> scopes, String userId, String clientId,
             ZonedDateTime authenticationTime) throws KustvaktException {
         ParameterChecker.checkStringValue(token, "access token");
-        ParameterChecker.checkObjectValue(refreshToken, "refresh token");
+//        ParameterChecker.checkObjectValue(refreshToken, "refresh token");
         ParameterChecker.checkObjectValue(scopes, "scopes");
         // ParameterChecker.checkStringValue(userId, "username");
         ParameterChecker.checkStringValue(clientId, "client_id");
@@ -63,15 +67,25 @@
         ZonedDateTime now =
                 ZonedDateTime.now(ZoneId.of(Attributes.DEFAULT_TIME_ZONE));
 
+        ZonedDateTime expiry;
         AccessToken accessToken = new AccessToken();
+        
+        if (refreshToken != null) {
+            accessToken.setRefreshToken(refreshToken);
+            expiry = now.plusSeconds(config.getAccessTokenExpiry());
+        }
+        else {
+            expiry = now.plusSeconds(config.getAccessTokenLongExpiry());
+        }
+        
+        OAuth2Client client = clientDao.retrieveClientById(clientId);
+        
         accessToken.setCreatedDate(now);
-        accessToken
-                .setExpiryDate(now.plusSeconds(config.getAccessTokenExpiry()));
+        accessToken.setExpiryDate(expiry);
         accessToken.setToken(token);
-        accessToken.setRefreshToken(refreshToken);
         accessToken.setScopes(scopes);
         accessToken.setUserId(userId);
-        accessToken.setClientId(clientId);
+        accessToken.setClient(client);
         accessToken.setUserAuthenticationTime(authenticationTime);
         entityManager.persist(accessToken);
     }
@@ -115,13 +129,56 @@
         }
     }
 
-    public List<AccessToken> retrieveAccessTokenByClientId (String clientId) {
+    public AccessToken retrieveAccessToken (String accessToken, String username)
+            throws KustvaktException {
+        ParameterChecker.checkStringValue(accessToken, "access_token");
+        ParameterChecker.checkStringValue(username, "username");
+        AccessToken token = (AccessToken) this.getCacheValue(accessToken);
+        if (token != null) {
+            return token;
+        }
+
         CriteriaBuilder builder = entityManager.getCriteriaBuilder();
         CriteriaQuery<AccessToken> query =
                 builder.createQuery(AccessToken.class);
         Root<AccessToken> root = query.from(AccessToken.class);
+        
+        Predicate condition = builder.and(
+                builder.equal(root.get(AccessToken_.userId), username),
+                builder.equal(root.get(AccessToken_.token), accessToken));
+        
         query.select(root);
-        query.where(builder.equal(root.get(AccessToken_.clientId), clientId));
+        query.where(condition);
+        Query q = entityManager.createQuery(query);
+        try {
+            token = (AccessToken) q.getSingleResult();
+            this.storeInCache(accessToken, token);
+            return token;
+        }
+        catch (NoResultException e) {
+            return null;
+        }
+    }
+
+    
+    public List<AccessToken> retrieveAccessTokenByClientId (String clientId,
+            String username) throws KustvaktException {
+        ParameterChecker.checkStringValue(clientId, "client_id");
+        OAuth2Client client = clientDao.retrieveClientById(clientId);
+        
+        CriteriaBuilder builder = entityManager.getCriteriaBuilder();
+        CriteriaQuery<AccessToken> query =
+                builder.createQuery(AccessToken.class);
+        Root<AccessToken> root = query.from(AccessToken.class);
+        
+        Predicate condition = builder.equal(root.get(AccessToken_.client), client);
+        if (username != null && !username.isEmpty()){
+            condition = builder.and(condition,
+                    builder.equal(root.get(AccessToken_.userId), username));
+        }
+        
+        query.select(root);
+        query.where(condition);
         TypedQuery<AccessToken> q = entityManager.createQuery(query);
         return q.getResultList();
     }
diff --git a/full/src/main/java/de/ids_mannheim/korap/oauth2/dao/OAuth2ClientDao.java b/full/src/main/java/de/ids_mannheim/korap/oauth2/dao/OAuth2ClientDao.java
index ef44bfa..9c421fd 100644
--- a/full/src/main/java/de/ids_mannheim/korap/oauth2/dao/OAuth2ClientDao.java
+++ b/full/src/main/java/de/ids_mannheim/korap/oauth2/dao/OAuth2ClientDao.java
@@ -22,13 +22,16 @@
 import de.ids_mannheim.korap.exceptions.KustvaktException;
 import de.ids_mannheim.korap.exceptions.StatusCodes;
 import de.ids_mannheim.korap.oauth2.constant.OAuth2ClientType;
+import de.ids_mannheim.korap.oauth2.entity.AccessToken;
+import de.ids_mannheim.korap.oauth2.entity.AccessToken_;
 import de.ids_mannheim.korap.oauth2.entity.OAuth2Client;
 import de.ids_mannheim.korap.oauth2.entity.OAuth2Client_;
 import de.ids_mannheim.korap.oauth2.entity.RefreshToken;
 import de.ids_mannheim.korap.oauth2.entity.RefreshToken_;
 import de.ids_mannheim.korap.utils.ParameterChecker;
 
-/** Manages database queries and transactions regarding OAuth2 clients. 
+/**
+ * Manages database queries and transactions regarding OAuth2 clients.
  * 
  * @author margaretha
  *
@@ -133,6 +136,32 @@
         return q.getResultList();
     }
 
+    public List<OAuth2Client> retrieveClientsByAccessTokens (String username)
+            throws KustvaktException {
+        ParameterChecker.checkStringValue(username, "username");
+
+        CriteriaBuilder builder = entityManager.getCriteriaBuilder();
+        CriteriaQuery<OAuth2Client> query =
+                builder.createQuery(OAuth2Client.class);
+
+        Root<OAuth2Client> client = query.from(OAuth2Client.class);
+        Join<OAuth2Client, AccessToken> accessToken =
+                client.join(OAuth2Client_.accessTokens);
+        Predicate condition = builder.and(
+                builder.equal(accessToken.get(AccessToken_.userId), username),
+                builder.equal(accessToken.get(AccessToken_.isRevoked), false),
+                builder.greaterThan(
+                        accessToken
+                                .<ZonedDateTime> get(AccessToken_.expiryDate),
+                        ZonedDateTime
+                                .now(ZoneId.of(Attributes.DEFAULT_TIME_ZONE))));
+        query.select(client);
+        query.where(condition);
+        query.distinct(true);
+        TypedQuery<OAuth2Client> q = entityManager.createQuery(query);
+        return q.getResultList();
+    }
+
     public List<OAuth2Client> retrieveUserRegisteredClients (String username)
             throws KustvaktException {
         ParameterChecker.checkStringValue(username, "username");
diff --git a/full/src/main/java/de/ids_mannheim/korap/oauth2/dao/RefreshTokenDao.java b/full/src/main/java/de/ids_mannheim/korap/oauth2/dao/RefreshTokenDao.java
index 71bafb5..0bcc54d 100644
--- a/full/src/main/java/de/ids_mannheim/korap/oauth2/dao/RefreshTokenDao.java
+++ b/full/src/main/java/de/ids_mannheim/korap/oauth2/dao/RefreshTokenDao.java
@@ -12,7 +12,6 @@
 import javax.persistence.TypedQuery;
 import javax.persistence.criteria.CriteriaBuilder;
 import javax.persistence.criteria.CriteriaQuery;
-import javax.persistence.criteria.Join;
 import javax.persistence.criteria.Predicate;
 import javax.persistence.criteria.Root;
 
@@ -25,7 +24,6 @@
 import de.ids_mannheim.korap.exceptions.KustvaktException;
 import de.ids_mannheim.korap.oauth2.entity.AccessScope;
 import de.ids_mannheim.korap.oauth2.entity.OAuth2Client;
-import de.ids_mannheim.korap.oauth2.entity.OAuth2Client_;
 import de.ids_mannheim.korap.oauth2.entity.RefreshToken;
 import de.ids_mannheim.korap.oauth2.entity.RefreshToken_;
 import de.ids_mannheim.korap.utils.ParameterChecker;
@@ -71,7 +69,7 @@
         entityManager.persist(token);
         return token;
     }
-    
+
     public RefreshToken updateRefreshToken (RefreshToken token)
             throws KustvaktException {
         ParameterChecker.checkObjectValue(token, "refresh_token");
@@ -83,7 +81,7 @@
     public RefreshToken retrieveRefreshToken (String token)
             throws KustvaktException {
         ParameterChecker.checkStringValue(token, "refresh token");
-        
+
         CriteriaBuilder builder = entityManager.getCriteriaBuilder();
         CriteriaQuery<RefreshToken> query =
                 builder.createQuery(RefreshToken.class);
@@ -95,22 +93,22 @@
         Query q = entityManager.createQuery(query);
         return (RefreshToken) q.getSingleResult();
     }
-    
+
     public RefreshToken retrieveRefreshToken (String token, String username)
             throws KustvaktException {
-        
+
         ParameterChecker.checkStringValue(token, "refresh token");
         ParameterChecker.checkStringValue(username, "username");
-        
+
         CriteriaBuilder builder = entityManager.getCriteriaBuilder();
         CriteriaQuery<RefreshToken> query =
                 builder.createQuery(RefreshToken.class);
-        
+
         Root<RefreshToken> root = query.from(RefreshToken.class);
         Predicate condition = builder.and(
                 builder.equal(root.get(RefreshToken_.userId), username),
                 builder.equal(root.get(RefreshToken_.token), token));
-        
+
         query.select(root);
         query.where(condition);
         TypedQuery<RefreshToken> q = entityManager.createQuery(query);
@@ -122,18 +120,25 @@
         }
     }
 
-    public List<RefreshToken> retrieveRefreshTokenByClientId (String clientId)
-            throws KustvaktException {
+    public List<RefreshToken> retrieveRefreshTokenByClientId (String clientId,
+            String username) throws KustvaktException {
         ParameterChecker.checkStringValue(clientId, "client_id");
-
+        OAuth2Client client = clientDao.retrieveClientById(clientId);
+        
         CriteriaBuilder builder = entityManager.getCriteriaBuilder();
         CriteriaQuery<RefreshToken> query =
                 builder.createQuery(RefreshToken.class);
         Root<RefreshToken> root = query.from(RefreshToken.class);
-        Join<RefreshToken, OAuth2Client> client =
-                root.join(RefreshToken_.client);
+
+        Predicate condition =
+                builder.equal(root.get(RefreshToken_.client), client);
+        if (username != null && !username.isEmpty()) {
+            condition = builder.and(condition,
+                    builder.equal(root.get(RefreshToken_.userId), username));
+        }
+
         query.select(root);
-        query.where(builder.equal(client.get(OAuth2Client_.id), clientId));
+        query.where(condition);
         TypedQuery<RefreshToken> q = entityManager.createQuery(query);
         return q.getResultList();
     }
diff --git a/full/src/main/java/de/ids_mannheim/korap/oauth2/entity/AccessToken.java b/full/src/main/java/de/ids_mannheim/korap/oauth2/entity/AccessToken.java
index a94b081..17ee77d 100644
--- a/full/src/main/java/de/ids_mannheim/korap/oauth2/entity/AccessToken.java
+++ b/full/src/main/java/de/ids_mannheim/korap/oauth2/entity/AccessToken.java
@@ -45,8 +45,8 @@
     private ZonedDateTime expiryDate;
     @Column(name = "user_id")
     private String userId;
-    @Column(name = "client_id")
-    private String clientId;
+//    @Column(name = "client_id")
+//    private String clientId;
     @Column(name = "is_revoked")
     private boolean isRevoked;
     @Column(name = "user_auth_time", updatable = false)
@@ -69,4 +69,8 @@
     @ManyToOne(fetch = FetchType.LAZY)
     @JoinColumn(name = "refresh_token")
     private RefreshToken refreshToken;
+    
+    @ManyToOne(fetch = FetchType.LAZY)
+    @JoinColumn(name = "client")
+    private OAuth2Client client;
 }
diff --git a/full/src/main/java/de/ids_mannheim/korap/oauth2/entity/OAuth2Client.java b/full/src/main/java/de/ids_mannheim/korap/oauth2/entity/OAuth2Client.java
index 6db32b1..1d7b200 100644
--- a/full/src/main/java/de/ids_mannheim/korap/oauth2/entity/OAuth2Client.java
+++ b/full/src/main/java/de/ids_mannheim/korap/oauth2/entity/OAuth2Client.java
@@ -44,6 +44,9 @@
     @OneToMany(fetch = FetchType.LAZY, mappedBy = "client")
     private List<RefreshToken> refreshTokens;
     
+    @OneToMany(fetch = FetchType.LAZY, mappedBy = "client")
+    private List<AccessToken> accessTokens;
+    
     @Override
     public String toString () {
         return "id=" + id + ", name=" + name + ", secret=" + secret + ", type="
diff --git a/full/src/main/java/de/ids_mannheim/korap/oauth2/oltu/service/OltuTokenService.java b/full/src/main/java/de/ids_mannheim/korap/oauth2/oltu/service/OltuTokenService.java
index dcbcafe..bbf7f04 100644
--- a/full/src/main/java/de/ids_mannheim/korap/oauth2/oltu/service/OltuTokenService.java
+++ b/full/src/main/java/de/ids_mannheim/korap/oauth2/oltu/service/OltuTokenService.java
@@ -125,7 +125,7 @@
                     OAuth2Error.INVALID_REQUEST);
         }
 
-        clientService.authenticateClient(clientId, clientSecret);
+        OAuth2Client oAuth2Client = clientService.authenticateClient(clientId, clientSecret);
 
         RefreshToken refreshToken;
         try {
@@ -166,7 +166,8 @@
 
         return createsAccessTokenResponse(scopes, requestedScopes, clientId,
                 refreshToken.getUserId(),
-                refreshToken.getUserAuthenticationTime());
+                refreshToken.getUserAuthenticationTime(),
+                clientService.isPublicClient(oAuth2Client));
 
         // without new refresh token
         // return createsAccessTokenResponse(scopes, requestedScopes,
@@ -201,10 +202,11 @@
 
         Set<String> scopes = scopeService
                 .convertAccessScopesToStringSet(authorization.getScopes());
+        OAuth2Client oAuth2Client = clientService.retrieveClient(clientId);
         return createsAccessTokenResponse(scopes, authorization.getScopes(),
                 authorization.getClientId(), authorization.getUserId(),
-                authorization.getUserAuthenticationTime());
-
+                authorization.getUserAuthenticationTime(),
+                clientService.isPublicClient(oAuth2Client));
     }
 
     /**
@@ -261,7 +263,8 @@
         Set<AccessScope> accessScopes =
                 scopeService.convertToAccessScope(scopes);
         return createsAccessTokenResponse(scopes, accessScopes, clientId,
-                username, authenticationTime);
+                username, authenticationTime,
+                false);
     }
 
     /**
@@ -290,7 +293,7 @@
         }
 
         // OAuth2Client client =
-        clientService.authenticateClient(clientId, clientSecret);
+        OAuth2Client oAuth2Client = clientService.authenticateClient(clientId, clientSecret);
 
         // if (!client.isNative()) {
         // throw new KustvaktException(
@@ -308,7 +311,7 @@
         Set<AccessScope> accessScopes =
                 scopeService.convertToAccessScope(scopes);
         return createsAccessTokenResponse(scopes, accessScopes, clientId, null,
-                authenticationTime);
+                authenticationTime,clientService.isPublicClient(oAuth2Client));
     }
 
     /**
@@ -339,14 +342,20 @@
      */
     private OAuthResponse createsAccessTokenResponse (Set<String> scopes,
             Set<AccessScope> accessScopes, String clientId, String userId,
-            ZonedDateTime authenticationTime)
+            ZonedDateTime authenticationTime, boolean isPublicClient)
             throws OAuthSystemException, KustvaktException {
 
         String random = randomGenerator.createRandomCode();
-        RefreshToken refreshToken = refreshDao.storeRefreshToken(random, userId,
-                authenticationTime, clientId, accessScopes);
-        return createsAccessTokenResponse(scopes, accessScopes, clientId,
-                userId, authenticationTime, refreshToken);
+        if (isPublicClient){
+            return createsAccessTokenResponse(scopes, accessScopes, clientId,
+                    userId, authenticationTime);
+            }
+        else {
+            RefreshToken refreshToken = refreshDao.storeRefreshToken(random, userId,
+                    authenticationTime, clientId, accessScopes);
+            return createsAccessTokenResponse(scopes, accessScopes, clientId,
+                    userId, authenticationTime, refreshToken);
+        }
     }
 
     private OAuthResponse createsAccessTokenResponse (Set<String> scopes,
@@ -365,6 +374,22 @@
                 .setRefreshToken(refreshToken.getToken())
                 .setScope(String.join(" ", scopes)).buildJSONMessage();
     }
+    
+    private OAuthResponse createsAccessTokenResponse (Set<String> scopes,
+            Set<AccessScope> accessScopes, String clientId, String userId,
+            ZonedDateTime authenticationTime)
+            throws OAuthSystemException, KustvaktException {
+
+        String accessToken = randomGenerator.createRandomCode();
+        tokenDao.storeAccessToken(accessToken, null, accessScopes,
+                userId, clientId, authenticationTime);
+
+        return OAuthASResponse.tokenResponse(Status.OK.getStatusCode())
+                .setAccessToken(accessToken)
+                .setTokenType(TokenType.BEARER.toString())
+                .setExpiresIn(String.valueOf(config.getAccessTokenLongExpiry()))
+                .setScope(String.join(" ", scopes)).buildJSONMessage();
+    }
 
     public void revokeToken (OAuth2RevokeTokenRequest revokeTokenRequest)
             throws KustvaktException {
@@ -389,8 +414,7 @@
     private boolean revokeAccessToken (String token) throws KustvaktException {
         try {
             AccessToken accessToken = tokenDao.retrieveAccessToken(token);
-            accessToken.setRevoked(true);
-            tokenDao.updateAccessToken(accessToken);
+            revokeAccessToken(accessToken);
             return true;
         }
         catch (KustvaktException e) {
@@ -400,6 +424,14 @@
             throw e;
         }
     }
+    
+    private void revokeAccessToken (AccessToken accessToken)
+            throws KustvaktException {
+        if (accessToken != null){
+            accessToken.setRevoked(true);
+            tokenDao.updateAccessToken(accessToken);
+        }
+    }
 
     private boolean revokeRefreshToken (String token) throws KustvaktException {
         RefreshToken refreshToken = null;
@@ -410,11 +442,10 @@
             return false;
         }
 
-        revokeRefreshToken(refreshToken);
-        return true;
+        return revokeRefreshToken(refreshToken);
     }
 
-    private void revokeRefreshToken (RefreshToken refreshToken)
+    private boolean revokeRefreshToken (RefreshToken refreshToken)
             throws KustvaktException {
         if (refreshToken != null){
             refreshToken.setRevoked(true);
@@ -425,7 +456,9 @@
                 accessToken.setRevoked(true);
                 tokenDao.updateAccessToken(accessToken);
             }
+            return true;
         }
+        return false;
     }
 
     public void revokeAllClientTokensViaSuperClient (String username,
@@ -442,11 +475,18 @@
         }
 
         String clientId = revokeTokenRequest.getClientId();
-        List<RefreshToken> refreshTokens =
-                refreshDao.retrieveRefreshTokenByClientId(clientId);
-
-        for (RefreshToken r : refreshTokens) {
-            if (r.getUserId().equals(username)){
+        OAuth2Client client = clientService.retrieveClient(clientId);
+        if (clientService.isPublicClient(client)) {
+            List<AccessToken> accessTokens =
+                    tokenDao.retrieveAccessTokenByClientId(clientId, username);
+            for (AccessToken t : accessTokens) {
+                revokeAccessToken(t);
+            }
+        }
+        else {
+            List<RefreshToken> refreshTokens = refreshDao
+                    .retrieveRefreshTokenByClientId(clientId, username);
+            for (RefreshToken r : refreshTokens) {
                 revokeRefreshToken(r);
             }
         }
@@ -466,7 +506,10 @@
         
         String token = revokeTokenRequest.getToken();
         RefreshToken refreshToken = refreshDao.retrieveRefreshToken(token, username);
-        revokeRefreshToken(refreshToken);
+        if (!revokeRefreshToken(refreshToken)){
+            AccessToken accessToken = tokenDao.retrieveAccessToken(token, username);
+            revokeAccessToken(accessToken);
+        }
     }
     
     public List<OAuth2RefreshTokenDto> listUserRefreshToken (String username, String clientId,
diff --git a/full/src/main/java/de/ids_mannheim/korap/oauth2/service/OAuth2ClientService.java b/full/src/main/java/de/ids_mannheim/korap/oauth2/service/OAuth2ClientService.java
index b6d7e2b..05280a2 100644
--- a/full/src/main/java/de/ids_mannheim/korap/oauth2/service/OAuth2ClientService.java
+++ b/full/src/main/java/de/ids_mannheim/korap/oauth2/service/OAuth2ClientService.java
@@ -217,14 +217,14 @@
 
         // revoke all related access tokens
         List<AccessToken> tokens =
-                tokenDao.retrieveAccessTokenByClientId(clientId);
+                tokenDao.retrieveAccessTokenByClientId(clientId,null);
         for (AccessToken token : tokens) {
             token.setRevoked(true);
             tokenDao.updateAccessToken(token);
         }
 
         List<RefreshToken> refreshTokens =
-                refreshDao.retrieveRefreshTokenByClientId(clientId);
+                refreshDao.retrieveRefreshTokenByClientId(clientId,null);
         for (RefreshToken token : refreshTokens) {
             token.setRevoked(true);
             refreshDao.updateRefreshToken(token);
@@ -358,10 +358,23 @@
                     "Only super client is allowed to list user authorized clients.",
                     OAuth2Error.UNAUTHORIZED_CLIENT);
         }
+                
         List<OAuth2Client> userClients =
                 clientDao.retrieveUserAuthorizedClients(username);
-        Collections.sort(userClients);
-        return createClientDtos(userClients);
+        userClients.addAll(clientDao.retrieveClientsByAccessTokens(username));
+        
+        List<String> clientIds = new ArrayList<>();
+        List<OAuth2Client> uniqueClients = new ArrayList<>();
+        for (OAuth2Client c : userClients){
+            String id = c.getId();
+            if (!clientIds.contains(id)){
+                clientIds.add(id);
+                uniqueClients.add(c);
+            }        
+        }
+        
+        Collections.sort(uniqueClients);
+        return createClientDtos(uniqueClients);
     }
     
     public List<OAuth2UserClientDto> listUserRegisteredClients (String username,
@@ -391,4 +404,8 @@
         }
         return dtoList;
     }
+
+    public boolean isPublicClient (OAuth2Client oAuth2Client) {
+        return oAuth2Client.getType().equals(OAuth2ClientType.PUBLIC);
+    }
 }
diff --git a/full/src/main/resources/db/sqlite/V1.4__oauth2_tables.sql b/full/src/main/resources/db/sqlite/V1.4__oauth2_tables.sql
index 00be974..abb9199 100644
--- a/full/src/main/resources/db/sqlite/V1.4__oauth2_tables.sql
+++ b/full/src/main/resources/db/sqlite/V1.4__oauth2_tables.sql
@@ -77,13 +77,13 @@
 	id INTEGER PRIMARY KEY AUTOINCREMENT,
 	token VARCHAR(255) NOT NULL,
 	user_id VARCHAR(100) DEFAULT NULL,
-	client_id VARCHAR(100) DEFAULT NULL,
 	created_date TIMESTAMP NOT NULL,
 	expiry_date TIMESTAMP NOT NULL,
 	is_revoked BOOLEAN DEFAULT 0,
 	user_auth_time TIMESTAMP NOT NULL,
 	refresh_token INTEGER DEFAULT NULL,
-	FOREIGN KEY (client_id)
+	client VARCHAR(100) DEFAULT NULL,
+	FOREIGN KEY (client)
 	   REFERENCES oauth2_client(id)
 	   ON DELETE CASCADE
 	FOREIGN KEY (refresh_token)
diff --git a/full/src/main/resources/db/test/V3.5__insert_oauth2_clients.sql b/full/src/main/resources/db/test/V3.5__insert_oauth2_clients.sql
index 33330c1..d179fdc 100644
--- a/full/src/main/resources/db/test/V3.5__insert_oauth2_clients.sql
+++ b/full/src/main/resources/db/test/V3.5__insert_oauth2_clients.sql
@@ -21,7 +21,15 @@
   "This is a test nonsuper confidential client.",
   "http://third.party.com/confidential", 1712550103);
 
-  
+INSERT INTO oauth2_client(id,name,secret,type,super,
+  redirect_uri,registered_by, description,url,url_hashcode) 
+VALUES ("52atrL0ajex_3_5imd9Mgw","confidential client 2",
+  "$2a$08$vi1FbuN3p6GcI1tSxMAoeuIYL8Yw3j6A8wJthaN8ZboVnrQaTwLPq",
+  "CONFIDENTIAL", 0,
+  "https://example.client.de/redirect", "system",
+  "This is a test nonsuper confidential client.",
+  "http://example.client.de", 1535365678);
+
 INSERT INTO oauth2_client(id,name,secret,type,super,
   redirect_uri, registered_by, description, url,url_hashcode) 
 VALUES ("8bIDtZnH6NvRkW2Fq","third party client",null,
diff --git a/full/src/main/resources/kustvakt.conf b/full/src/main/resources/kustvakt.conf
index 68577ae..62546c6 100644
--- a/full/src/main/resources/kustvakt.conf
+++ b/full/src/main/resources/kustvakt.conf
@@ -60,6 +60,7 @@
 oauth2.max.attempts = 1
 # expiry in seconds (S), minutes (M), hours (H), days (D)
 oauth2.access.token.expiry = 1D
+oauth2.access.token.long.expiry = 365D
 oauth2.refresh.token.expiry = 90D
 oauth2.authorization.code.expiry = 10M
 # -- scopes separated by space
diff --git a/full/src/test/java/de/ids_mannheim/korap/web/controller/OAuth2AccessTokenTest.java b/full/src/test/java/de/ids_mannheim/korap/web/controller/OAuth2AccessTokenTest.java
index cc28512..f6768f1 100644
--- a/full/src/test/java/de/ids_mannheim/korap/web/controller/OAuth2AccessTokenTest.java
+++ b/full/src/test/java/de/ids_mannheim/korap/web/controller/OAuth2AccessTokenTest.java
@@ -179,6 +179,20 @@
 
         testSearchWithRevokedAccessToken(accessToken);
     }
+    
+    @Test
+    public void testRevokeAccessTokenPublicClientViaSuperClient()
+            throws KustvaktException {
+        String code = requestAuthorizationCode(publicClientId, "", null,
+                userAuthHeader);
+        ClientResponse response = requestTokenWithAuthorizationCodeAndForm(
+                publicClientId, "", code);
+        
+        JsonNode node = JsonUtils.readTree(response.getEntity(String.class));
+        String accessToken = node.at("/access_token").asText();
+        testRevokeTokenViaSuperClient(accessToken, userAuthHeader);
+        testSearchWithRevokedAccessToken(accessToken);
+    }
 
     private void testSearchWithRevokedAccessToken (String accessToken)
             throws KustvaktException {
diff --git a/full/src/test/java/de/ids_mannheim/korap/web/controller/OAuth2ClientControllerTest.java b/full/src/test/java/de/ids_mannheim/korap/web/controller/OAuth2ClientControllerTest.java
index c80ab36..2ee4bc2 100644
--- a/full/src/test/java/de/ids_mannheim/korap/web/controller/OAuth2ClientControllerTest.java
+++ b/full/src/test/java/de/ids_mannheim/korap/web/controller/OAuth2ClientControllerTest.java
@@ -515,14 +515,13 @@
         assertEquals(Status.OK.getStatusCode(), response.getStatus());
 
         // client 1
-        String code = requestAuthorizationCode(publicClientId, clientSecret,
+        String code = requestAuthorizationCode(publicClientId, "",
                 null, userAuthHeader);
         response = requestTokenWithAuthorizationCodeAndForm(publicClientId, "",
                 code);
         assertEquals(Status.OK.getStatusCode(), response.getStatus());
 
         JsonNode node = JsonUtils.readTree(response.getEntity(String.class));
-        String refreshToken = node.at("/refresh_token").asText();
         String accessToken = node.at("/access_token").asText();
 
         // client 2
@@ -530,30 +529,45 @@
                 null, userAuthHeader);
         response = requestTokenWithAuthorizationCodeAndForm(
                 confidentialClientId, clientSecret, code);
+        String refreshToken = node.at("/refresh_token").asText();
         assertEquals(Status.OK.getStatusCode(), response.getStatus());
 
         requestAuthorizedClientList(userAuthHeader);
         testListAuthorizedClientWithMultipleRefreshTokens(userAuthHeader);
-        testListAuthorizedClientWithRefreshTokenFromAnotherUser(userAuthHeader);
+        testListAuthorizedClientWithMultipleAccessTokens(userAuthHeader);
+        testWithClientsFromAnotherUser(userAuthHeader);
         
         // revoke client 1
         testRevokeAllTokenViaSuperClient(publicClientId, userAuthHeader,
-                accessToken, refreshToken);
-        testRequestTokenWithRevokedRefreshToken(publicClientId, clientSecret,
-                refreshToken);
+                accessToken);
         
         // revoke client 2
         node = JsonUtils.readTree(response.getEntity(String.class));
         accessToken = node.at("/access_token").asText();
         refreshToken = node.at("/refresh_token").asText();
         testRevokeAllTokenViaSuperClient(confidentialClientId, userAuthHeader,
-                accessToken, refreshToken);
+                accessToken);
+        testRequestTokenWithRevokedRefreshToken(confidentialClientId, clientSecret,
+                refreshToken);
     }
 
     private void testListAuthorizedClientWithMultipleRefreshTokens (
             String userAuthHeader) throws KustvaktException {
+        // client 2
+        String code = requestAuthorizationCode(confidentialClientId, clientSecret,
+                null, userAuthHeader);
+        ClientResponse response = requestTokenWithAuthorizationCodeAndForm(
+                confidentialClientId, clientSecret, code);
+
+        assertEquals(Status.OK.getStatusCode(), response.getStatus());
+        
+        requestAuthorizedClientList(userAuthHeader);
+    }
+    
+    private void testListAuthorizedClientWithMultipleAccessTokens (
+            String userAuthHeader) throws KustvaktException {
         // client 1
-        String code = requestAuthorizationCode(publicClientId, clientSecret,
+        String code = requestAuthorizationCode(publicClientId, "",
                 null, userAuthHeader);
         ClientResponse response = requestTokenWithAuthorizationCodeAndForm(
                 publicClientId, "", code);
@@ -563,29 +577,44 @@
         requestAuthorizedClientList(userAuthHeader);
     }
     
-    private void testListAuthorizedClientWithRefreshTokenFromAnotherUser (
+    private void testWithClientsFromAnotherUser (
             String userAuthHeader) throws KustvaktException {
 
         String aaaAuthHeader = HttpAuthorizationHandler
                 .createBasicAuthorizationHeaderValue("aaa", "pwd");
+        
         // client 1
-        String code = requestAuthorizationCode(publicClientId, clientSecret,
+        String code = requestAuthorizationCode(publicClientId, "",
                 null, aaaAuthHeader);
         ClientResponse response = requestTokenWithAuthorizationCodeAndForm(
                 publicClientId, "", code);
-
-        requestAuthorizedClientList(userAuthHeader);
-
+        
         JsonNode node = JsonUtils.readTree(response.getEntity(String.class));
-        String accessToken = node.at("/access_token").asText();
+        String accessToken1 = node.at("/access_token").asText();
+        
+        // client 2
+        code = requestAuthorizationCode(confidentialClientId, clientSecret,
+                null, aaaAuthHeader);
+        response = requestTokenWithAuthorizationCodeAndForm(
+                confidentialClientId, clientSecret, code);
+
+        node = JsonUtils.readTree(response.getEntity(String.class));
+        String accessToken2 = node.at("/access_token").asText();
         String refreshToken = node.at("/refresh_token").asText();
 
+        requestAuthorizedClientList(aaaAuthHeader);
+        requestAuthorizedClientList(userAuthHeader);
+
         testRevokeAllTokenViaSuperClient(publicClientId, aaaAuthHeader,
-                accessToken, refreshToken);
+                accessToken1);
+        testRevokeAllTokenViaSuperClient(confidentialClientId, aaaAuthHeader,
+                accessToken2);
+        testRequestTokenWithRevokedRefreshToken(confidentialClientId, clientSecret,
+                refreshToken);
     }
 
     private void testRevokeAllTokenViaSuperClient (String clientId,
-            String userAuthHeader, String accessToken, String refreshToken)
+            String userAuthHeader, String accessToken)
             throws KustvaktException {
         // check token before revoking
         ClientResponse response = searchWithAccessToken(accessToken);
@@ -613,9 +642,6 @@
                 node.at("/errors/0/0").asInt());
         assertEquals("Access token is invalid",
                 node.at("/errors/0/1").asText());
-
-        testRequestTokenWithRevokedRefreshToken(clientId, clientSecret,
-                refreshToken);
     }
     
     @Test
diff --git a/full/src/test/java/de/ids_mannheim/korap/web/controller/OAuth2ControllerTest.java b/full/src/test/java/de/ids_mannheim/korap/web/controller/OAuth2ControllerTest.java
index b15d1e7..be97272 100644
--- a/full/src/test/java/de/ids_mannheim/korap/web/controller/OAuth2ControllerTest.java
+++ b/full/src/test/java/de/ids_mannheim/korap/web/controller/OAuth2ControllerTest.java
@@ -176,22 +176,15 @@
         JsonNode node = JsonUtils.readTree(entity);
 
         String accessToken = node.at("/access_token").asText();
-        String refreshToken = node.at("/refresh_token").asText();
 
         assertEquals(TokenType.BEARER.toString(),
                 node.at("/token_type").asText());
-        assertNotNull(node.at("/expires_in").asText());
+        assertEquals(31536000, node.at("/expires_in").asInt());
 
         testRevokeToken(accessToken, publicClientId,null,
                 ACCESS_TOKEN_TYPE);
 
-        testRequestRefreshTokenInvalidScope(publicClientId, refreshToken);
-        testRequestRefreshTokenInvalidClient(refreshToken);
-        testRequestRefreshTokenInvalidRefreshToken(publicClientId);
-        testRequestRefreshTokenPublicClient(publicClientId, refreshToken);
-
-        testRequestTokenWithRevokedRefreshToken(publicClientId, null,
-                refreshToken);
+        assertTrue(node.at("/refresh_token").isMissingNode());
     }
 
     @Test
@@ -226,6 +219,11 @@
         testRequestTokenWithUsedAuthorization(code);
         
         String refreshToken = node.at("/refresh_token").asText();
+        
+        testRequestRefreshTokenInvalidScope(confidentialClientId, refreshToken);
+        testRequestRefreshTokenInvalidClient(refreshToken);
+        testRequestRefreshTokenInvalidRefreshToken(confidentialClientId);
+        
         testRevokeToken(refreshToken, confidentialClientId,clientSecret,
                 REFRESH_TOKEN_TYPE);
         testRequestTokenWithRevokedRefreshToken(confidentialClientId,
@@ -567,6 +565,7 @@
         MultivaluedMap<String, String> form = new MultivaluedMapImpl();
         form.add("grant_type", GrantType.REFRESH_TOKEN.toString());
         form.add("client_id", clientId);
+        form.add("client_secret", clientSecret);
         form.add("refresh_token", refreshToken);
         form.add("scope", "search serialize_query");
 
@@ -635,6 +634,7 @@
         MultivaluedMap<String, String> form = new MultivaluedMapImpl();
         form.add("grant_type", GrantType.REFRESH_TOKEN.toString());
         form.add("client_id", clientId);
+        form.add("client_secret", clientSecret);
         form.add("refresh_token", "Lia8s8w8tJeZSBlaQDrYV8ion3l");
 
         ClientResponse response = resource().path(API_VERSION).path("oauth2").path("token")
@@ -667,23 +667,6 @@
         assertEquals("SUCCESS", response.getEntity(String.class));
     }
     
-    private void testRevokeTokenViaSuperClient (String token, String userAuthHeader) {
-        MultivaluedMap<String, String> form = new MultivaluedMapImpl();
-        form.add("token", token);
-        form.add("super_client_id", superClientId);
-        form.add("super_client_secret", clientSecret);
-
-        ClientResponse response = resource().path(API_VERSION)
-                .path("oauth2").path("revoke").path("super")
-                .header(HttpHeaders.CONTENT_TYPE,
-                        ContentType.APPLICATION_FORM_URLENCODED)
-                .header(Attributes.AUTHORIZATION, userAuthHeader)
-                .entity(form).post(ClientResponse.class);
-
-        assertEquals(Status.OK.getStatusCode(), response.getStatus());
-        assertEquals("SUCCESS", response.getEntity(String.class));
-    }
-    
     @Test
     public void testListRefreshToken () throws KustvaktException {
         String username = "gurgle";
@@ -699,41 +682,41 @@
         String refreshToken1 = node.at("/refresh_token").asText();
 
         // client 1
-        String code = requestAuthorizationCode(publicClientId, clientSecret,
+        String code = requestAuthorizationCode(confidentialClientId, clientSecret,
                 null, userAuthHeader);
-        response = requestTokenWithAuthorizationCodeAndForm(publicClientId, "",
-                code);
+        response = requestTokenWithAuthorizationCodeAndForm(
+                confidentialClientId, clientSecret, code);
         assertEquals(Status.OK.getStatusCode(), response.getStatus());
 
         // client 2
+        code = requestAuthorizationCode(confidentialClientId2, clientSecret,
+                null, userAuthHeader);
+        response = requestTokenWithAuthorizationCodeAndForm(
+                confidentialClientId2, clientSecret, code);
+        assertEquals(Status.OK.getStatusCode(), response.getStatus());
+
+        // list
+        node = requestRefreshTokenList(userAuthHeader);
+        assertEquals(2, node.size());
+        assertEquals(confidentialClientId, node.at("/0/clientId").asText());
+        assertEquals(confidentialClientId2, node.at("/1/clientId").asText());
+        
+        // client 1
         code = requestAuthorizationCode(confidentialClientId, clientSecret,
                 null, userAuthHeader);
         response = requestTokenWithAuthorizationCodeAndForm(
                 confidentialClientId, clientSecret, code);
         assertEquals(Status.OK.getStatusCode(), response.getStatus());
 
-        // list
-        node = requestRefreshTokenList(userAuthHeader);
-        assertEquals(2, node.size());
-        assertEquals(publicClientId, node.at("/0/clientId").asText());
-        assertEquals(confidentialClientId, node.at("/1/clientId").asText());
-        
-        // client 1
-        code = requestAuthorizationCode(publicClientId, clientSecret,
-                null, userAuthHeader);
-        response = requestTokenWithAuthorizationCodeAndForm(
-                publicClientId, "", code);
-        assertEquals(Status.OK.getStatusCode(), response.getStatus());
-
         // another user
         String darlaAuthHeader = HttpAuthorizationHandler
                 .createBasicAuthorizationHeaderValue("darla", "pwd");
         // client 1
-        code = requestAuthorizationCode(publicClientId, clientSecret,
+        code = requestAuthorizationCode(confidentialClientId, clientSecret,
                 null, darlaAuthHeader);
         assertEquals(Status.OK.getStatusCode(), response.getStatus());
         response = requestTokenWithAuthorizationCodeAndForm(
-                publicClientId, "", code);
+                confidentialClientId, clientSecret, code);
         
         node = JsonUtils.readTree(response.getEntity(String.class));
         String refreshToken5 = node.at("/refresh_token").asText();
@@ -744,9 +727,9 @@
         
         testRevokeToken(refreshToken1, superClientId, clientSecret,
                 REFRESH_TOKEN_TYPE);
-        testRevokeToken(node.at("/0/token").asText(), publicClientId, null,
-                REFRESH_TOKEN_TYPE);
-        testRevokeToken(node.at("/1/token").asText(), confidentialClientId,
+        testRevokeToken(node.at("/0/token").asText(), confidentialClientId, 
+                clientSecret, REFRESH_TOKEN_TYPE);
+        testRevokeToken(node.at("/1/token").asText(), confidentialClientId2,
                 clientSecret, REFRESH_TOKEN_TYPE);
         
         node = requestRefreshTokenList(userAuthHeader);
diff --git a/full/src/test/java/de/ids_mannheim/korap/web/controller/OAuth2TestBase.java b/full/src/test/java/de/ids_mannheim/korap/web/controller/OAuth2TestBase.java
index a30ca24..91604bb 100644
--- a/full/src/test/java/de/ids_mannheim/korap/web/controller/OAuth2TestBase.java
+++ b/full/src/test/java/de/ids_mannheim/korap/web/controller/OAuth2TestBase.java
@@ -42,6 +42,7 @@
     
     protected String publicClientId = "8bIDtZnH6NvRkW2Fq";
     protected String confidentialClientId = "9aHsGW6QflV13ixNpez";
+    protected String confidentialClientId2 = "9aHsGW6QflV13ixNpez";
     protected String superClientId = "fCBbQkAyYzI4NzUxMg";
     protected String clientSecret = "secret";
 
@@ -147,6 +148,7 @@
         MultivaluedMap<String, String> form = new MultivaluedMapImpl();
         form.add("grant_type", GrantType.REFRESH_TOKEN.toString());
         form.add("client_id", clientId);
+        form.add("client_secret", clientSecret);
         form.add("refresh_token", refreshToken);
         if (clientSecret != null) {
             form.add("client_secret", clientSecret);
@@ -187,5 +189,22 @@
                 .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
                 .get(ClientResponse.class);
     }
+    
+    protected void testRevokeTokenViaSuperClient (String token, String userAuthHeader) {
+        MultivaluedMap<String, String> form = new MultivaluedMapImpl();
+        form.add("token", token);
+        form.add("super_client_id", superClientId);
+        form.add("super_client_secret", clientSecret);
+
+        ClientResponse response = resource().path(API_VERSION)
+                .path("oauth2").path("revoke").path("super")
+                .header(HttpHeaders.CONTENT_TYPE,
+                        ContentType.APPLICATION_FORM_URLENCODED)
+                .header(Attributes.AUTHORIZATION, userAuthHeader)
+                .entity(form).post(ClientResponse.class);
+
+        assertEquals(Status.OK.getStatusCode(), response.getStatus());
+        assertEquals("SUCCESS", response.getEntity(String.class));
+    }
 
 }