Handled user-defined refresh token expiry.

Added in client info and list API responses.

Change-Id: I309fc1856ccbab4e3b61e6e1953d573dee3b409b
diff --git a/full/Changes b/full/Changes
index 6eff087..1827ffc 100644
--- a/full/Changes
+++ b/full/Changes
@@ -3,6 +3,8 @@
 2022-05-25
  - Added a new API: list plugins (e.g for marketplace)
  - Added redirect URI validation in authorization request (addressed #374)
+ - Handled user-defined refresh token expiry (added in client info and 
+   list API response)
 
 
 # version 0.67.1
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 c528e86..13f0397 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
@@ -23,7 +23,6 @@
 
 import de.ids_mannheim.korap.config.Attributes;
 import de.ids_mannheim.korap.config.FullConfiguration;
-import de.ids_mannheim.korap.dao.AdminDao;
 import de.ids_mannheim.korap.exceptions.KustvaktException;
 import de.ids_mannheim.korap.exceptions.StatusCodes;
 import de.ids_mannheim.korap.oauth2.constant.OAuth2ClientType;
@@ -49,8 +48,6 @@
     private EntityManager entityManager;
     @Autowired
     private FullConfiguration config;
-    @Autowired
-    private AdminDao adminDao;
 
     public void registerClient (String id, String secretHashcode, String name,
             OAuth2ClientType type, String url, String redirectURI,
@@ -81,9 +78,10 @@
         else {
             client.setPermitted(true);
         }
-        if (refreshTokenExpiry <= 0) {
+        if (refreshTokenExpiry <= 0 && type.equals(OAuth2ClientType.CONFIDENTIAL)) {
            refreshTokenExpiry = config.getRefreshTokenLongExpiry();
         }
+        client.setRefreshTokenExpiry(refreshTokenExpiry);
         entityManager.persist(client);
     }
 
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 7ff6138..83fbc6f 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
@@ -20,7 +20,6 @@
 import org.springframework.transaction.annotation.Transactional;
 
 import de.ids_mannheim.korap.config.Attributes;
-import de.ids_mannheim.korap.config.FullConfiguration;
 import de.ids_mannheim.korap.exceptions.KustvaktException;
 import de.ids_mannheim.korap.oauth2.entity.AccessScope;
 import de.ids_mannheim.korap.oauth2.entity.OAuth2Client;
@@ -41,21 +40,18 @@
     @PersistenceContext
     private EntityManager entityManager;
     @Autowired
-    private FullConfiguration config;
-    @Autowired
     private OAuth2ClientDao clientDao;
 
     public RefreshToken storeRefreshToken (String refreshToken, String userId,
-            ZonedDateTime userAuthenticationTime, String clientId,
+            ZonedDateTime userAuthenticationTime, OAuth2Client client,
             Set<AccessScope> scopes) throws KustvaktException {
         ParameterChecker.checkStringValue(refreshToken, "refresh_token");
         // ParameterChecker.checkStringValue(userId, "username");
-        ParameterChecker.checkStringValue(clientId, "client_id");
+        ParameterChecker.checkObjectValue(client, "client");
         ParameterChecker.checkObjectValue(scopes, "scopes");
 
         ZonedDateTime now =
                 ZonedDateTime.now(ZoneId.of(Attributes.DEFAULT_TIME_ZONE));
-        OAuth2Client client = clientDao.retrieveClientById(clientId);
 
         RefreshToken token = new RefreshToken();
         token.setToken(refreshToken);
@@ -63,7 +59,7 @@
         token.setUserAuthenticationTime(userAuthenticationTime);
         token.setClient(client);
         token.setCreatedDate(now);
-        token.setExpiryDate(now.plusSeconds(config.getRefreshTokenExpiry()));
+        token.setExpiryDate(now.plusSeconds(client.getRefreshTokenExpiry()));
         token.setScopes(scopes);
 
         entityManager.persist(token);
diff --git a/full/src/main/java/de/ids_mannheim/korap/oauth2/dto/OAuth2ClientInfoDto.java b/full/src/main/java/de/ids_mannheim/korap/oauth2/dto/OAuth2ClientInfoDto.java
index 04dc175..69cf45f 100644
--- a/full/src/main/java/de/ids_mannheim/korap/oauth2/dto/OAuth2ClientInfoDto.java
+++ b/full/src/main/java/de/ids_mannheim/korap/oauth2/dto/OAuth2ClientInfoDto.java
@@ -2,8 +2,8 @@
 
 import com.fasterxml.jackson.annotation.JsonInclude;
 import com.fasterxml.jackson.annotation.JsonInclude.Include;
-import com.fasterxml.jackson.databind.JsonNode;
 import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.databind.JsonNode;
 
 import de.ids_mannheim.korap.exceptions.KustvaktException;
 import de.ids_mannheim.korap.oauth2.constant.OAuth2ClientType;
@@ -29,6 +29,8 @@
     private String registeredBy;
     @JsonProperty("registration_date")
     private String registrationDate;
+    @JsonProperty("refresh_token_expiry")
+    private int refreshTokenExpiry; // in seconds
     private OAuth2ClientType type;
     
     @JsonProperty("permitted")
@@ -52,6 +54,7 @@
         if (client.isSuper()) {
             this.isSuper = "true";
         }
+        this.refreshTokenExpiry = client.getRefreshTokenExpiry();
     }
 
     public String getId () {
@@ -138,5 +141,12 @@
     public void setPermitted (boolean isPermitted) {
         this.isPermitted = isPermitted;
     }
+    
+    public int getRefreshTokenExpiry () {
+        return refreshTokenExpiry;
+    }
+    public void setRefreshTokenExpiry (int refreshTokenExpiry) {
+        this.refreshTokenExpiry = refreshTokenExpiry;
+    }
 
 }
diff --git a/full/src/main/java/de/ids_mannheim/korap/oauth2/dto/OAuth2UserClientDto.java b/full/src/main/java/de/ids_mannheim/korap/oauth2/dto/OAuth2UserClientDto.java
index 59a3c56..8a9c6fd 100644
--- a/full/src/main/java/de/ids_mannheim/korap/oauth2/dto/OAuth2UserClientDto.java
+++ b/full/src/main/java/de/ids_mannheim/korap/oauth2/dto/OAuth2UserClientDto.java
@@ -16,7 +16,7 @@
  * @author margaretha
  *
  */
-@JsonInclude(Include.NON_EMPTY)
+@JsonInclude(Include.NON_DEFAULT)
 public class OAuth2UserClientDto {
     @JsonProperty("client_id")
     private String clientId;
@@ -32,6 +32,8 @@
     private String redirect_uri;
     @JsonProperty("registration_date")
     private String registrationDate;
+    @JsonProperty("refresh_token_expiry")
+    private int refreshTokenExpiry;
     
     private boolean permitted;
     private JsonNode source;
@@ -46,7 +48,9 @@
         this.setRedirect_uri(client.getRedirectURI());
         this.setRegistrationDate(client.getRegistrationDate().toString());
         this.setPermitted(client.isPermitted());
-        
+        if (client.getType().equals(OAuth2ClientType.CONFIDENTIAL)) {
+            this.setRefreshTokenExpiry(client.getRefreshTokenExpiry());
+        }
         String source = client.getSource();
         if (source!=null) {
             this.setSource(JsonUtils.readTree(client.getSource()));
@@ -122,4 +126,11 @@
     public void setRegistrationDate (String registrationDate) {
         this.registrationDate = registrationDate;
     }
+    
+    public int getRefreshTokenExpiry () {
+        return refreshTokenExpiry;
+    }
+    public void setRefreshTokenExpiry (int refreshTokenExpiry) {
+        this.refreshTokenExpiry = refreshTokenExpiry;
+    }
 }
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 b7d1031..62bb543 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
@@ -38,9 +38,8 @@
     private String registeredBy;
     @Column(name = "registration_date", updatable = false)
     private ZonedDateTime registrationDate;
-    
     @Column(name = "refresh_token_expiry")
-    private int refresTokenExpiry;
+    private int refreshTokenExpiry;
     private String description;
     private String url;
 
@@ -161,4 +160,11 @@
     public void setPermitted (boolean isPermitted) {
         this.isPermitted = isPermitted;
     }
+    
+    public int getRefreshTokenExpiry () {
+        return refreshTokenExpiry;
+    }
+    public void setRefreshTokenExpiry (int refreshTokenExpiry) {
+        this.refreshTokenExpiry = refreshTokenExpiry;
+    }
 }
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 786398f..b9e32c6 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
@@ -1,5 +1,7 @@
 package de.ids_mannheim.korap.oauth2.oltu.service;
 
+import java.net.InetAddress;
+import java.net.UnknownHostException;
 import java.time.ZoneId;
 import java.time.ZonedDateTime;
 import java.time.format.DateTimeFormatter;
@@ -97,6 +99,20 @@
 
     }
 
+    private boolean isInternalClient (String clientIpAddress) {
+        try {
+            InetAddress ip = InetAddress.getByName(clientIpAddress);
+            if (ip.isSiteLocalAddress()) {
+                return true;
+            }
+        }
+        catch (UnknownHostException e) {
+            // TODO Auto-generated catch block
+            e.printStackTrace();
+        }
+        return false;
+    }
+
     /**
      * Revokes all access token associated with the given refresh
      * token, and creates a new access token and a new refresh
@@ -170,8 +186,7 @@
 
         return createsAccessTokenResponse(scopes, requestedScopes, clientId,
                 refreshToken.getUserId(),
-                refreshToken.getUserAuthenticationTime(),
-                clientService.isPublicClient(oAuth2Client));
+                refreshToken.getUserAuthenticationTime(), oAuth2Client);
 
         // without new refresh token
         // return createsAccessTokenResponse(scopes, requestedScopes,
@@ -209,8 +224,7 @@
         OAuth2Client oAuth2Client = clientService.retrieveClient(clientId);
         return createsAccessTokenResponse(scopes, authorization.getScopes(),
                 authorization.getClientId(), authorization.getUserId(),
-                authorization.getUserAuthenticationTime(),
-                clientService.isPublicClient(oAuth2Client));
+                authorization.getUserAuthenticationTime(), oAuth2Client);
     }
 
     /**
@@ -279,8 +293,7 @@
         }
         
         return createsAccessTokenResponse(scopes, accessScopes, clientId,
-                username, authenticationTime,
-                false);
+                username, authenticationTime, client);
     }
 
     /**
@@ -327,7 +340,7 @@
         Set<AccessScope> accessScopes =
                 scopeService.convertToAccessScope(scopes);
         return createsAccessTokenResponse(scopes, accessScopes, clientId, null,
-                authenticationTime,clientService.isPublicClient(oAuth2Client));
+                authenticationTime,oAuth2Client);
     }
 
     /**
@@ -358,18 +371,22 @@
      */
     private OAuthResponse createsAccessTokenResponse (Set<String> scopes,
             Set<AccessScope> accessScopes, String clientId, String userId,
-            ZonedDateTime authenticationTime, boolean isPublicClient)
+            ZonedDateTime authenticationTime, OAuth2Client client)
             throws OAuthSystemException, KustvaktException {
 
         String random = randomGenerator.createRandomCode();
         random += randomGenerator.createRandomCode();
-        if (isPublicClient){
+        
+        if (clientService.isPublicClient(client)){
+            // refresh token == null, getAccessTokenLongExpiry
             return createsAccessTokenResponse(scopes, accessScopes, clientId,
                     userId, authenticationTime);
             }
         else {
-            RefreshToken refreshToken = refreshDao.storeRefreshToken(random, userId,
-                    authenticationTime, clientId, accessScopes);
+            // refresh token != null, getAccessTokenExpiry
+            // default refresh token expiry: 365 days in seconds
+            RefreshToken refreshToken = refreshDao.storeRefreshToken(random,
+                    userId, authenticationTime, client, accessScopes);
             return createsAccessTokenResponse(scopes, accessScopes, clientId,
                     userId, authenticationTime, refreshToken);
         }
diff --git a/full/src/main/java/de/ids_mannheim/korap/oauth2/openid/service/OpenIdTokenService.java b/full/src/main/java/de/ids_mannheim/korap/oauth2/openid/service/OpenIdTokenService.java
index 3da8545..85bb057 100644
--- a/full/src/main/java/de/ids_mannheim/korap/oauth2/openid/service/OpenIdTokenService.java
+++ b/full/src/main/java/de/ids_mannheim/korap/oauth2/openid/service/OpenIdTokenService.java
@@ -185,7 +185,7 @@
         Set<AccessScope> scopes = scopeService.convertToAccessScope(scopeSet);
         de.ids_mannheim.korap.oauth2.entity.RefreshToken rt =
                 refreshDao.storeRefreshToken(refreshToken.getValue(), username,
-                        authenticationTime, clientId.getValue(), scopes);
+                        authenticationTime, client, scopes);
         tokenDao.storeAccessToken(accessToken.getValue(), rt, scopes, username,
                 clientIdStr, authenticationTime);
 
@@ -236,11 +236,14 @@
         AccessToken accessToken =
                 new BearerAccessToken(config.getAccessTokenExpiry(), scope);
         RefreshToken refreshToken = new RefreshToken();
+        OAuth2Client client =
+                clientService.retrieveClient(authorization.getClientId());
+        
         de.ids_mannheim.korap.oauth2.entity.RefreshToken rt =
                 refreshDao.storeRefreshToken(refreshToken.getValue(),
                         authorization.getUserId(),
                         authorization.getUserAuthenticationTime(),
-                        authorization.getClientId(), scopes);
+                        client, scopes);
 
         tokenDao.storeAccessToken(accessToken.getValue(), rt, scopes,
                 authorization.getUserId(), authorization.getClientId(),
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 00f9e70..33ef04e 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
@@ -88,6 +88,7 @@
         try {
             ParameterChecker.checkNameValue(clientJson.getName(), "client_name");
             ParameterChecker.checkObjectValue(clientJson.getType(), "client_type");
+            ParameterChecker.checkStringValue(clientJson.getName(), "client_description");
         }
         catch (KustvaktException e) {
             throw new KustvaktException(e.getStatusCode(), e.getMessage(),
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 c52b7a7..0ea7f10 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
@@ -111,25 +111,6 @@
     }
     
     @Test
-    public void testRegisteredPublicClient () throws KustvaktException {
-        String clientName = "OAuth2DoryClient";
-        OAuth2ClientJson json = createOAuth2ClientJson(clientName,
-                OAuth2ClientType.PUBLIC, "Dory's client.");
-        registerClient("dory", json);
-
-        JsonNode node = listUserRegisteredClients("dory");
-        assertEquals(1, node.size());
-        assertEquals(clientName, node.at("/0/client_name").asText());
-        assertEquals(OAuth2ClientType.PUBLIC.name(),
-                node.at("/0/client_type").asText());
-        assertTrue(node.at("/0/permitted").asBoolean());
-        assertFalse(node.at("/0/registration_date").isMissingNode());
-        
-        String clientId = node.at("/0/client_id").asText();
-        testDeregisterPublicClient(clientId, "dory");
-    }
-
-    @Test
     public void testRegisterConfidentialClient () throws KustvaktException {
         ClientResponse response = registerConfidentialClient(username);
         String entity = response.getEntity(String.class);
@@ -141,6 +122,7 @@
         assertNotNull(clientSecret);
         assertFalse(clientId.contains("a"));
 
+        testListConfidentialClient(username, clientId);
         testConfidentialClientInfo(clientId, username);
         testResetConfidentialClientSecret(clientId, clientSecret);
         deregisterConfidentialClient(username, clientId);
@@ -587,6 +569,47 @@
     }
 
     @Test
+    public void testListPublicClient () throws KustvaktException {
+        String clientName = "OAuth2DoryClient";
+        OAuth2ClientJson json = createOAuth2ClientJson(clientName,
+                OAuth2ClientType.PUBLIC, "Dory's client.");
+        registerClient("dory", json);
+
+        JsonNode node = listUserRegisteredClients("dory");
+        assertEquals(1, node.size());
+        assertEquals(clientName, node.at("/0/client_name").asText());
+        assertEquals(OAuth2ClientType.PUBLIC.name(),
+                node.at("/0/client_type").asText());
+        assertTrue(node.at("/0/permitted").asBoolean());
+        assertFalse(node.at("/0/registration_date").isMissingNode());
+        assertTrue(node.at("/refresh_token_expiry").isMissingNode());
+        
+        String clientId = node.at("/0/client_id").asText();
+        testDeregisterPublicClient(clientId, "dory");
+    }
+    
+    private void testListConfidentialClient (String username, String clientId)
+            throws UniformInterfaceException, ClientHandlerException,
+            KustvaktException {
+        JsonNode node = listUserRegisteredClients(username);
+        assertEquals(1, node.size());
+        assertEquals(clientId, node.at("/0/client_id").asText());
+        assertEquals("OAuth2ClientTest", node.at("/0/client_name").asText());
+        assertEquals(OAuth2ClientType.CONFIDENTIAL.name(),
+                node.at("/0/client_type").asText());
+        assertNotNull(node.at("/0/client_description"));
+        assertEquals(clientURL, node.at("/0/client_url").asText());
+        assertEquals(clientRedirectUri,
+                node.at("/0/client_redirect_uri").asText());
+        assertNotNull(node.at("/0/registration_date"));
+
+        assertEquals(defaultRefreshTokenExpiry,
+                node.at("/0/refresh_token_expiry").asInt());
+        assertTrue(node.at("/0/permitted").asBoolean());
+        assertTrue(node.at("/0/source").isMissingNode());
+    }
+    
+    @Test
     public void testListUserClients () throws KustvaktException {
         String username = "pearl";
         String password = "pwd";
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 15e1954..0a2475c 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
@@ -5,6 +5,7 @@
 import static org.junit.Assert.assertTrue;
 
 import java.net.URI;
+import java.time.ZonedDateTime;
 import java.util.Set;
 
 import javax.ws.rs.core.MultivaluedMap;
@@ -333,7 +334,8 @@
         testRequestTokenWithUsedAuthorization(code);
 
         String refreshToken = node.at("/refresh_token").asText();
-
+        
+        testRefreshTokenExpiry(refreshToken);
         testRequestRefreshTokenInvalidScope(confidentialClientId, refreshToken);
         testRequestRefreshTokenInvalidClient(refreshToken);
         testRequestRefreshTokenInvalidRefreshToken(confidentialClientId);
@@ -446,11 +448,14 @@
                 node.at("/token_type").asText());
         assertNotNull(node.at("/expires_in").asText());
 
-        RefreshToken refreshToken = refreshTokenDao
-                .retrieveRefreshToken(node.at("/refresh_token").asText());
+        String refresh = node.at("/refresh_token").asText();
+        RefreshToken refreshToken =
+                refreshTokenDao.retrieveRefreshToken(refresh);
         Set<AccessScope> scopes = refreshToken.getScopes();
         assertEquals(1, scopes.size());
         assertEquals("[all]", scopes.toString());
+        
+        testRefreshTokenExpiry(refresh);
     }
 
     @Test
@@ -943,4 +948,11 @@
         node = requestTokenList(userAuthHeader, ACCESS_TOKEN_TYPE);
         assertEquals(0, node.size());
     }
