Implemented refreshing OAuth2 access token

Change-Id: I559ba31a96965d5fc2594935f179b7b3f1f2d66a
diff --git a/full/Changes b/full/Changes
index 1715ded..d23bcc5 100644
--- a/full/Changes
+++ b/full/Changes
@@ -11,6 +11,7 @@
 	- Fixed authentication time in authentication controller (margaretha)
 	- Added OAuth2 access token tests (margaretha)
 	- Updated maven surefire setting for faster test suite runtime (margaretha)
+	- Implemented refreshing OAuth2 access token (margaretha)
 	
 version 0.60.4
 05/07/2018
diff --git a/full/src/main/java/de/ids_mannheim/korap/encryption/KustvaktEncryption.java b/full/src/main/java/de/ids_mannheim/korap/encryption/KustvaktEncryption.java
index 2ae1a32..735546c 100644
--- a/full/src/main/java/de/ids_mannheim/korap/encryption/KustvaktEncryption.java
+++ b/full/src/main/java/de/ids_mannheim/korap/encryption/KustvaktEncryption.java
@@ -179,7 +179,7 @@
     @Override
     public String createRandomNumber (Object ... obj) {
         final byte[] rNumber = SecureRGenerator
-                .getNextSecureRandom(SecureRGenerator.CORPUS_RANDOM_SIZE);
+                .getNextSecureRandom(SecureRGenerator.ID_RANDOM_SIZE);
         if (obj.length == 0) {
             obj = new Object[1];
             obj[0] = rNumber;
@@ -245,8 +245,8 @@
         private static final String SHA1_PRNG = "SHA1PRNG";
         protected static final int DEFAULT_RANDOM_SIZE = 128;
         protected static final int TOKEN_RANDOM_SIZE = 128;
-        protected static final int USERID_RANDOM_SIZE = 64;
-        protected static final int CORPUS_RANDOM_SIZE = 48;
+        protected static final int ID_RANDOM_SIZE = 128;
+        protected static final int CORPUS_RANDOM_SIZE = 64;
         private static final char[] HEX_DIGIT = { '0', '1', '2', '3', '4', '5',
                 '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', 'z', 'x',
                 'h', 'q', 'w' };
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 76daaab..9e96654 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
@@ -17,6 +17,7 @@
 import de.ids_mannheim.korap.config.KustvaktCacheable;
 import de.ids_mannheim.korap.exceptions.KustvaktException;
 import de.ids_mannheim.korap.exceptions.StatusCodes;
+import de.ids_mannheim.korap.oauth2.constant.OAuth2Error;
 import de.ids_mannheim.korap.oauth2.entity.AccessScope;
 import de.ids_mannheim.korap.oauth2.entity.AccessToken;
 import de.ids_mannheim.korap.oauth2.entity.AccessToken_;
@@ -50,14 +51,20 @@
         entityManager.persist(accessToken);
     }
 
-    public void storeAccessToken (String token, Set<AccessScope> scopes,
-            String userId, String clientId, ZonedDateTime authenticationTime)
-            throws KustvaktException {
+    public void storeAccessToken (String token, String refreshToken,
+            Set<AccessScope> scopes, String userId, String clientId,
+            ZonedDateTime authenticationTime) throws KustvaktException {
+        ParameterChecker.checkStringValue(token, "access token");
+        ParameterChecker.checkStringValue(refreshToken, "refresh token");
         ParameterChecker.checkObjectValue(scopes, "scopes");
+//        ParameterChecker.checkStringValue(userId, "username");
+        ParameterChecker.checkStringValue(clientId, "client_id");
         ParameterChecker.checkObjectValue(authenticationTime,
                 "authentication time");
+
         AccessToken accessToken = new AccessToken();
         accessToken.setToken(token);
+        accessToken.setRefreshToken(refreshToken);
         accessToken.setScopes(scopes);
         accessToken.setUserId(userId);
         accessToken.setClientId(clientId);
@@ -88,7 +95,42 @@
         }
         catch (NoResultException e) {
             throw new KustvaktException(StatusCodes.INVALID_ACCESS_TOKEN,
-                    "Access token is not found");
+                    "Access token is not found", OAuth2Error.INVALID_TOKEN);
         }
     }
