Implemented OAuth2 revoke token.
Change-Id: I896aeb759741395c1516772fea15886bb2470cdf
diff --git a/full/Changes b/full/Changes
index 081208d..bba0520 100644
--- a/full/Changes
+++ b/full/Changes
@@ -1,7 +1,9 @@
version 0.61.0
-02/08/2018
+03/08/2018
- Added VC referencing tests (margaretha)
- Implemented loading and caching named VCs (margaretha)
+ - Implemented OAuth2 revoke token (margaretha)
+ - Updated OAuth2 refresh token implementation (margaretha)
version 0.60.5
02/08/2018
diff --git a/full/src/main/java/de/ids_mannheim/korap/authentication/OAuth2Authentication.java b/full/src/main/java/de/ids_mannheim/korap/authentication/OAuth2Authentication.java
index 8f5e702..1b42d3e 100644
--- a/full/src/main/java/de/ids_mannheim/korap/authentication/OAuth2Authentication.java
+++ b/full/src/main/java/de/ids_mannheim/korap/authentication/OAuth2Authentication.java
@@ -34,7 +34,8 @@
AccessToken accessToken = accessDao.retrieveAccessToken(authToken);
if (accessToken.isRevoked()) {
- throw new KustvaktException(StatusCodes.EXPIRED);
+ throw new KustvaktException(StatusCodes.INVALID_ACCESS_TOKEN,
+ "Access token has been revoked");
}
ZonedDateTime expiry = accessToken.getCreatedDate()
diff --git a/full/src/main/java/de/ids_mannheim/korap/dao/AnnotationDao.java b/full/src/main/java/de/ids_mannheim/korap/dao/AnnotationDao.java
index afc360f..22f2f37 100644
--- a/full/src/main/java/de/ids_mannheim/korap/dao/AnnotationDao.java
+++ b/full/src/main/java/de/ids_mannheim/korap/dao/AnnotationDao.java
@@ -36,6 +36,7 @@
*
* @return a list of foundry-layer pairs.
*/
+ @SuppressWarnings("unchecked")
public List<AnnotationPair> getAllFoundryLayerPairs () {
CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
CriteriaQuery<AnnotationPair> query =
@@ -61,6 +62,7 @@
* a layer code
* @return a list of foundry-layer pairs.
*/
+ @SuppressWarnings("unchecked")
public List<AnnotationPair> getAnnotationDescriptions (String foundry,
String layer) {
diff --git a/full/src/main/java/de/ids_mannheim/korap/dao/PrivilegeDao.java b/full/src/main/java/de/ids_mannheim/korap/dao/PrivilegeDao.java
index 8bd533d..80a52a2 100644
--- a/full/src/main/java/de/ids_mannheim/korap/dao/PrivilegeDao.java
+++ b/full/src/main/java/de/ids_mannheim/korap/dao/PrivilegeDao.java
@@ -54,6 +54,7 @@
}
}
+ @SuppressWarnings("unchecked")
public List<Privilege> retrievePrivilegeByRoleId (int roleId) {
CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
CriteriaQuery<Privilege> query =
diff --git a/full/src/main/java/de/ids_mannheim/korap/dao/ResourceDao.java b/full/src/main/java/de/ids_mannheim/korap/dao/ResourceDao.java
index 16913bd..0de5c9c 100644
--- a/full/src/main/java/de/ids_mannheim/korap/dao/ResourceDao.java
+++ b/full/src/main/java/de/ids_mannheim/korap/dao/ResourceDao.java
@@ -30,6 +30,7 @@
*
* @return a list of resources
*/
+ @SuppressWarnings("unchecked")
public List<Resource> getAllResources () {
CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
CriteriaQuery<Resource> query =
diff --git a/full/src/main/java/de/ids_mannheim/korap/dao/RoleDao.java b/full/src/main/java/de/ids_mannheim/korap/dao/RoleDao.java
index b02a283..8898685 100644
--- a/full/src/main/java/de/ids_mannheim/korap/dao/RoleDao.java
+++ b/full/src/main/java/de/ids_mannheim/korap/dao/RoleDao.java
@@ -78,6 +78,7 @@
return (Role) q.getSingleResult();
}
+ @SuppressWarnings("unchecked")
public List<Role> retrieveRoleByGroupMemberId (int userId) {
CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
CriteriaQuery<Role> query = criteriaBuilder.createQuery(Role.class);
diff --git a/full/src/main/java/de/ids_mannheim/korap/dao/UserGroupDao.java b/full/src/main/java/de/ids_mannheim/korap/dao/UserGroupDao.java
index 74d5b1a..2e59379 100644
--- a/full/src/main/java/de/ids_mannheim/korap/dao/UserGroupDao.java
+++ b/full/src/main/java/de/ids_mannheim/korap/dao/UserGroupDao.java
@@ -163,6 +163,7 @@
* @return a list of UserGroup
* @throws KustvaktException
*/
+ @SuppressWarnings("unchecked")
public List<UserGroup> retrieveGroupByUserId (String userId)
throws KustvaktException {
ParameterChecker.checkStringValue(userId, "userId");
@@ -268,6 +269,7 @@
* @return a list of {@link UserGroup}s
* @throws KustvaktException
*/
+ @SuppressWarnings("unchecked")
public List<UserGroup> retrieveGroupByStatus (String userId,
UserGroupStatus status) throws KustvaktException {
diff --git a/full/src/main/java/de/ids_mannheim/korap/dao/UserGroupMemberDao.java b/full/src/main/java/de/ids_mannheim/korap/dao/UserGroupMemberDao.java
index 47fef03..6d1975e 100644
--- a/full/src/main/java/de/ids_mannheim/korap/dao/UserGroupMemberDao.java
+++ b/full/src/main/java/de/ids_mannheim/korap/dao/UserGroupMemberDao.java
@@ -101,6 +101,7 @@
}
+ @SuppressWarnings("unchecked")
public List<UserGroupMember> retrieveMemberByRole (int groupId, int roleId)
throws KustvaktException {
ParameterChecker.checkIntegerValue(roleId, "roleId");
@@ -139,6 +140,7 @@
return retrieveMemberByGroupId(groupId, false);
}
+ @SuppressWarnings("unchecked")
public List<UserGroupMember> retrieveMemberByGroupId (int groupId,
boolean isAdmin) throws KustvaktException {
CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
diff --git a/full/src/main/java/de/ids_mannheim/korap/dao/VirtualCorpusAccessDao.java b/full/src/main/java/de/ids_mannheim/korap/dao/VirtualCorpusAccessDao.java
index 00eb460..3df91d3 100644
--- a/full/src/main/java/de/ids_mannheim/korap/dao/VirtualCorpusAccessDao.java
+++ b/full/src/main/java/de/ids_mannheim/korap/dao/VirtualCorpusAccessDao.java
@@ -58,6 +58,7 @@
}
// for vca admins
+ @SuppressWarnings("unchecked")
public List<VirtualCorpusAccess> retrieveActiveAccessByVC (int vcId)
throws KustvaktException {
ParameterChecker.checkIntegerValue(vcId, "vcId");
@@ -82,6 +83,7 @@
}
// for system admins
+ @SuppressWarnings("unchecked")
public List<VirtualCorpusAccess> retrieveAllAccessByVC (int vcId)
throws KustvaktException {
ParameterChecker.checkIntegerValue(vcId, "vcId");
@@ -101,6 +103,7 @@
return q.getResultList();
}
+ @SuppressWarnings("unchecked")
public List<VirtualCorpusAccess> retrieveAllAccessByGroup (int groupId)
throws KustvaktException {
ParameterChecker.checkIntegerValue(groupId, "groupId");
@@ -120,6 +123,7 @@
return q.getResultList();
}
+ @SuppressWarnings("unchecked")
public List<VirtualCorpusAccess> retrieveActiveAccessByGroup (int groupId)
throws KustvaktException {
ParameterChecker.checkIntegerValue(groupId, "groupId");
diff --git a/full/src/main/java/de/ids_mannheim/korap/dao/VirtualCorpusDao.java b/full/src/main/java/de/ids_mannheim/korap/dao/VirtualCorpusDao.java
index 4b26b2f..9e9983c 100644
--- a/full/src/main/java/de/ids_mannheim/korap/dao/VirtualCorpusDao.java
+++ b/full/src/main/java/de/ids_mannheim/korap/dao/VirtualCorpusDao.java
@@ -116,6 +116,7 @@
* @return a list of {@link VirtualCorpus}
* @throws KustvaktException
*/
+ @SuppressWarnings("unchecked")
public List<VirtualCorpus> retrieveVCByType (VirtualCorpusType type,
String createdBy) throws KustvaktException {
@@ -171,6 +172,7 @@
return vc;
}
+ @SuppressWarnings("unchecked")
public List<VirtualCorpus> retrieveOwnerVC (String userId)
throws KustvaktException {
ParameterChecker.checkStringValue(userId, "userId");
@@ -188,6 +190,7 @@
return q.getResultList();
}
+ @SuppressWarnings("unchecked")
public List<VirtualCorpus> retrieveOwnerVCByType (String userId,
VirtualCorpusType type) throws KustvaktException {
ParameterChecker.checkStringValue(userId, "userId");
@@ -209,6 +212,7 @@
return q.getResultList();
}
+ @SuppressWarnings("unchecked")
public List<VirtualCorpus> retrieveGroupVCByUser (String userId)
throws KustvaktException {
ParameterChecker.checkStringValue(userId, "userId");
@@ -272,6 +276,7 @@
query.distinct(true);
Query q = entityManager.createQuery(query);
+ @SuppressWarnings("unchecked")
List<VirtualCorpus> vcList = q.getResultList();
List<VirtualCorpus> groupVC = retrieveGroupVCByUser(userId);
Set<VirtualCorpus> vcSet = new HashSet<VirtualCorpus>();
@@ -285,6 +290,7 @@
}
// for admins
+ @SuppressWarnings("unchecked")
public List<VirtualCorpus> retrieveVCByGroup (int groupId)
throws KustvaktException {
ParameterChecker.checkIntegerValue(groupId, "groupId");
diff --git a/full/src/main/java/de/ids_mannheim/korap/oauth2/dao/AccessScopeDao.java b/full/src/main/java/de/ids_mannheim/korap/oauth2/dao/AccessScopeDao.java
index a402c33..a94b506 100644
--- a/full/src/main/java/de/ids_mannheim/korap/oauth2/dao/AccessScopeDao.java
+++ b/full/src/main/java/de/ids_mannheim/korap/oauth2/dao/AccessScopeDao.java
@@ -22,6 +22,7 @@
@PersistenceContext
private EntityManager entityManager;
+ @SuppressWarnings("unchecked")
public List<AccessScope> retrieveAccessScopes () {
CriteriaBuilder builder = entityManager.getCriteriaBuilder();
CriteriaQuery<AccessScope> query =
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 9e96654..67503e1 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
@@ -1,6 +1,7 @@
package de.ids_mannheim.korap.oauth2.dao;
import java.time.ZonedDateTime;
+import java.util.List;
import java.util.Set;
import javax.persistence.EntityManager;
@@ -9,6 +10,7 @@
import javax.persistence.Query;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
+import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;
import org.springframework.stereotype.Repository;
@@ -55,9 +57,9 @@
Set<AccessScope> scopes, String userId, String clientId,
ZonedDateTime authenticationTime) throws KustvaktException {
ParameterChecker.checkStringValue(token, "access token");
- ParameterChecker.checkStringValue(refreshToken, "refresh token");
+ ParameterChecker.checkObjectValue(refreshToken, "refresh token");
ParameterChecker.checkObjectValue(scopes, "scopes");
-// ParameterChecker.checkStringValue(userId, "username");
+ // ParameterChecker.checkStringValue(userId, "username");
ParameterChecker.checkStringValue(clientId, "client_id");
ParameterChecker.checkObjectValue(authenticationTime,
"authentication time");
@@ -75,7 +77,7 @@
public AccessToken retrieveAccessToken (String accessToken)
throws KustvaktException {
-
+ ParameterChecker.checkStringValue(accessToken, "access_token");
AccessToken token = (AccessToken) this.getCacheValue(accessToken);
if (token != null) {
return token;
@@ -99,31 +101,29 @@
}
}
- public AccessToken retrieveAccessTokenByRefreshToken (String refreshToken)
- throws KustvaktException {
+ @SuppressWarnings("unchecked")
+ public List<AccessToken> retrieveAccessTokenByRefreshToken (
+ String refreshToken) throws KustvaktException {
+ ParameterChecker.checkStringValue(refreshToken, "refresh_token");
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);
- }
+ Predicate condition = builder.equal(root.get(AccessToken_.refreshToken),
+ refreshToken);
+ query.select(root);
+ query.where(condition);
+ query.orderBy(builder.desc(root.get(AccessToken_.createdDate)));
+
+ Query q = entityManager.createQuery(query);
+ return q.getResultList();
}
public AccessToken updateAccessToken (AccessToken accessToken)
throws KustvaktException {
- ParameterChecker.checkObjectValue(accessToken, "access token");
+ ParameterChecker.checkObjectValue(accessToken, "access_token");
AccessToken cachedToken =
(AccessToken) this.getCacheValue(accessToken.getId());
if (cachedToken != null) {
@@ -133,4 +133,36 @@
accessToken = entityManager.merge(accessToken);
return accessToken;
}
+
+ public AccessToken retrieveAccessTokenByAnynomousToken (String token)
+ throws KustvaktException {
+ ParameterChecker.checkObjectValue(token, "token");
+ AccessToken accessToken = (AccessToken) this.getCacheValue(token);
+ if (accessToken != null) {
+ return accessToken;
+ }
+
+ 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_.token), token),
+ builder.equal(root.get(AccessToken_.refreshToken), token));
+
+
+ query.select(root);
+ query.where(condition);
+ Query q = entityManager.createQuery(query);
+
+ try {
+ accessToken = (AccessToken) q.getSingleResult();
+ return accessToken;
+ }
+ catch (NoResultException e) {
+ throw new KustvaktException(StatusCodes.INVALID_ACCESS_TOKEN,
+ "Access token is not found", OAuth2Error.INVALID_TOKEN);
+ }
+ }
}
diff --git a/full/src/main/java/de/ids_mannheim/korap/oauth2/oltu/OAuth2RevokeTokenRequest.java b/full/src/main/java/de/ids_mannheim/korap/oauth2/oltu/OAuth2RevokeTokenRequest.java
new file mode 100644
index 0000000..78bf412
--- /dev/null
+++ b/full/src/main/java/de/ids_mannheim/korap/oauth2/oltu/OAuth2RevokeTokenRequest.java
@@ -0,0 +1,83 @@
+package de.ids_mannheim.korap.oauth2.oltu;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.servlet.http.HttpServletRequest;
+
+import org.apache.oltu.oauth2.as.request.OAuthRequest;
+import org.apache.oltu.oauth2.common.OAuth;
+import org.apache.oltu.oauth2.common.exception.OAuthProblemException;
+import org.apache.oltu.oauth2.common.exception.OAuthSystemException;
+import org.apache.oltu.oauth2.common.utils.OAuthUtils;
+import org.apache.oltu.oauth2.common.validators.OAuthValidator;
+
+/**
+ * A custom request based on {@link OAuthRequest}.
+ *
+ * This class does not extend {@link OAuthRequest} because it contains some
+ * parameters i.e. redirect_uri and scopes that are not parts of
+ * revoke token request.
+ *
+ * @author margaretha
+ *
+ */
+public class OAuth2RevokeTokenRequest {
+
+ protected HttpServletRequest request;
+ protected OAuthValidator<HttpServletRequest> validator;
+ protected Map<String, Class<? extends OAuthValidator<HttpServletRequest>>> validators =
+ new HashMap<String, Class<? extends OAuthValidator<HttpServletRequest>>>();
+
+ public OAuth2RevokeTokenRequest () {}
+
+ public OAuth2RevokeTokenRequest (HttpServletRequest request)
+ throws OAuthSystemException, OAuthProblemException {
+ this.request = request;
+ validate();
+ }
+
+ protected void validate ()
+ throws OAuthSystemException, OAuthProblemException {
+ validator = initValidator();
+ validator.validateMethod(request);
+ validator.validateContentType(request);
+ validator.validateRequiredParameters(request);
+// validator.validateClientAuthenticationCredentials(request);
+ }
+
+ protected OAuthValidator<HttpServletRequest> initValidator ()
+ throws OAuthProblemException, OAuthSystemException {
+ return OAuthUtils.instantiateClass(RevokeTokenValidator.class);
+ }
+
+ public String getParam (String name) {
+ return request.getParameter(name);
+ }
+
+ public String getToken () {
+ return getParam("token");
+ }
+
+ public String getTokenType () {
+ return getParam(OAuth.OAUTH_TOKEN_TYPE);
+ }
+
+ public String getClientId () {
+ String[] creds = OAuthUtils.decodeClientAuthenticationHeader(
+ request.getHeader(OAuth.HeaderType.AUTHORIZATION));
+ if (creds != null) {
+ return creds[0];
+ }
+ return getParam(OAuth.OAUTH_CLIENT_ID);
+ }
+
+ public String getClientSecret () {
+ String[] creds = OAuthUtils.decodeClientAuthenticationHeader(
+ request.getHeader(OAuth.HeaderType.AUTHORIZATION));
+ if (creds != null) {
+ return creds[1];
+ }
+ return getParam(OAuth.OAUTH_CLIENT_SECRET);
+ }
+}
diff --git a/full/src/main/java/de/ids_mannheim/korap/oauth2/oltu/RevokeTokenValidator.java b/full/src/main/java/de/ids_mannheim/korap/oauth2/oltu/RevokeTokenValidator.java
new file mode 100644
index 0000000..0a9d9c7
--- /dev/null
+++ b/full/src/main/java/de/ids_mannheim/korap/oauth2/oltu/RevokeTokenValidator.java
@@ -0,0 +1,45 @@
+package de.ids_mannheim.korap.oauth2.oltu;
+
+import javax.servlet.http.HttpServletRequest;
+
+import org.apache.oltu.oauth2.common.OAuth;
+import org.apache.oltu.oauth2.common.exception.OAuthProblemException;
+import org.apache.oltu.oauth2.common.validators.AbstractValidator;
+
+import de.ids_mannheim.korap.oauth2.constant.OAuth2Error;
+
+/**
+ * A custom revoke token validator based on RFC 7009.
+ *
+ * Additional changes to the RFC:
+ * <ul>
+ * <li>client_id is made required for public client
+ * authentication</li>
+ * </ul>
+ *
+ * @author margaretha
+ *
+ */
+public class RevokeTokenValidator
+ extends AbstractValidator<HttpServletRequest> {
+
+ public RevokeTokenValidator () {
+ requiredParams.add("token");
+ requiredParams.add(OAuth.OAUTH_CLIENT_ID);
+ }
+
+ @Override
+ public void validateMethod (HttpServletRequest request)
+ throws OAuthProblemException {
+ String method = request.getMethod();
+ if (!OAuth.HttpMethod.POST.equals(method)) {
+ throw OAuthProblemException.error(OAuth2Error.INVALID_REQUEST)
+ .description("Method not correct.");
+ }
+ }
+
+ @Override
+ public void validateContentType (HttpServletRequest request)
+ throws OAuthProblemException {}
+
+}
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 c9b574d..c53fbb6 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
@@ -2,6 +2,7 @@
import java.time.ZoneId;
import java.time.ZonedDateTime;
+import java.util.List;
import java.util.Set;
import javax.ws.rs.core.Response.Status;
@@ -25,14 +26,15 @@
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.oltu.OAuth2RevokeTokenRequest;
import de.ids_mannheim.korap.oauth2.service.OAuth2TokenService;
@Service
public class OltuTokenService extends OAuth2TokenService {
-
+
@Autowired
- private RandomCodeGenerator randomGenerator;
-
+ private RandomCodeGenerator randomGenerator;
+
@Autowired
private AccessTokenDao tokenDao;
@@ -70,6 +72,23 @@
}
+ /**
+ * Revokes all access token associated with the given refresh
+ * token, and creates a new access token with the given refresh
+ * token and scopes. Thus, at one point of time, there is only one
+ * active access token associated with a refresh token.
+ *
+ * Client authentication is done using the given client
+ * credentials.
+ *
+ * @param refreshToken
+ * @param scopes
+ * @param clientId
+ * @param clientSecret
+ * @return if successful, a new access token
+ * @throws KustvaktException
+ * @throws OAuthSystemException
+ */
private OAuthResponse requestAccessTokenWithRefreshToken (
String refreshToken, Set<String> scopes, String clientId,
String clientSecret)
@@ -82,39 +101,47 @@
}
clientService.authenticateClient(clientId, clientSecret);
- AccessToken accessToken =
+ List<AccessToken> accessTokenList =
tokenDao.retrieveAccessTokenByRefreshToken(refreshToken);
+ if (accessTokenList.isEmpty()) {
+ throw new KustvaktException(StatusCodes.INVALID_REFRESH_TOKEN,
+ "Refresh token is not found", OAuth2Error.INVALID_GRANT);
+ }
- if (!clientId.equals(accessToken.getClientId())) {
+ AccessToken latestAccessToken = accessTokenList.get(0);
+ AccessToken origin = accessTokenList.get(accessTokenList.size() - 1);
+
+ if (!clientId.equals(latestAccessToken.getClientId())) {
throw new KustvaktException(StatusCodes.CLIENT_AUTHORIZATION_FAILED,
"Client " + clientId + "is not authorized",
OAuth2Error.INVALID_CLIENT);
}
- if (accessToken.isRefreshTokenRevoked()) {
+ if (latestAccessToken.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()
+ .isAfter(origin.getCreatedDate()
.plusSeconds(config.getRefreshTokenExpiry()))) {
throw new KustvaktException(StatusCodes.INVALID_REFRESH_TOKEN,
"Refresh token is expired.", OAuth2Error.INVALID_GRANT);
}
- Set<AccessScope> requestedScopes = accessToken.getScopes();
+ Set<AccessScope> requestedScopes = latestAccessToken.getScopes();
if (scopes != null && !scopes.isEmpty()) {
requestedScopes = scopeService.verifyRefreshScope(scopes,
- accessToken.getScopes());
+ latestAccessToken.getScopes());
}
- accessToken.setRefreshTokenRevoked(true);
- accessToken = tokenDao.updateAccessToken(accessToken);
+ if (!latestAccessToken.isRevoked()) {
+ tokenDao.updateAccessToken(latestAccessToken);
+ }
return createsAccessTokenResponse(scopes, requestedScopes, clientId,
- accessToken.getUserId(),
- accessToken.getUserAuthenticationTime());
+ latestAccessToken.getUserId(),
+ latestAccessToken.getUserAuthenticationTime(), refreshToken);
}
/**
@@ -280,8 +307,16 @@
ZonedDateTime authenticationTime)
throws OAuthSystemException, KustvaktException {
- String accessToken = randomGenerator.createRandomCode();
String refreshToken = randomGenerator.createRandomCode();
+ return createsAccessTokenResponse(scopes, accessScopes, clientId,
+ userId, authenticationTime, refreshToken);
+ }
+
+ private OAuthResponse createsAccessTokenResponse (Set<String> scopes,
+ Set<AccessScope> accessScopes, String clientId, String userId,
+ ZonedDateTime authenticationTime, String refreshToken)
+ throws OAuthSystemException, KustvaktException {
+ String accessToken = randomGenerator.createRandomCode();
tokenDao.storeAccessToken(accessToken, refreshToken, accessScopes,
userId, clientId, authenticationTime);
@@ -293,4 +328,55 @@
.setRefreshToken(refreshToken)
.setScope(String.join(" ", scopes)).buildJSONMessage();
}
+
+ public void revokeToken (OAuth2RevokeTokenRequest revokeTokenRequest)
+ throws KustvaktException {
+ String clientId = revokeTokenRequest.getClientId();
+ String clientSecret = revokeTokenRequest.getClientSecret();
+ String token = revokeTokenRequest.getToken();
+ String tokenType = revokeTokenRequest.getTokenType();
+
+ clientService.authenticateClient(clientId, clientSecret);
+ tokenDao.removeCacheEntry(token);
+ if (tokenType != null && tokenType.equals("refresh_token")) {
+ if (!revokeRefreshToken(token)) {
+ revokeAccessToken(token);
+ }
+ return;
+ }
+
+ if (!revokeAccessToken(token)) {
+ revokeRefreshToken(token);
+ }
+ }
+
+ private boolean revokeAccessToken (String token) throws KustvaktException {
+ try {
+ AccessToken accessToken = tokenDao.retrieveAccessToken(token);
+ accessToken.setRevoked(true);
+ tokenDao.updateAccessToken(accessToken);
+ return true;
+ }
+ catch (KustvaktException e) {
+ if (!e.getStatusCode().equals(StatusCodes.INVALID_ACCESS_TOKEN)) {
+ return false;
+ }
+ throw e;
+ }
+ }
+
+ private boolean revokeRefreshToken (String token) throws KustvaktException {
+ List<AccessToken> accessTokenList =
+ tokenDao.retrieveAccessTokenByRefreshToken(token);
+ if (accessTokenList.isEmpty()) {
+ return false;
+ }
+
+ for (AccessToken accessToken : accessTokenList) {
+ accessToken.setRevoked(true);
+ accessToken.setRefreshTokenRevoked(true);
+ tokenDao.updateAccessToken(accessToken);
+ }
+ return true;
+ }
}
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 4ab46b4..e78aa2a 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
@@ -24,10 +24,12 @@
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
+import com.sun.jersey.api.client.ClientResponse.Status;
import com.sun.jersey.spi.container.ResourceFilters;
import de.ids_mannheim.korap.exceptions.KustvaktException;
import de.ids_mannheim.korap.oauth2.oltu.OAuth2AuthorizationRequest;
+import de.ids_mannheim.korap.oauth2.oltu.OAuth2RevokeTokenRequest;
import de.ids_mannheim.korap.oauth2.oltu.service.OltuAuthorizationService;
import de.ids_mannheim.korap.oauth2.oltu.service.OltuTokenService;
import de.ids_mannheim.korap.security.context.TokenContext;
@@ -106,15 +108,26 @@
/**
* Grants a client an access token, namely a string used in
* authenticated requests representing user authorization for
- * the client to access user resources. Client credentials for
- * authentication can be provided either as an authorization
- * header with Basic authentication scheme or as form parameters
- * in the request body.
+ * the client to access user resources. An additional refresh
+ * token strictly associated to the access token is also granted.
*
* <br /><br />
*
- * OAuth2 describes various ways of requesting an access token.
- * Kustvakt supports:
+ * Clients may refreshing access token using this endpoint. This
+ * request will revoke all access token associated with the given
+ * refresh token, and grants a new access token. The refresh token
+ * is not changed and can be used until it expires.
+ *
+ * <br /><br />
+ *
+ * Client credentials for authentication can be provided either as
+ * an authorization header with Basic authentication scheme or as
+ * form parameters in the request body.
+ *
+ * <br /><br />
+ *
+ * OAuth2 specification describes various ways of requesting an
+ * access token. Kustvakt supports:
* <ul>
* <li> Authorization code grant: obtains authorization from a
* third party application. Required parameters: grant_type,
@@ -135,7 +148,6 @@
* </li>
* </ul>
*
- * <br /><br />
* RFC 6749: The value of the scope parameter is expressed as a
* list of space-delimited, case-sensitive strings defined by the
* authorization server.
@@ -186,14 +198,40 @@
}
}
- // @POST
- // @Path("revoke")
- // @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
- // @Produces(MediaType.APPLICATION_JSON + ";charset=utf-8")
- // public Response revokeAccessToken (@Context HttpServletRequest
- // request,
- // @FormParam("grant_type") String grantType,
- // MultivaluedMap<String, String> form) {
- // return null;
- // }
+ /**
+ * Revoking an access token also revokes its refresh token, vice
+ * versa.
+ *
+ * RFC 7009
+ * Client authentication for confidential client
+ *
+ * @param request
+ * @param form
+ * @return 200 if token invalidation is successful or the given
+ * token is invalid
+ */
+ @POST
+ @Path("revoke")
+ @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
+ @Produces(MediaType.APPLICATION_JSON + ";charset=utf-8")
+ public Response revokeAccessToken (@Context HttpServletRequest request,
+ MultivaluedMap<String, String> form) {
+
+ try {
+ OAuth2RevokeTokenRequest revokeTokenRequest =
+ new OAuth2RevokeTokenRequest(
+ new FormRequestWrapper(request, form));
+ tokenService.revokeToken(revokeTokenRequest);
+ return Response.ok().build();
+ }
+ catch (OAuthProblemException e) {
+ throw responseHandler.throwit(e);
+ }
+ catch (OAuthSystemException e) {
+ throw responseHandler.throwit(e);
+ }
+ catch (KustvaktException e) {
+ throw responseHandler.throwit(e);
+ }
+ }
}
diff --git a/full/src/test/java/de/ids_mannheim/korap/web/controller/OAuth2AccessTokenTest.java b/full/src/test/java/de/ids_mannheim/korap/web/controller/OAuth2AccessTokenTest.java
index d7bcde2..8282baa 100644
--- a/full/src/test/java/de/ids_mannheim/korap/web/controller/OAuth2AccessTokenTest.java
+++ b/full/src/test/java/de/ids_mannheim/korap/web/controller/OAuth2AccessTokenTest.java
@@ -77,9 +77,16 @@
}
@Test
- public void testListVCScopeNotAuthorized ()
- throws KustvaktException, IOException {
+ public void testTokenAccessScope () throws KustvaktException, IOException {
String accessToken = requestToken();
+ testListVCScopeNotAuthorized(accessToken);
+ testListVCAccessBearerNotAuthorize(accessToken);
+ testSearchWithOAuth2Token(accessToken);
+
+ }
+
+ private void testListVCScopeNotAuthorized (String accessToken)
+ throws KustvaktException {
ClientResponse response = resource().path("vc").path("list")
.header(Attributes.AUTHORIZATION, "Bearer " + accessToken)
.get(ClientResponse.class);
@@ -92,9 +99,6 @@
node.at("/errors/0/0").asInt());
assertEquals("Scope vc_info is not authorized",
node.at("/errors/0/1").asText());
-
- testListVCAccessBearerNotAuthorize(accessToken);
- testSearchWithOAuth2Token(accessToken);
}
private void testListVCAccessBearerNotAuthorize (String accessToken)
@@ -151,4 +155,41 @@
assertEquals("Access token is not found",
node.at("/errors/0/1").asText());
}
+
+ @Test
+ public void testRevokeAccessTokenConfidentialClient ()
+ throws KustvaktException {
+ String accessToken = requestToken();
+ MultivaluedMap<String, String> form = new MultivaluedMapImpl();
+ form.add("token", accessToken);
+ form.add("client_id", "fCBbQkAyYzI4NzUxMg");
+ form.add("client_secret", "secret");
+
+ ClientResponse response = resource().path("oauth2").path("revoke")
+ .header(HttpHeaders.CONTENT_TYPE,
+ ContentType.APPLICATION_FORM_URLENCODED)
+ .entity(form).post(ClientResponse.class);
+
+ assertEquals(Status.OK.getStatusCode(), response.getStatus());
+
+ testSearchWithRevokedToken(accessToken);
+ }
+
+ private void testSearchWithRevokedToken (String accessToken)
+ throws KustvaktException {
+ ClientResponse response = resource().path("search")
+ .queryParam("q", "Wasser").queryParam("ql", "poliqarp")
+ .header(Attributes.AUTHORIZATION, "Bearer " + accessToken)
+ .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
+ .get(ClientResponse.class);
+
+ String entity = response.getEntity(String.class);
+ assertEquals(ClientResponse.Status.UNAUTHORIZED.getStatusCode(),
+ response.getStatus());
+
+ JsonNode node = JsonUtils.readTree(entity);
+ assertEquals(StatusCodes.INVALID_ACCESS_TOKEN, node.at("/errors/0/0").asInt());
+ assertEquals("Access token has been revoked", node.at("/errors/0/1").asText());
+
+ }
}
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 a5ba179..3dabb7b 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
@@ -35,7 +35,7 @@
*/
public class OAuth2ControllerTest extends SpringJerseyTest {
- private ClientResponse requestAuthorizationConfidentialClient (
+ private ClientResponse requestAuthorization (
MultivaluedMap<String, String> form) throws KustvaktException {
return resource().path("oauth2").path("authorize").header(
@@ -55,7 +55,25 @@
form.add("client_id", "fCBbQkAyYzI4NzUxMg");
form.add("state", "thisIsMyState");
- ClientResponse response = requestAuthorizationConfidentialClient(form);
+ ClientResponse response = requestAuthorization(form);
+
+ assertEquals(Status.TEMPORARY_REDIRECT.getStatusCode(),
+ response.getStatus());
+ URI redirectUri = response.getLocation();
+ MultiValueMap<String, String> params = UriComponentsBuilder
+ .fromUri(redirectUri).build().getQueryParams();
+ assertNotNull(params.getFirst("code"));
+ assertEquals("thisIsMyState", params.getFirst("state"));
+ }
+
+ @Test
+ public void testAuthorizePublicClient () throws KustvaktException {
+ MultivaluedMap<String, String> form = new MultivaluedMapImpl();
+ form.add("response_type", "code");
+ form.add("client_id", "8bIDtZnH6NvRkW2Fq");
+ form.add("state", "thisIsMyState");
+
+ ClientResponse response = requestAuthorization(form);
assertEquals(Status.TEMPORARY_REDIRECT.getStatusCode(),
response.getStatus());
@@ -75,7 +93,7 @@
form.add("client_id", "fCBbQkAyYzI4NzUxMg");
form.add("redirect_uri", redirectUri);
form.add("state", "thisIsMyState");
- ClientResponse response = requestAuthorizationConfidentialClient(form);
+ ClientResponse response = requestAuthorization(form);
assertEquals(Status.BAD_REQUEST.getStatusCode(), response.getStatus());
@@ -94,7 +112,7 @@
MultivaluedMap<String, String> form = new MultivaluedMapImpl();
form.add("state", "thisIsMyState");
// missing response_type
- ClientResponse response = requestAuthorizationConfidentialClient(form);
+ ClientResponse response = requestAuthorization(form);
assertEquals(Status.BAD_REQUEST.getStatusCode(), response.getStatus());
@@ -108,7 +126,7 @@
// missing client_id
form.add("response_type", "code");
- response = requestAuthorizationConfidentialClient(form);
+ response = requestAuthorization(form);
entity = response.getEntity(String.class);
node = JsonUtils.readTree(entity);
assertEquals("Missing parameters: client_id",
@@ -121,7 +139,7 @@
form.add("response_type", "string");
form.add("state", "thisIsMyState");
- ClientResponse response = requestAuthorizationConfidentialClient(form);
+ ClientResponse response = requestAuthorization(form);
assertEquals(Status.BAD_REQUEST.getStatusCode(), response.getStatus());
String entity = response.getEntity(String.class);
@@ -141,7 +159,7 @@
form.add("scope", "read_address");
form.add("state", "thisIsMyState");
- ClientResponse response = requestAuthorizationConfidentialClient(form);
+ ClientResponse response = requestAuthorization(form);
assertEquals(Status.BAD_REQUEST.getStatusCode(), response.getStatus());
URI location = response.getLocation();
@@ -171,8 +189,7 @@
authForm.add("client_id", "fCBbQkAyYzI4NzUxMg");
authForm.add("scope", "search");
- ClientResponse response =
- requestAuthorizationConfidentialClient(authForm);
+ ClientResponse response = requestAuthorization(authForm);
URI redirectUri = response.getLocation();
MultivaluedMap<String, String> params =
UriComponent.decodeQuery(redirectUri, true);
@@ -243,8 +260,7 @@
authForm.add("scope", "search");
authForm.add("redirect_uri", uri);
- ClientResponse response =
- requestAuthorizationConfidentialClient(authForm);
+ ClientResponse response = requestAuthorization(authForm);
URI redirectUri = response.getLocation();
MultivaluedMap<String, String> params =
UriComponent.decodeQuery(redirectUri, true);
@@ -434,17 +450,22 @@
assertEquals(Status.OK.getStatusCode(), response.getStatus());
JsonNode node = JsonUtils.readTree(entity);
- assertNotNull(node.at("/access_token").asText());
+ String accessToken = node.at("/access_token").asText();
assertEquals(TokenType.BEARER.toString(),
node.at("/token_type").asText());
assertNotNull(node.at("/expires_in").asText());
+
+ testRevokeTokenPublicClient(accessToken, clientId, "access_token");
+
String refreshToken = node.at("/refresh_token").asText();
testRequestRefreshTokenInvalidScope(clientId, refreshToken);
testRequestRefreshTokenPublicClient(clientId, refreshToken);
testRequestRefreshTokenInvalidClient(refreshToken);
testRequestRefreshTokenInvalidRefreshToken(clientId);
- testRevokedRefreshToken(clientId, refreshToken);
+
+ testRevokeTokenPublicClient(refreshToken, clientId, "refresh_token");
+ testRequestRefreshWithRevokedRefreshToken(clientId, refreshToken);
}
@Test
@@ -490,6 +511,29 @@
assertNotNull(node.at("/expires_in").asText());
}
+ /**
+ * Client credentials grant is only allowed for confidential
+ * clients.
+ */
+ @Test
+ public void testRequestTokenClientCredentialsGrantPublic ()
+ throws KustvaktException {
+
+ MultivaluedMap<String, String> form = new MultivaluedMapImpl();
+ form.add("grant_type", "client_credentials");
+ form.add("client_id", "8bIDtZnH6NvRkW2Fq");
+ form.add("client_secret", "");
+ ClientResponse response = requestToken(form);
+
+ String entity = response.getEntity(String.class);
+ assertEquals(Status.BAD_REQUEST.getStatusCode(), response.getStatus());
+ JsonNode node = JsonUtils.readTree(entity);
+ assertEquals(OAuthError.TokenResponse.INVALID_REQUEST,
+ node.at("/error").asText());
+ assertEquals("Missing parameters: client_secret",
+ node.at("/error_description").asText());
+ }
+
@Test
public void testRequestTokenClientCredentialsGrantReducedScope ()
throws KustvaktException {
@@ -627,8 +671,8 @@
assertEquals(OAuth2Error.INVALID_GRANT, node.at("/error").asText());
}
- private void testRevokedRefreshToken (String clientId, String refreshToken)
- throws KustvaktException {
+ private void testRequestRefreshWithRevokedRefreshToken (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);
@@ -645,4 +689,19 @@
assertEquals(OAuth2Error.INVALID_GRANT, node.at("/error").asText());
}
+ private void testRevokeTokenPublicClient (String token,
+ String clientId, String tokenType) {
+ MultivaluedMap<String, String> form = new MultivaluedMapImpl();
+ form.add("token_type", tokenType);
+ form.add("token", token);
+ form.add("client_id", clientId);
+
+ ClientResponse response = resource().path("oauth2").path("revoke")
+ .header(HttpHeaders.CONTENT_TYPE,
+ ContentType.APPLICATION_FORM_URLENCODED)
+ .entity(form).post(ClientResponse.class);
+
+ assertEquals(Status.OK.getStatusCode(), response.getStatus());
+ }
+
}