+    
+    private void testRefreshTokenExpiry (String refreshToken)
+            throws KustvaktException {
+        RefreshToken token = refreshTokenDao.retrieveRefreshToken(refreshToken);
+        ZonedDateTime expiry = token.getCreatedDate().plusYears(1);
+        assertTrue(expiry.equals(token.getExpiryDate()));
+    }
 }
diff --git a/full/src/test/java/de/ids_mannheim/korap/web/controller/OAuth2PluginTest.java b/full/src/test/java/de/ids_mannheim/korap/web/controller/OAuth2PluginTest.java
index f5efc97..4bb0770 100644
--- a/full/src/test/java/de/ids_mannheim/korap/web/controller/OAuth2PluginTest.java
+++ b/full/src/test/java/de/ids_mannheim/korap/web/controller/OAuth2PluginTest.java
@@ -3,10 +3,10 @@
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
 
 import javax.ws.rs.core.MultivaluedMap;
 
-import org.apache.commons.codec.binary.Base64;
 import org.apache.http.entity.ContentType;
 import org.junit.Test;
 
@@ -68,13 +68,16 @@
         JsonNode clientInfo = retrieveClientInfo(clientId, username);
         assertEquals(clientId, clientInfo.at("/id").asText());
         assertEquals("Plugin", clientInfo.at("/name").asText());
