Added an admin API to delete expired/revoked access and refresh tokens.

Change-Id: Ie939a6aa26edf2747981943e5f40daa3485be461
diff --git a/full/Changes b/full/Changes
index 0a6469c..653149b 100644
--- a/full/Changes
+++ b/full/Changes
@@ -7,6 +7,8 @@
  - Added more parameter checks and OAuth2Client web-service tests.
 2022-03-17
  - Updated admin filter by using admin token and role checks.
+2022-03-18
+ - Added an OAuth2 admin API to delete expired/revoked access and refresh tokens.
 
 # version 0.65.1
 
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 6684a53..6d8dd43 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
@@ -57,7 +57,8 @@
             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");
@@ -69,7 +70,7 @@
 
         ZonedDateTime expiry;
         AccessToken accessToken = new AccessToken();
-        
+
         if (refreshToken != null) {
             accessToken.setRefreshToken(refreshToken);
             expiry = now.plusSeconds(config.getAccessTokenExpiry());
@@ -77,9 +78,9 @@
         else {
             expiry = now.plusSeconds(config.getAccessTokenLongExpiry());
         }
-        
+
         OAuth2Client client = clientDao.retrieveClientById(clientId);
-        
+
         accessToken.setCreatedDate(now);
         accessToken.setExpiryDate(expiry);
         accessToken.setToken(token);
@@ -142,11 +143,11 @@
         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(condition);
         Query q = entityManager.createQuery(query);
@@ -160,31 +161,31 @@
         }
     }
 
-    
     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()){
+
+        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();
     }
 
-    public List<AccessToken> retrieveAccessTokenByUser (String username, String clientId)
-            throws KustvaktException {
+    public List<AccessToken> retrieveAccessTokenByUser (String username,
+            String clientId) throws KustvaktException {
         ParameterChecker.checkStringValue(username, "username");
 
         CriteriaBuilder builder = entityManager.getCriteriaBuilder();
@@ -205,7 +206,31 @@
             condition = builder.and(condition,
                     builder.equal(root.get(AccessToken_.client), client));
         }
-            
+
+        query.select(root);
+        query.where(condition);
+        TypedQuery<AccessToken> q = entityManager.createQuery(query);
+        return q.getResultList();
+    }
+
+    public void deleteInvalidAccessTokens () {
+        List<AccessToken> invalidAccessTokens = retrieveInvalidAccessTokens();
+        invalidAccessTokens.forEach(token -> entityManager.remove(token));
+    }
+    
+    public List<AccessToken> retrieveInvalidAccessTokens () {
+        CriteriaBuilder builder = entityManager.getCriteriaBuilder();
+        CriteriaQuery<AccessToken> query =
+                builder.createQuery(AccessToken.class);
+
+        Root<AccessToken> root = query.from(AccessToken.class);
+        Predicate condition = builder.or(
+                builder.equal(root.get(AccessToken_.isRevoked), true),
+                builder.lessThan(
+                        root.<ZonedDateTime> get(AccessToken_.expiryDate),
+                        ZonedDateTime
+                                .now(ZoneId.of(Attributes.DEFAULT_TIME_ZONE))));
+
         query.select(root);
         query.where(condition);
         TypedQuery<AccessToken> q = entityManager.createQuery(query);
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 2367fe3..7ff6138 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
@@ -124,7 +124,7 @@
             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);
@@ -144,8 +144,7 @@
     }
 
     public List<RefreshToken> retrieveRefreshTokenByUser (String username,
-            String clientId)
-            throws KustvaktException {
+            String clientId) throws KustvaktException {
         ParameterChecker.checkStringValue(username, "username");
 
         CriteriaBuilder builder = entityManager.getCriteriaBuilder();
@@ -166,11 +165,35 @@
             condition = builder.and(condition,
                     builder.equal(root.get(RefreshToken_.client), client));
         }
-        
+
         query.select(root);
         query.where(condition);
         TypedQuery<RefreshToken> q = entityManager.createQuery(query);
         return q.getResultList();
     }
+
+    public void deleteInvalidRefreshTokens () {
+        List<RefreshToken> invalidRefreshTokens = retrieveInvalidRefreshTokens();
+        invalidRefreshTokens.forEach(token -> entityManager.remove(token));
+    }
     