+
+    public AccessToken retrieveAccessTokenByRefreshToken (String refreshToken)
+            throws KustvaktException {
+
+        CriteriaBuilder builder = entityManager.getCriteriaBuilder();
+        CriteriaQuery<AccessToken> query =
+                builder.createQuery(AccessToken.class);
+        Root<AccessToken> root = query.from(AccessToken.class);
+        query.select(root);
+        query.where(builder.equal(root.get(AccessToken_.refreshToken),
+                refreshToken));
+        Query q = entityManager.createQuery(query);
+        try {
+            AccessToken token = (AccessToken) q.getSingleResult();
+            return token;
+        }
+        catch (NoResultException e) {
+            throw new KustvaktException(StatusCodes.INVALID_REFRESH_TOKEN,
+                    "Refresh token is not found", OAuth2Error.INVALID_GRANT);
+        }
+
+    }
+
+    public AccessToken updateAccessToken (AccessToken accessToken)
+            throws KustvaktException {
+        ParameterChecker.checkObjectValue(accessToken, "access token");
+        AccessToken cachedToken =
+                (AccessToken) this.getCacheValue(accessToken.getId());
+        if (cachedToken != null) {
+            this.removeCacheEntry(cachedToken);
+        }
+
+        accessToken = entityManager.merge(accessToken);
+        return accessToken;
+    }
 }
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 ce7cfd8..e9aa596 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
@@ -23,7 +23,7 @@
 @Setter
 @Entity
 @Table(name = "oauth2_access_token")