+
         assertEquals(OAuth2ClientType.CONFIDENTIAL.name(),
                 clientInfo.at("/type").asText());
+        assertNotNull(clientInfo.at("/description").asText());
+        assertNotNull(clientInfo.at("/source").asText());
+        assertFalse(clientInfo.at("/permitted").asBoolean());
         assertEquals(username, clientInfo.at("/registered_by").asText());
         assertNotNull(clientInfo.at("/registration_date"));
-
-        assertFalse(clientInfo.at("/permitted").asBoolean());
-        assertNotNull(clientInfo.at("/source"));
+        assertEquals(defaultRefreshTokenExpiry,
+                clientInfo.at("/refresh_token_expiry").asInt());
     }
 
     private void testListUserRegisteredPlugins (String username,
@@ -91,6 +94,8 @@
         assertFalse(node.at("/0/permitted").asBoolean());
         assertFalse(node.at("/0/registration_date").isMissingNode());
         assertFalse(node.at("/0/source").isMissingNode());
+        assertEquals(defaultRefreshTokenExpiry,
+                node.at("/0/refresh_token_expiry").asInt());
     }
 
     @Test
@@ -190,6 +195,9 @@
         assertFalse(node.at("/0/permitted").isMissingNode());
         assertFalse(node.at("/0/registration_date").isMissingNode());
         assertFalse(node.at("/0/source").isMissingNode());