+    public List<RefreshToken> retrieveInvalidRefreshTokens () {
+        CriteriaBuilder builder = entityManager.getCriteriaBuilder();
+        CriteriaQuery<RefreshToken> query =
+                builder.createQuery(RefreshToken.class);
+
+        Root<RefreshToken> root = query.from(RefreshToken.class);
+        Predicate condition = builder.or(
+                builder.equal(root.get(RefreshToken_.isRevoked), true),
+                builder.lessThan(
+                        root.<ZonedDateTime> get(RefreshToken_.expiryDate),
+                        ZonedDateTime
+                                .now(ZoneId.of(Attributes.DEFAULT_TIME_ZONE))));
+
+        query.select(root);
+        query.where(condition);
+        TypedQuery<RefreshToken> q = entityManager.createQuery(query);
+        return q.getResultList();
+    }
+
 }
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 8741553..6bbed24 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
@@ -40,7 +40,6 @@
 import de.ids_mannheim.korap.oauth2.oltu.OAuth2RevokeTokenSuperRequest;
 import de.ids_mannheim.korap.oauth2.service.OAuth2ClientService;
 import de.ids_mannheim.korap.oauth2.service.OAuth2TokenService;
-import de.ids_mannheim.korap.utils.TimeUtils;
 
 /** Implementation of token service using Apache Oltu.
  * 
diff --git a/full/src/main/java/de/ids_mannheim/korap/oauth2/service/OAuth2AdminService.java b/full/src/main/java/de/ids_mannheim/korap/oauth2/service/OAuth2AdminService.java
new file mode 100644
index 0000000..8000b64
--- /dev/null
+++ b/full/src/main/java/de/ids_mannheim/korap/oauth2/service/OAuth2AdminService.java
@@ -0,0 +1,24 @@
+package de.ids_mannheim.korap.oauth2.service;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import de.ids_mannheim.korap.oauth2.dao.AccessTokenDao;
+import de.ids_mannheim.korap.oauth2.dao.RefreshTokenDao;
+
+@Service
+public class OAuth2AdminService {
+
+    @Autowired
+    private AccessTokenDao tokenDao;
+    @Autowired
+    private RefreshTokenDao refreshDao;
+ 
+    
+    public void cleanTokens () {
+        tokenDao.deleteInvalidAccessTokens();
+        refreshDao.deleteInvalidRefreshTokens();
+    }
+
+    
+}
diff --git a/full/src/main/java/de/ids_mannheim/korap/web/controller/OAuth2AdminController.java b/full/src/main/java/de/ids_mannheim/korap/web/controller/OAuth2AdminController.java
new file mode 100644
index 0000000..336f1a8
--- /dev/null
+++ b/full/src/main/java/de/ids_mannheim/korap/web/controller/OAuth2AdminController.java
@@ -0,0 +1,56 @@
+package de.ids_mannheim.korap.web.controller;
+
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.SecurityContext;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Controller;
+
+import com.sun.jersey.spi.container.ResourceFilters;
+
+import de.ids_mannheim.korap.constant.OAuth2Scope;
+import de.ids_mannheim.korap.exceptions.KustvaktException;
+import de.ids_mannheim.korap.oauth2.service.OAuth2AdminService;
+import de.ids_mannheim.korap.oauth2.service.OAuth2ScopeService;
+import de.ids_mannheim.korap.security.context.TokenContext;
+import de.ids_mannheim.korap.web.OAuth2ResponseHandler;
+import de.ids_mannheim.korap.web.filter.APIVersionFilter;
+import de.ids_mannheim.korap.web.filter.AdminFilter;
+import de.ids_mannheim.korap.web.filter.AuthenticationFilter;
+import de.ids_mannheim.korap.web.filter.BlockingFilter;
+
+@Controller
+@Path("{version}/oauth2/admin")
+@ResourceFilters({ APIVersionFilter.class, AuthenticationFilter.class,
+        BlockingFilter.class, AdminFilter.class })
+public class OAuth2AdminController {
+
+    @Autowired
+    private OAuth2AdminService adminService;
+    @Autowired
+    private OAuth2ScopeService scopeService;
+    @Autowired
+    private OAuth2ResponseHandler responseHandler;
+    
+    @POST
+    @Path("/token/clean")
+    public Response cleanExpiredInvalidToken (String token,
+            @Context SecurityContext securityContext) {
+
+        TokenContext context =
+                (TokenContext) securityContext.getUserPrincipal();
+        
+        try {
+            scopeService.verifyScope(context, OAuth2Scope.ADMIN);
+            adminService.cleanTokens();
+            
+        }
+        catch (KustvaktException e) {
+            throw responseHandler.throwit(e);
+        }
+        return Response.ok().build();
+    }
+}
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 fb70f79..858d238 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
@@ -50,13 +50,13 @@
 
 INSERT INTO oauth2_access_token(token,user_id,created_date, 
 expiry_date, user_auth_time)
-VALUES("fia0123ikBWn931470H8s5gRqx7Moc4p","marlin","2018-05-30 16:25:50", 
-"2018-05-31 16:25:50", "2018-05-30 16:23:10");
+VALUES("fia0123ikBWn931470H8s5gRqx7Moc4p","marlin",1527776750000, 
+1527776750000, 1527690190000);
 
 INSERT INTO oauth2_refresh_token(token,user_id,user_auth_time, 
 created_date, expiry_date, client)
-VALUES("js9iQ4lw1Ri7fz06l0dXl8fCVp3Yn7vmq8","pearl","2017-05-30 16:25:50", 
-"2017-05-31 16:26:35", 1527784020000, "nW5qM63Rb2a7KdT9L");
+VALUES("js9iQ4lw1Ri7fz06l0dXl8fCVp3Yn7vmq8","pearl",1496154350000, 
+1496240795000, 1527784020000, "nW5qM63Rb2a7KdT9L");
 
 -- EM: expiry date must be in epoch milis format for testing with sqlite,
 -- on the contrary, for testing using mysql use this format: "2018-05-31 16:27:00"
diff --git a/full/src/test/java/de/ids_mannheim/korap/web/controller/OAuth2AdminControllerTest.java b/full/src/test/java/de/ids_mannheim/korap/web/controller/OAuth2AdminControllerTest.java
new file mode 100644
index 0000000..50ab44d
--- /dev/null
+++ b/full/src/test/java/de/ids_mannheim/korap/web/controller/OAuth2AdminControllerTest.java
@@ -0,0 +1,45 @@
+package de.ids_mannheim.korap.web.controller;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import org.junit.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+
+import com.sun.jersey.api.client.ClientResponse;
+
+import de.ids_mannheim.korap.authentication.http.HttpAuthorizationHandler;
+import de.ids_mannheim.korap.config.Attributes;
+import de.ids_mannheim.korap.exceptions.KustvaktException;
+import de.ids_mannheim.korap.oauth2.dao.AccessTokenDao;
+import de.ids_mannheim.korap.oauth2.dao.RefreshTokenDao;
+
+public class OAuth2AdminControllerTest extends OAuth2TestBase {
+
+    private String adminAuthHeader;
+    @Autowired
+    private RefreshTokenDao refreshDao;
+    @Autowired
+    private AccessTokenDao accessDao;
+
+    public OAuth2AdminControllerTest () throws KustvaktException {
+        adminAuthHeader = HttpAuthorizationHandler
+                .createBasicAuthorizationHeaderValue("admin", "password");
+    }
+
+    @Test
+    public void testCleanTokens () {
+        int refreshTokensBefore =
+                refreshDao.retrieveInvalidRefreshTokens().size();
+        assertTrue(refreshTokensBefore > 0);
+
+        int accessTokensBefore = accessDao.retrieveInvalidAccessTokens().size();
+        assertTrue(accessTokensBefore > 0);
+
+        resource().path(API_VERSION).path("oauth2").path("admin").path("token")
+                .path("clean").header(Attributes.AUTHORIZATION, adminAuthHeader)
+                .entity("token=adminToken").post(ClientResponse.class);
+        assertEquals(0, refreshDao.retrieveInvalidRefreshTokens().size());
+        assertEquals(0, accessDao.retrieveInvalidAccessTokens().size());
+    }
+}