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