+        assertFalse(node.at("/0/refresh_token_expiry").isMissingNode());
+        
+        assertTrue(node.at("/1/refresh_token_expiry").isMissingNode());
     }
 
     private JsonNode listPlugins (boolean permitted_only)
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 67fe2d8..45f02b4 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
@@ -33,6 +33,7 @@
 import de.ids_mannheim.korap.oauth2.constant.OAuth2Error;
 import de.ids_mannheim.korap.oauth2.dao.RefreshTokenDao;
 import de.ids_mannheim.korap.utils.JsonUtils;
+import de.ids_mannheim.korap.utils.TimeUtils;
 import de.ids_mannheim.korap.web.input.OAuth2ClientJson;
 
 /**
@@ -59,6 +60,8 @@
     public static String ACCESS_TOKEN_TYPE = "access_token";
     public static String REFRESH_TOKEN_TYPE = "refresh_token";
 
+    protected int defaultRefreshTokenExpiry = TimeUtils.convertTimeToSeconds("365D");
+    
     protected String clientURL = "http://example.client.com";
     protected String clientRedirectUri = "https://example.client.com/redirect";
 
@@ -274,6 +277,9 @@
         assertEquals(clientURL, clientInfo.at("/url").asText());
         assertEquals(clientRedirectUri,
                 clientInfo.at("/redirect_uri").asText());
+        // 31536000 seconds
+        assertEquals(TimeUtils.convertTimeToSeconds("365D"),
+                clientInfo.at("/refresh_token_expiry").asInt());
         assertNotNull(clientInfo.at("/description"));
         assertNotNull(clientInfo.at("/registration_date"));
         assertTrue(clientInfo.at("/permitted").asBoolean());