-public class AccessToken implements Serializable{
+public class AccessToken implements Serializable {
 
     private static final long serialVersionUID = 8452701765986475302L;
 
@@ -41,11 +41,15 @@
     private boolean isRevoked;
     @Column(name = "user_auth_time", updatable = false)
     private ZonedDateTime userAuthenticationTime;
-    
-//    @OneToOne(fetch=FetchType.LAZY, cascade=CascadeType.REMOVE)
-//    @JoinColumn(name="authorization_id")
-//    private Authorization authorization;
-    
+    @Column(name = "refresh_token", updatable = false)
+    private String refreshToken;
+    @Column(name = "is_refresh_revoked")
+    private boolean isRefreshTokenRevoked;
+
+    // @OneToOne(fetch=FetchType.LAZY, cascade=CascadeType.REMOVE)
+    // @JoinColumn(name="authorization_id")
+    // private Authorization authorization;
+
     @ManyToMany(fetch = FetchType.EAGER)
     @JoinTable(name = "oauth2_access_token_scope",
             joinColumns = @JoinColumn(name = "token_id",
@@ -55,5 +59,5 @@
             uniqueConstraints = @UniqueConstraint(
                     columnNames = { "token_id", "scope_id" }))
     private Set<AccessScope> scopes;
-    
+
 }
diff --git a/full/src/main/java/de/ids_mannheim/korap/oauth2/oltu/service/OltuAuthorizationService.java b/full/src/main/java/de/ids_mannheim/korap/oauth2/oltu/service/OltuAuthorizationService.java
index 3a7a294..b05ed06 100644
--- a/full/src/main/java/de/ids_mannheim/korap/oauth2/oltu/service/OltuAuthorizationService.java
+++ b/full/src/main/java/de/ids_mannheim/korap/oauth2/oltu/service/OltuAuthorizationService.java
@@ -6,6 +6,7 @@
 
 import javax.servlet.http.HttpServletRequest;
 
+import org.apache.commons.codec.binary.Base64;
 import org.apache.oltu.oauth2.as.issuer.OAuthIssuer;
 import org.apache.oltu.oauth2.as.request.OAuthAuthzRequest;
 import org.apache.oltu.oauth2.as.response.OAuthASResponse;
@@ -70,8 +71,11 @@
 
         String scope, code;
         try {
-            code = oauthIssuer.authorizationCode();
             checkResponseType(authzRequest.getResponseType());
+
+            code = oauthIssuer.authorizationCode();
+//            code = Base64.encodeBase64String(code.getBytes());
+            
             scope = createAuthorization(username, authzRequest.getClientId(),
                     redirectUri, authzRequest.getScopes(), code,
                     authenticationTime, null);
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 e089ece..e8ad63b 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
@@ -6,6 +6,7 @@
 
 import javax.ws.rs.core.Response.Status;
 
+import org.apache.commons.codec.binary.Base64;
 import org.apache.oltu.oauth2.as.issuer.OAuthIssuer;
 import org.apache.oltu.oauth2.as.request.AbstractOAuthTokenRequest;
 import org.apache.oltu.oauth2.as.response.OAuthASResponse;
@@ -22,6 +23,7 @@
 import de.ids_mannheim.korap.oauth2.constant.OAuth2Error;
 import de.ids_mannheim.korap.oauth2.dao.AccessTokenDao;
 import de.ids_mannheim.korap.oauth2.entity.AccessScope;
+import de.ids_mannheim.korap.oauth2.entity.AccessToken;
 import de.ids_mannheim.korap.oauth2.entity.Authorization;
 import de.ids_mannheim.korap.oauth2.entity.OAuth2Client;
 import de.ids_mannheim.korap.oauth2.service.OAuth2TokenService;
@@ -56,6 +58,11 @@
                     oAuthRequest.getClientId(), oAuthRequest.getClientSecret(),
                     oAuthRequest.getScopes());
         }
+        else if (grantType.equals(GrantType.REFRESH_TOKEN.toString())) {
+            return requestAccessTokenWithRefreshToken(
+                    oAuthRequest.getRefreshToken(), oAuthRequest.getScopes(),
+                    oAuthRequest.getClientId(), oAuthRequest.getClientSecret());
+        }
         else {
             throw new KustvaktException(StatusCodes.UNSUPPORTED_GRANT_TYPE,
                     grantType + " is not supported.",
@@ -64,6 +71,53 @@
 
     }
 
+    private OAuthResponse requestAccessTokenWithRefreshToken (
+            String refreshToken, Set<String> scopes, String clientId,
+            String clientSecret)
+            throws KustvaktException, OAuthSystemException {
+
+        if (refreshToken == null || refreshToken.isEmpty()) {
+            throw new KustvaktException(StatusCodes.MISSING_PARAMETER,
+                    "Missing parameters: refresh_token",
+                    OAuth2Error.INVALID_REQUEST);
+        }
+
+        clientService.authenticateClient(clientId, clientSecret);
+        AccessToken accessToken =
+                tokenDao.retrieveAccessTokenByRefreshToken(refreshToken);
+
+        if (!clientId.equals(accessToken.getClientId())) {
+            throw new KustvaktException(StatusCodes.CLIENT_AUTHORIZATION_FAILED,
+                    "Client " + clientId + "is not authorized",
+                    OAuth2Error.INVALID_CLIENT);
+        }
+
+        if (accessToken.isRefreshTokenRevoked()) {
+            throw new KustvaktException(StatusCodes.INVALID_REFRESH_TOKEN,
+                    "Refresh token has been revoked.",
+                    OAuth2Error.INVALID_GRANT);
+        }
+        else if (ZonedDateTime.now(ZoneId.of(Attributes.DEFAULT_TIME_ZONE))
+                .isAfter(accessToken.getCreatedDate()
+                        .plusSeconds(config.getRefreshTokenExpiry()))) {
+            throw new KustvaktException(StatusCodes.INVALID_REFRESH_TOKEN,
+                    "Refresh token is expired.", OAuth2Error.INVALID_GRANT);
+        }
+
+        Set<AccessScope> requestedScopes = accessToken.getScopes();
+        if (scopes != null && !scopes.isEmpty()) {
+            requestedScopes = scopeService.verifyRefreshScope(scopes,
+                    accessToken.getScopes());
+        }
+
+        accessToken.setRefreshTokenRevoked(true);
+        accessToken = tokenDao.updateAccessToken(accessToken);
+
+        return createsAccessTokenResponse(scopes, requestedScopes, clientId,
+                accessToken.getUserId(),
+                accessToken.getUserAuthenticationTime());
+    }
+
     /**
      * Issues an access token for the specified client if the
      * authorization code is valid and client successfully
@@ -197,12 +251,28 @@
     }
 
     /**
-     * Creates an OAuthResponse containing an access token and a
-     * refresh token with type Bearer.
+     * Creates an OAuthResponse containing an access token of type
+     * Bearer. By default, MD generator is used to generates access
+     * token of 128 bit values, represented in hexadecimal comprising
+     * 32 bytes. The generated value is subsequently encoded in
+     * Base64.
      * 
+     * <br /><br />
+     * Additionally, a refresh token is issued. It can be used to
+     * request a new access token without requiring user
+     * reauthentication.
+     * 
+     * @param scopes
+     *            a set of access token scopes in String
+     * @param accessScopes
+     *            a set of access token scopes in {@link AccessScope}
+     * @param clientId
+     *            a client id
+     * @param userId
+     *            a user id
      * @param authenticationTime
-     * 
-     * @return an OAuthResponse containing an access token
+     *            the user authentication time
+     * @return an {@link OAuthResponse}
      * @throws OAuthSystemException
      * @throws KustvaktException
      */
@@ -212,17 +282,19 @@
             throws OAuthSystemException, KustvaktException {
 
         String accessToken = oauthIssuer.accessToken();
-        // String refreshToken = oauthIssuer.refreshToken();
+//        accessToken = Base64.encodeBase64String(accessToken.getBytes());
 
-        tokenDao.storeAccessToken(accessToken, accessScopes, userId, clientId,
-                authenticationTime);
+        String refreshToken = oauthIssuer.refreshToken();
+//        refreshToken = Base64.encodeBase64String(refreshToken.getBytes());
+
+        tokenDao.storeAccessToken(accessToken, refreshToken, accessScopes,
+                userId, clientId, authenticationTime);
 
         return OAuthASResponse.tokenResponse(Status.OK.getStatusCode())
                 .setAccessToken(accessToken)
                 .setTokenType(TokenType.BEARER.toString())
                 .setExpiresIn(String.valueOf(config.getTokenTTL()))
-                // .setRefreshToken(refreshToken)
+                .setRefreshToken(refreshToken)
                 .setScope(String.join(" ", scopes)).buildJSONMessage();
     }
-
 }
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 91a51bf..45735ed 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
@@ -176,12 +176,15 @@
         AccessToken accessToken =
                 new BearerAccessToken(config.getTokenTTL(), scope);
 
+        RefreshToken refreshToken = new RefreshToken();
+
         tokenDao.storeAccessToken(accessToken.getValue(),
+                refreshToken.getValue(),
                 scopeService.convertToAccessScope(scopeSet), username,
                 clientIdStr, authenticationTime);
 
-        return createsAccessTokenResponse(accessToken, scope, clientIdStr,
-                username, authenticationTime, null);
+        return createsAccessTokenResponse(accessToken, refreshToken, scope,
+                clientIdStr, username, authenticationTime, null);
     }
 
     private AccessTokenResponse requestAccessTokenWithAuthorizationCode (
@@ -226,23 +229,25 @@
         Scope scope = new Scope(scopeArray);
         AccessToken accessToken =
                 new BearerAccessToken(config.getTokenTTL(), scope);
-        tokenDao.storeAccessToken(accessToken.getValue(), scopes,
-                authorization.getUserId(), authorization.getClientId(),
+        RefreshToken refreshToken = new RefreshToken();
+
+        tokenDao.storeAccessToken(accessToken.getValue(),
+                refreshToken.getValue(), scopes, authorization.getUserId(),
+                authorization.getClientId(),
                 authorization.getUserAuthenticationTime());
 
-        return createsAccessTokenResponse(accessToken, scope,
+        return createsAccessTokenResponse(accessToken, refreshToken, scope,
                 authorization.getClientId(), authorization.getUserId(),
                 authorization.getUserAuthenticationTime(),
                 authorization.getNonce());
     }
 
     private AccessTokenResponse createsAccessTokenResponse (
-            AccessToken accessToken, Scope scope, String clientId,
-            String userId, ZonedDateTime userAuthenticationTime, String nonce)
+            AccessToken accessToken, RefreshToken refreshToken, Scope scope,
+            String clientId, String userId,
+            ZonedDateTime userAuthenticationTime, String nonce)
             throws KustvaktException {
 
-        RefreshToken refreshToken = new RefreshToken();
-
         if (scope.contains("openid")) {
             JWTClaimsSet claims = createIdTokenClaims(clientId, userId,
                     userAuthenticationTime, nonce);
diff --git a/full/src/main/java/de/ids_mannheim/korap/oauth2/service/OAuth2ScopeService.java b/full/src/main/java/de/ids_mannheim/korap/oauth2/service/OAuth2ScopeService.java
index 6f1ce49..c820a30 100644
--- a/full/src/main/java/de/ids_mannheim/korap/oauth2/service/OAuth2ScopeService.java
+++ b/full/src/main/java/de/ids_mannheim/korap/oauth2/service/OAuth2ScopeService.java
@@ -30,7 +30,7 @@
 
     @Autowired
     private AdminDao adminDao;
-    
+
     /**
      * Converts a set of scope strings to a set of {@link AccessScope}
      * 
@@ -100,4 +100,29 @@
             }
         }
     }
+
+    /**
+     * Verify scopes given in a refresh request. The scopes must not
+     * include other scopes than those authorized in the original
+     * access token issued together with the refresh token.
+     * 
+     * @param requestScopes
+     *            requested scopes
+     * @param originalScopes
+     *            authorized scopes
+     * @return a set of requested {@link AccessScope}
+     * @throws KustvaktException
+     */
+    public Set<AccessScope> verifyRefreshScope (Set<String> requestScopes,
+            Set<AccessScope> originalScopes) throws KustvaktException {
+        Set<AccessScope> requestedScopes = convertToAccessScope(requestScopes);
+        for (AccessScope scope : requestedScopes) {
+            if (!originalScopes.contains(scope)) {
+                throw new KustvaktException(StatusCodes.INVALID_SCOPE,
+                        "Scope " + scope.getId() + " is not authorized.",
+                        OAuth2Error.INVALID_SCOPE);
+            }
+        }
+        return requestedScopes;
+    }
 }
diff --git a/full/src/main/java/de/ids_mannheim/korap/web/controller/OAuth2Controller.java b/full/src/main/java/de/ids_mannheim/korap/web/controller/OAuth2Controller.java
index aff9e19..4ab46b4 100644
--- a/full/src/main/java/de/ids_mannheim/korap/web/controller/OAuth2Controller.java
+++ b/full/src/main/java/de/ids_mannheim/korap/web/controller/OAuth2Controller.java
@@ -75,8 +75,7 @@
     @Produces(MediaType.APPLICATION_JSON + ";charset=utf-8")
     public Response requestAuthorizationCode (
             @Context HttpServletRequest request,
-            @Context SecurityContext context,
-            @FormParam("state") String state,
+            @Context SecurityContext context, @FormParam("state") String state,
             MultivaluedMap<String, String> form) {
 
         TokenContext tokenContext = (TokenContext) context.getUserPrincipal();
@@ -159,8 +158,9 @@
             MultivaluedMap<String, String> form) {
 
         try {
+            boolean grantTypeExist = grantType != null && !grantType.isEmpty();
             AbstractOAuthTokenRequest oAuthRequest = null;
-            if (grantType != null && !grantType.isEmpty() && grantType
+            if (grantTypeExist && grantType
                     .equals(GrantType.CLIENT_CREDENTIALS.toString())) {
                 oAuthRequest = new OAuthTokenRequest(
                         new FormRequestWrapper(request, form));
diff --git a/full/src/main/resources/db/new-mysql/V1.4__oauth2_tables.sql b/full/src/main/resources/db/new-mysql/V1.4__oauth2_tables.sql
index b8df487..4c35fca 100644
--- a/full/src/main/resources/db/new-mysql/V1.4__oauth2_tables.sql
+++ b/full/src/main/resources/db/new-mysql/V1.4__oauth2_tables.sql
@@ -59,6 +59,8 @@
 	created_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
 	is_revoked BOOLEAN DEFAULT 0,
 	user_auth_time TIMESTAMP NULL,
+    refresh_token VARCHAR(255) DEFAULT NULL,
+	is_refresh_revoked BOOLEAN DEFAULT 0,
 	FOREIGN KEY (client_id)
 	   REFERENCES oauth2_client(id)
 );
diff --git a/full/src/main/resources/db/new-sqlite/V1.4__oauth2_tables.sql b/full/src/main/resources/db/new-sqlite/V1.4__oauth2_tables.sql
index 2310c12..dff4df8 100644
--- a/full/src/main/resources/db/new-sqlite/V1.4__oauth2_tables.sql
+++ b/full/src/main/resources/db/new-sqlite/V1.4__oauth2_tables.sql
@@ -64,6 +64,8 @@
 	created_date TIMESTAMP DEFAULT (datetime('now','localtime')),
 	is_revoked BOOLEAN DEFAULT 0,
 	user_auth_time TIMESTAMP NOT NULL,
+	refresh_token VARCHAR(255) DEFAULT NULL,
+	is_refresh_revoked BOOLEAN DEFAULT 0,
 	FOREIGN KEY (client_id)
 	   REFERENCES oauth2_client(id)
 );
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 a0b143b..7fb5d4c 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
@@ -8,8 +8,10 @@
 import javax.ws.rs.core.MultivaluedMap;
 import javax.ws.rs.core.Response.Status;
 
+import org.apache.commons.codec.binary.Base64;
 import org.apache.http.entity.ContentType;
 import org.apache.oltu.oauth2.common.error.OAuthError;
+import org.apache.oltu.oauth2.common.message.types.GrantType;
 import org.apache.oltu.oauth2.common.message.types.TokenType;
 import org.junit.Test;
 import org.springframework.util.MultiValueMap;
@@ -316,7 +318,6 @@
 
         JsonNode node = JsonUtils.readTree(entity);
         assertNotNull(node.at("/access_token").asText());
-        assertNotNull(node.at("/refresh_token").asText());
         assertEquals(TokenType.BEARER.toString(),
                 node.at("/token_type").asText());
         assertNotNull(node.at("/expires_in").asText());
@@ -420,21 +421,30 @@
     @Test
     public void testRequestTokenPasswordGrantPublic ()
             throws KustvaktException {
+        String clientId = "iBr3LsTCxOj7D2o0A5m";
         MultivaluedMap<String, String> form = new MultivaluedMapImpl();
         form.add("grant_type", "password");
         form.add("username", "dory");
         form.add("password", "password");
-        form.add("client_id", "iBr3LsTCxOj7D2o0A5m");
+        form.add("client_id", clientId);
 
         ClientResponse response = requestToken(form);
         String entity = response.getEntity(String.class);
 
+        assertEquals(Status.OK.getStatusCode(), response.getStatus());
+
         JsonNode node = JsonUtils.readTree(entity);
         assertNotNull(node.at("/access_token").asText());
-        assertNotNull(node.at("/refresh_token").asText());
         assertEquals(TokenType.BEARER.toString(),
                 node.at("/token_type").asText());
         assertNotNull(node.at("/expires_in").asText());
+
+        String refreshToken = node.at("/refresh_token").asText();
+        testRequestRefreshTokenInvalidScope(clientId, refreshToken);
+        testRequestRefreshTokenPublicClient(clientId, refreshToken);
+        testRequestRefreshTokenInvalidClient(refreshToken);
+        testRequestRefreshTokenInvalidRefreshToken(clientId);
+        testRevokedRefreshToken(clientId, refreshToken);
     }
 
     @Test
@@ -538,4 +548,101 @@
                 node.get("error").asText());
     }
 
+    private void testRequestRefreshTokenInvalidScope (String clientId,
+            String refreshToken) throws KustvaktException {
+        MultivaluedMap<String, String> form = new MultivaluedMapImpl();
+        form.add("grant_type", GrantType.REFRESH_TOKEN.toString());
+        form.add("client_id", clientId);
+        form.add("refresh_token", refreshToken);
+        form.add("scope", "search serialize_query");
+
+        ClientResponse response = resource().path("oauth2").path("token")
+                .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
+                .header(HttpHeaders.CONTENT_TYPE,
+                        ContentType.APPLICATION_FORM_URLENCODED)
+                .entity(form).post(ClientResponse.class);
+
+        String entity = response.getEntity(String.class);
+        JsonNode node = JsonUtils.readTree(entity);
+        assertEquals(OAuth2Error.INVALID_SCOPE, node.at("/error").asText());
+    }
+
+    private void testRequestRefreshTokenPublicClient (String clientId,
+            String refreshToken) throws KustvaktException {
+        MultivaluedMap<String, String> form = new MultivaluedMapImpl();
+        form.add("grant_type", GrantType.REFRESH_TOKEN.toString());
+        form.add("client_id", clientId);
+        form.add("refresh_token", refreshToken);
+
+        ClientResponse response = resource().path("oauth2").path("token")
+                .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
+                .header(HttpHeaders.CONTENT_TYPE,
+                        ContentType.APPLICATION_FORM_URLENCODED)
+                .entity(form).post(ClientResponse.class);
+
+        String entity = response.getEntity(String.class);
+        assertEquals(Status.OK.getStatusCode(), response.getStatus());
+
+        JsonNode node = JsonUtils.readTree(entity);
+        assertNotNull(node.at("/access_token").asText());
+        assertNotNull(node.at("/refresh_token").asText());
+        assertEquals(TokenType.BEARER.toString(),
+                node.at("/token_type").asText());
+        assertNotNull(node.at("/expires_in").asText());
+    }
+
+    private void testRequestRefreshTokenInvalidClient (String refreshToken)
+            throws KustvaktException {
+        MultivaluedMap<String, String> form = new MultivaluedMapImpl();
+        form.add("grant_type", GrantType.REFRESH_TOKEN.toString());
+        form.add("client_id", "8bIDtZnH6NvRkW2Fq");
+        form.add("refresh_token", refreshToken);
+
+        ClientResponse response = resource().path("oauth2").path("token")
+                .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
+                .header(HttpHeaders.CONTENT_TYPE,
+                        ContentType.APPLICATION_FORM_URLENCODED)
+                .entity(form).post(ClientResponse.class);
+
+        String entity = response.getEntity(String.class);
+        JsonNode node = JsonUtils.readTree(entity);
+        assertEquals(OAuth2Error.INVALID_CLIENT, node.at("/error").asText());
+    }
+
+    private void testRequestRefreshTokenInvalidRefreshToken (String clientId)
+            throws KustvaktException {
+        MultivaluedMap<String, String> form = new MultivaluedMapImpl();
+        form.add("grant_type", GrantType.REFRESH_TOKEN.toString());
+        form.add("client_id", clientId);
+        form.add("refresh_token", "Lia8s8w8tJeZSBlaQDrYV8ion3l");
+
+        ClientResponse response = resource().path("oauth2").path("token")
+                .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
+                .header(HttpHeaders.CONTENT_TYPE,
+                        ContentType.APPLICATION_FORM_URLENCODED)
+                .entity(form).post(ClientResponse.class);
+
+        String entity = response.getEntity(String.class);
+        JsonNode node = JsonUtils.readTree(entity);
+        assertEquals(OAuth2Error.INVALID_GRANT, node.at("/error").asText());
+    }
+
+    private void testRevokedRefreshToken (String clientId, String refreshToken)
+            throws KustvaktException {
+        MultivaluedMap<String, String> form = new MultivaluedMapImpl();
+        form.add("grant_type", GrantType.REFRESH_TOKEN.toString());
+        form.add("client_id", clientId);
+        form.add("refresh_token", refreshToken);
+
+        ClientResponse response = resource().path("oauth2").path("token")
+                .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
+                .header(HttpHeaders.CONTENT_TYPE,
+                        ContentType.APPLICATION_FORM_URLENCODED)
+                .entity(form).post(ClientResponse.class);
+
+        String entity = response.getEntity(String.class);
+        JsonNode node = JsonUtils.readTree(entity);
+        assertEquals(OAuth2Error.INVALID_GRANT, node.at("/error").asText());
+    }
+
 }
diff --git a/full/src/test/java/de/ids_mannheim/korap/web/controller/OAuth2OpenIdControllerTest.java b/full/src/test/java/de/ids_mannheim/korap/web/controller/OAuth2OpenIdControllerTest.java
index 1bea9d3..eb716ab 100644
--- a/full/src/test/java/de/ids_mannheim/korap/web/controller/OAuth2OpenIdControllerTest.java
+++ b/full/src/test/java/de/ids_mannheim/korap/web/controller/OAuth2OpenIdControllerTest.java
@@ -372,7 +372,6 @@
 
         ClientResponse tokenResponse = sendTokenRequest(tokenForm);
         String entity = tokenResponse.getEntity(String.class);
-
         JsonNode node = JsonUtils.readTree(entity);
         assertNotNull(node.at("/access_token").asText());
         assertNotNull(node.at("/refresh_token").asText());