Handled scopes & added request token with authorization code tests.
Change-Id: I775141b8b94bf2d1c86ad873807fcb1b12f3914f
diff --git a/full/Changes b/full/Changes
index 9182b80..4589e7b 100644
--- a/full/Changes
+++ b/full/Changes
@@ -1,5 +1,5 @@
version 0.60.2
-02/05/2018
+03/05/2018
- implemented OAuth2 client registration (margaretha)
- implemented OAuth2 client authentication (margaretha)
- changed virtual corpus search to retrieval (margaretha)
@@ -19,6 +19,8 @@
- fixed loading spring config multiple times in the test suite (margaretha)
- added SQLite created_date trigger for access token (margaretha)
- added a join table for access token scopes (margaretha)
+ - added access scopes handling (margaretha)
+ - added tests about request token with authorization code (margaretha)
version 0.60.1
28/03/2018
diff --git a/full/src/main/java/de/ids_mannheim/korap/config/FullConfiguration.java b/full/src/main/java/de/ids_mannheim/korap/config/FullConfiguration.java
index 5fb08a8..d237494 100644
--- a/full/src/main/java/de/ids_mannheim/korap/config/FullConfiguration.java
+++ b/full/src/main/java/de/ids_mannheim/korap/config/FullConfiguration.java
@@ -54,7 +54,8 @@
private AuthenticationMethod OAuth2passwordAuthentication;
private String nativeClientHost;
- private Set<String> accessScopes;
+ private Set<String> defaultAccessScopes;
+ private Set<String> clientCredentialsScopes;
private int maxAuthenticationAttempts;
public FullConfiguration (Properties properties) throws IOException {
@@ -94,7 +95,12 @@
"read_username read_email");
Set<String> scopeSet =
Arrays.stream(scopes.split(" ")).collect(Collectors.toSet());
- setAccessScopes(scopeSet);
+ setDefaultAccessScopes(scopeSet);
+
+ String clientScopes = properties.getProperty(
+ "oauth2.client.credentials.scopes", "read_client_info");
+ setClientCredentialsScopes(Arrays.stream(clientScopes.split(" "))
+ .collect(Collectors.toSet()));
}
private void setMailConfiguration (Properties properties) {
@@ -352,12 +358,21 @@
this.maxAuthenticationAttempts = maxAuthenticationAttempts;
}
- public Set<String> getAccessScopes () {
- return accessScopes;
+ public Set<String> getDefaultAccessScopes () {
+ return defaultAccessScopes;
}
- public void setAccessScopes (Set<String> accessScopes) {
- this.accessScopes = accessScopes;
+ public void setDefaultAccessScopes (Set<String> accessScopes) {
+ this.defaultAccessScopes = accessScopes;
+ }
+
+ public Set<String> getClientCredentialsScopes () {
+ return clientCredentialsScopes;
+ }
+
+ public void setClientCredentialsScopes (
+ Set<String> clientCredentialsScopes) {
+ this.clientCredentialsScopes = clientCredentialsScopes;
}
}
diff --git a/full/src/main/java/de/ids_mannheim/korap/config/Initializator.java b/full/src/main/java/de/ids_mannheim/korap/config/Initializator.java
index e77f006..6c04431 100644
--- a/full/src/main/java/de/ids_mannheim/korap/config/Initializator.java
+++ b/full/src/main/java/de/ids_mannheim/korap/config/Initializator.java
@@ -4,7 +4,8 @@
import de.ids_mannheim.korap.oauth2.dao.AccessScopeDao;
-/** Initializes values in the database from kustvakt configuration.
+/**
+ * Initializes values in the database from kustvakt configuration.
*
* @author margaretha
*
@@ -13,19 +14,20 @@
private FullConfiguration config;
private AccessScopeDao accessScopeDao;
-
- public Initializator (FullConfiguration config, AccessScopeDao accessScopeDao) {
+
+ public Initializator (FullConfiguration config,
+ AccessScopeDao accessScopeDao) {
this.config = config;
this.accessScopeDao = accessScopeDao;
}
-
+
public void init () {
setAccessScope();
}
-
- private void setAccessScope(){
- Set<String> accessScopes = config.getAccessScopes();
- accessScopeDao.storeAccessScopes(accessScopes);
+
+ private void setAccessScope () {
+ accessScopeDao.storeAccessScopes(config.getDefaultAccessScopes());
+ accessScopeDao.storeAccessScopes(config.getClientCredentialsScopes());
}
}
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 1834c66..f1507b8 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,13 +1,18 @@
package de.ids_mannheim.korap.oauth2.dao;
+import java.util.Set;
+
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;
+import de.ids_mannheim.korap.exceptions.KustvaktException;
+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.utils.ParameterChecker;
@Repository
@Transactional
@@ -16,11 +21,24 @@
@PersistenceContext
private EntityManager entityManager;
- public void storeAccessToken (Authorization authorization, String token) {
+ public void storeAccessToken (Authorization authorization, String token)
+ throws KustvaktException {
+ ParameterChecker.checkObjectValue(authorization, "Authorization");
+ ParameterChecker.checkStringValue(token, "accessToken");
+
AccessToken accessToken = new AccessToken();
accessToken.setAuthorization(authorization);
accessToken.setToken(token);
accessToken.setScopes(authorization.getScopes());
entityManager.persist(accessToken);
}
+
+ public void storeAccessToken (String token, Set<AccessScope> scopes)
+ throws KustvaktException {
+ ParameterChecker.checkObjectValue(scopes, "scopes");
+ AccessToken accessToken = new AccessToken();
+ accessToken.setToken(token);
+ accessToken.setScopes(scopes);
+ entityManager.persist(accessToken);
+ }
}
diff --git a/full/src/main/java/de/ids_mannheim/korap/oauth2/dao/AuthorizationDao.java b/full/src/main/java/de/ids_mannheim/korap/oauth2/dao/AuthorizationDao.java
index f6f505f..d60fe63 100644
--- a/full/src/main/java/de/ids_mannheim/korap/oauth2/dao/AuthorizationDao.java
+++ b/full/src/main/java/de/ids_mannheim/korap/oauth2/dao/AuthorizationDao.java
@@ -14,6 +14,8 @@
import org.springframework.transaction.annotation.Transactional;
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.Authorization;
import de.ids_mannheim.korap.oauth2.entity.Authorization_;
@@ -39,24 +41,29 @@
// what if unique fails
}
- public Authorization retrieveAuthorizationCode (String code,
- String clientId) throws KustvaktException {
+ public Authorization retrieveAuthorizationCode (String code)
+ throws KustvaktException {
ParameterChecker.checkStringValue(code, "code");
- ParameterChecker.checkStringValue(clientId, "client_id");
CriteriaBuilder builder = entityManager.getCriteriaBuilder();
CriteriaQuery<Authorization> query =
builder.createQuery(Authorization.class);
Root<Authorization> root = query.from(Authorization.class);
- Predicate restrictions = builder.and(
- builder.equal(root.get(Authorization_.code), code),
- builder.equal(root.get(Authorization_.clientId), clientId));
+ Predicate restrictions =
+ builder.equal(root.get(Authorization_.code), code);
query.select(root);
query.where(restrictions);
Query q = entityManager.createQuery(query);
- return (Authorization) q.getSingleResult();
+ try {
+ return (Authorization) q.getSingleResult();
+ }
+ catch (Exception e) {
+ throw new KustvaktException(StatusCodes.INVALID_AUTHORIZATION,
+ "Invalid authorization: " + e.getMessage(),
+ OAuth2Error.INVALID_REQUEST);
+ }
}
public Authorization updateAuthorization (Authorization authorization) {
diff --git a/full/src/main/java/de/ids_mannheim/korap/oauth2/entity/AccessScope.java b/full/src/main/java/de/ids_mannheim/korap/oauth2/entity/AccessScope.java
index cca86ac..46d2b5c 100644
--- a/full/src/main/java/de/ids_mannheim/korap/oauth2/entity/AccessScope.java
+++ b/full/src/main/java/de/ids_mannheim/korap/oauth2/entity/AccessScope.java
@@ -34,7 +34,7 @@
@Override
public String toString () {
- return "id: " + id;
+ return id;
}
@Override
diff --git a/full/src/main/java/de/ids_mannheim/korap/oauth2/service/OAuth2AuthorizationService.java b/full/src/main/java/de/ids_mannheim/korap/oauth2/service/OAuth2AuthorizationService.java
index 5c1d02b..1a33155 100644
--- a/full/src/main/java/de/ids_mannheim/korap/oauth2/service/OAuth2AuthorizationService.java
+++ b/full/src/main/java/de/ids_mannheim/korap/oauth2/service/OAuth2AuthorizationService.java
@@ -1,8 +1,6 @@
package de.ids_mannheim.korap.oauth2.service;
import java.time.ZonedDateTime;
-import java.util.HashSet;
-import java.util.List;
import java.util.Set;
import javax.servlet.http.HttpServletRequest;
@@ -24,7 +22,6 @@
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.dao.AccessScopeDao;
import de.ids_mannheim.korap.oauth2.dao.AuthorizationDao;
import de.ids_mannheim.korap.oauth2.entity.AccessScope;
import de.ids_mannheim.korap.oauth2.entity.Authorization;
@@ -41,12 +38,12 @@
@Autowired
private OAuth2TokenService auth2Service;
@Autowired
+ private OAuth2ScopeService scopeService;
+ @Autowired
private OAuthIssuer oauthIssuer;
@Autowired
private AuthorizationDao authorizationDao;
- @Autowired
- private AccessScopeDao accessScopeDao;
@Autowired
private FullConfiguration config;
@@ -55,11 +52,7 @@
OAuthAuthzRequest authzRequest, String authorization)
throws KustvaktException, OAuthSystemException {
- String responseType = authzRequest.getResponseType();
- if (responseType == null || responseType.isEmpty()) {
- throw new KustvaktException(StatusCodes.MISSING_PARAMETER,
- "response_type is missing.", OAuth2Error.INVALID_REQUEST);
- }
+ checkResponseType(authzRequest.getResponseType());
OAuth2Client client = clientService.authenticateClient(
authzRequest.getClientId(), authzRequest.getClientSecret());
@@ -74,42 +67,41 @@
authzRequest.getScopes());
String code = oauthIssuer.authorizationCode();
- Set<AccessScope> scopes =
- convertToAccessScope(authzRequest.getScopes());
+ Set<String> scopeSet = authzRequest.getScopes();
+ if (scopeSet == null || scopeSet.isEmpty()) {
+ scopeSet = config.getDefaultAccessScopes();
+ }
+ String scopeStr = String.join(" ", scopeSet);
+ Set<AccessScope> scopes = scopeService.convertToAccessScope(scopeSet);
authorizationDao.storeAuthorizationCode(authzRequest.getClientId(),
username, code, scopes, authzRequest.getRedirectURI());
return OAuthASResponse
.authorizationResponse(request, Status.FOUND.getStatusCode())
- .setCode(code).location(redirectUri).buildQueryMessage();
+ .setCode(code).setScope(scopeStr).location(redirectUri)
+ .buildQueryMessage();
}
- private Set<AccessScope> convertToAccessScope (Set<String> scopes)
+ private void checkResponseType (String responseType)
throws KustvaktException {
-
- if (scopes.isEmpty()) {
- // return default scopes
- return null;
+ if (responseType == null || responseType.isEmpty()) {
+ throw new KustvaktException(StatusCodes.MISSING_PARAMETER,
+ "response_type is missing.", OAuth2Error.INVALID_REQUEST);
}
-
- List<AccessScope> definedScopes = accessScopeDao.retrieveAccessScopes();
- Set<AccessScope> requestedScopes =
- new HashSet<AccessScope>(scopes.size());
- int index;
- for (String scope : scopes) {
- index = definedScopes.indexOf(new AccessScope(scope));
- if (index == -1) {
- throw new KustvaktException(StatusCodes.INVALID_SCOPE,
- scope + " is invalid.", OAuth2Error.INVALID_SCOPE);
- }
- else {
- requestedScopes.add(definedScopes.get(index));
- }
+ else if (responseType.equals("token")) {
+ throw new KustvaktException(StatusCodes.NOT_SUPPORTED,
+ "response_type token is not supported.",
+ OAuth2Error.INVALID_REQUEST);
}
- return requestedScopes;
+ else if (!responseType.equals("code")) {
+ throw new KustvaktException(StatusCodes.INVALID_ARGUMENT,
+ "unknown response_type", OAuth2Error.INVALID_REQUEST);
+ }
}
+
+
private boolean hasRedirectUri (String redirectURI) {
if (redirectURI != null && !redirectURI.isEmpty()) {
return true;
@@ -152,7 +144,8 @@
}
else {
// check if there is a redirect URI in the DB
- // This should not happened as it is required in client registration!
+ // This should not happened as it is required in client
+ // registration!
if (registeredUri != null && !registeredUri.isEmpty()) {
redirectUri = registeredUri;
}
@@ -167,52 +160,55 @@
}
- public Authorization verifyAuthorization (String code, String clientId,
- String redirectURI) throws KustvaktException {
- Authorization authorization =
- authorizationDao.retrieveAuthorizationCode(code, clientId);
+ public Authorization retrieveAuthorization (String code)
+ throws KustvaktException {
+ return authorizationDao.retrieveAuthorizationCode(code);
+ }
- // EM: can Kustvakt be specific about the invalid request param?
- if (authorization.isRevoked()) {
- addTotalAttempts(authorization);
+ public Authorization verifyAuthorization (Authorization authorization,
+ String clientId, String redirectURI) throws KustvaktException {
+
+ // EM: can Kustvakt be specific about the invalid grant error
+ // description?
+ if (!authorization.getClientId().equals(clientId)) {
throw new KustvaktException(StatusCodes.INVALID_AUTHORIZATION,
- "Invalid authorization", OAuth2Error.INVALID_REQUEST);
+ "Invalid authorization", OAuth2Error.INVALID_GRANT);
+ }
+ if (authorization.isRevoked()) {
+ throw new KustvaktException(StatusCodes.INVALID_AUTHORIZATION,
+ "Invalid authorization", OAuth2Error.INVALID_GRANT);
}
if (isExpired(authorization.getCreatedDate())) {
- addTotalAttempts(authorization);
throw new KustvaktException(StatusCodes.INVALID_AUTHORIZATION,
- "Authorization expired", OAuth2Error.INVALID_REQUEST);
+ "Authorization expired", OAuth2Error.INVALID_GRANT);
}
String authorizedUri = authorization.getRedirectURI();
if (authorizedUri != null && !authorizedUri.isEmpty()
&& !authorizedUri.equals(redirectURI)) {
- addTotalAttempts(authorization);
throw new KustvaktException(StatusCodes.INVALID_REDIRECT_URI,
- "Invalid redirect URI", OAuth2Error.INVALID_REQUEST);
+ "Invalid redirect URI", OAuth2Error.INVALID_GRANT);
}
authorization.setRevoked(true);
authorization = authorizationDao.updateAuthorization(authorization);
-
+
return authorization;
}
public void addTotalAttempts (Authorization authorization) {
int totalAttempts = authorization.getTotalAttempts() + 1;
- if (totalAttempts > config.getMaxAuthenticationAttempts()) {
+ if (totalAttempts == config.getMaxAuthenticationAttempts()) {
authorization.setRevoked(true);
}
- else {
- authorization.setTotalAttempts(totalAttempts);
- }
+ authorization.setTotalAttempts(totalAttempts);
authorizationDao.updateAuthorization(authorization);
}
private boolean isExpired (ZonedDateTime createdDate) {
jlog.debug("createdDate: " + createdDate);
- ZonedDateTime expiration = createdDate.plusMinutes(10);
+ ZonedDateTime expiration = createdDate.plusSeconds(60);
ZonedDateTime now = ZonedDateTime.now();
jlog.debug("expiration: " + expiration + ", now: " + now);
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
new file mode 100644
index 0000000..15b51e3
--- /dev/null
+++ b/full/src/main/java/de/ids_mannheim/korap/oauth2/service/OAuth2ScopeService.java
@@ -0,0 +1,74 @@
+package de.ids_mannheim.korap.oauth2.service;
+
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+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.dao.AccessScopeDao;
+import de.ids_mannheim.korap.oauth2.entity.AccessScope;
+
+@Service
+public class OAuth2ScopeService {
+
+ @Autowired
+ private AccessScopeDao accessScopeDao;
+
+ /**
+ * Converts a set of scope strings to a set of {@link AccessScope}
+ *
+ * @param scopes
+ * @return
+ * @throws KustvaktException
+ */
+ public Set<AccessScope> convertToAccessScope (Set<String> scopes)
+ throws KustvaktException {
+
+ List<AccessScope> definedScopes = accessScopeDao.retrieveAccessScopes();
+ Set<AccessScope> requestedScopes =
+ new HashSet<AccessScope>(scopes.size());
+ int index;
+ for (String scope : scopes) {
+ index = definedScopes.indexOf(new AccessScope(scope));
+ if (index == -1) {
+ throw new KustvaktException(StatusCodes.INVALID_SCOPE,
+ scope + " is an invalid scope",
+ OAuth2Error.INVALID_SCOPE);
+ }
+ else {
+ requestedScopes.add(definedScopes.get(index));
+ }
+ }
+ return requestedScopes;
+ }
+
+ public String convertAccessScopesToString (Set<AccessScope> scopes) {
+ Set<String> set = scopes.stream().map(scope -> scope.toString())
+ .collect(Collectors.toSet());
+ return String.join(" ", set);
+ }
+
+ /**
+ * Simple reduction of requested scopes, i.e. excluding any scopes
+ * that are not default scopes for a specific authorization grant.
+ *
+ * @param scopes
+ * @param defaultScopes
+ * @return accepted scopes
+ */
+ public Set<String> filterScopes (Set<String> scopes,
+ Set<String> defaultScopes) {
+ Stream<String> stream = scopes.stream();
+ Set<String> filteredScopes =
+ stream.filter(scope -> defaultScopes.contains(scope))
+ .collect(Collectors.toSet());
+ return filteredScopes;
+ }
+}
diff --git a/full/src/main/java/de/ids_mannheim/korap/oauth2/service/OAuth2TokenService.java b/full/src/main/java/de/ids_mannheim/korap/oauth2/service/OAuth2TokenService.java
index a2d1ba5..506fde8 100644
--- a/full/src/main/java/de/ids_mannheim/korap/oauth2/service/OAuth2TokenService.java
+++ b/full/src/main/java/de/ids_mannheim/korap/oauth2/service/OAuth2TokenService.java
@@ -23,6 +23,7 @@
import de.ids_mannheim.korap.interfaces.AuthenticationManagerIface;
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.Authorization;
import de.ids_mannheim.korap.oauth2.entity.OAuth2Client;
@@ -34,6 +35,8 @@
@Autowired
private OAuth2AuthorizationService authorizationService;
@Autowired
+ private OAuth2ScopeService scopeService;
+ @Autowired
private AccessTokenDao tokenDao;
@Autowired
@@ -86,7 +89,8 @@
* @param clientSecret
* clilent_secret, required if client_secret was issued
* for the client in client registration.
- * @return
+ * @return an OAuthResponse containing an access token if
+ * successful
* @throws OAuthSystemException
* @throws KustvaktException
*/
@@ -95,9 +99,17 @@
String clientSecret)
throws KustvaktException, OAuthSystemException {
- clientService.authenticateClient(clientId, clientSecret);
- Authorization authorization = authorizationService
- .verifyAuthorization(authorizationCode, clientId, redirectURI);
+ Authorization authorization =
+ authorizationService.retrieveAuthorization(authorizationCode);
+ try {
+ clientService.authenticateClient(clientId, clientSecret);
+ authorization = authorizationService
+ .verifyAuthorization(authorization, clientId, redirectURI);
+ }
+ catch (KustvaktException e) {
+ authorizationService.addTotalAttempts(authorization);
+ throw e;
+ }
return createsAccessTokenResponse(authorization);
}
@@ -126,7 +138,8 @@
* @param clientSecret
* clilent_secret, required if client_secret was issued
* for the client in client registration.
- * @return
+ * @return an OAuthResponse containing an access token if
+ * successful
* @throws KustvaktException
* @throws OAuthSystemException
*/
@@ -144,7 +157,8 @@
}
authenticateUser(username, password, scopes);
- return createsAccessTokenResponse();
+ // verify or limit scopes ?
+ return createsAccessTokenResponse(scopes);
}
public void authenticateUser (String username, String password,
@@ -169,13 +183,15 @@
/**
* Clients must authenticate.
+ * Client credentials grant is limited to native clients.
*
* @param clientId
* client_id parameter, required
* @param clientSecret
* client_secret parameter, required
* @param scopes
- * @return
+ * @return an OAuthResponse containing an access token if
+ * successful
* @throws KustvaktException
* @throws OAuthSystemException
*/
@@ -190,10 +206,21 @@
OAuth2Error.INVALID_REQUEST);
}
+ // OAuth2Client client =
clientService.authenticateClient(clientId, clientSecret);
- return createsAccessTokenResponse();
- }
+ // if (client.isNative()) {
+ // throw new KustvaktException(
+ // StatusCodes.CLIENT_AUTHENTICATION_FAILED,
+ // "Client credentials grant is not allowed for third party
+ // clients",
+ // OAuth2Error.UNAUTHORIZED_CLIENT);
+ // }
+
+ scopes = scopeService.filterScopes(scopes,
+ config.getClientCredentialsScopes());
+ return createsAccessTokenResponse(scopes);
+ }
/**
* Creates an OAuthResponse containing an access token and a
@@ -201,27 +228,45 @@
*
* @return an OAuthResponse containing an access token
* @throws OAuthSystemException
+ * @throws KustvaktException
*/
- private OAuthResponse createsAccessTokenResponse ()
- throws OAuthSystemException {
- return createsAccessTokenResponse(null);
+ private OAuthResponse createsAccessTokenResponse (Set<String> scopes)
+ throws OAuthSystemException, KustvaktException {
+
+ String accessToken = oauthIssuer.accessToken();
+ // String refreshToken = oauthIssuer.refreshToken();
+
+ Set<AccessScope> accessScopes =
+ scopeService.convertToAccessScope(scopes);
+ tokenDao.storeAccessToken(accessToken, accessScopes);
+
+ return OAuthASResponse.tokenResponse(Status.OK.getStatusCode())
+ .setAccessToken(accessToken)
+ .setTokenType(TokenType.BEARER.toString())
+ .setExpiresIn(String.valueOf(config.getTokenTTL()))
+ // .setRefreshToken(refreshToken)
+ .setScope(String.join(" ", scopes)).buildJSONMessage();
}
private OAuthResponse createsAccessTokenResponse (
- Authorization authorization) throws OAuthSystemException {
+ Authorization authorization)
+ throws OAuthSystemException, KustvaktException {
String accessToken = oauthIssuer.accessToken();
- String refreshToken = oauthIssuer.refreshToken();
+ // String refreshToken = oauthIssuer.refreshToken();
tokenDao.storeAccessToken(authorization, accessToken);
+ String scopes = scopeService
+ .convertAccessScopesToString(authorization.getScopes());
+
OAuthResponse r =
OAuthASResponse.tokenResponse(Status.OK.getStatusCode())
.setAccessToken(accessToken)
.setTokenType(TokenType.BEARER.toString())
.setExpiresIn(String.valueOf(config.getTokenTTL()))
- .setRefreshToken(refreshToken).buildJSONMessage();
- // scope
+ // .setRefreshToken(refreshToken)
+ .setScope(scopes).buildJSONMessage();
return r;
}
}
diff --git a/full/src/main/java/de/ids_mannheim/korap/web/controller/OAuthClientController.java b/full/src/main/java/de/ids_mannheim/korap/web/controller/OAuthClientController.java
index 05c0bd0..d272f73 100644
--- a/full/src/main/java/de/ids_mannheim/korap/web/controller/OAuthClientController.java
+++ b/full/src/main/java/de/ids_mannheim/korap/web/controller/OAuthClientController.java
@@ -112,13 +112,18 @@
}
+ /** Deregisters confidential clients. Clients must authenticate.
+ *
+ * @param securityContext
+ * @param request
+ * @param form
+ * @return
+ */
@DELETE
@Path("deregister/confidential")
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
public Response deregisterConfidentialClient (
@Context SecurityContext securityContext,
- // @HeaderParam("Authorization") String authorization,
- // @FormParam("client_id") String clientId
@Context HttpServletRequest request,
MultivaluedMap<String, String> form) {
try {
diff --git a/full/src/main/resources/db/insert/V3.5__insert_oauth2_clients.sql b/full/src/main/resources/db/insert/V3.5__insert_oauth2_clients.sql
index f9cd171..b42ae28 100644
--- a/full/src/main/resources/db/insert/V3.5__insert_oauth2_clients.sql
+++ b/full/src/main/resources/db/insert/V3.5__insert_oauth2_clients.sql
@@ -10,7 +10,7 @@
"This is a test native confidential client.");
-- plain secret value is "secret"
-INSERT INTO oauth2_client(id,name,secret,type,native, url,url_hashcode,
+INSERT INTO oauth2_client(id,name,secret,type,native,url,url_hashcode,
redirect_uri,registered_by, description)
VALUES ("9aHsGW6QflV13ixNpez","test non native confidential client",
"$2a$08$vi1FbuN3p6GcI1tSxMAoeuIYL8Yw3j6A8wJthaN8ZboVnrQaTwLPq",
@@ -18,10 +18,10 @@
"https://third.party.com/confidential/redirect", "system",
"This is a test nonnative confidential client.");
-INSERT INTO oauth2_client(id,name,secret,type,url,url_hashcode,
+INSERT INTO oauth2_client(id,name,secret,type,native,url,url_hashcode,
redirect_uri, registered_by, description)
VALUES ("8bIDtZnH6NvRkW2Fq","third party client",null,
- "PUBLIC","http://third.party.client.com", -2137275617,
+ "PUBLIC", 0,"http://third.party.client.com", -2137275617,
"https://third.party.client.com/redirect","system",
"This is a test nonnative public client.");
diff --git a/full/src/main/resources/kustvakt.conf b/full/src/main/resources/kustvakt.conf
index 99e3b94..75a3483 100644
--- a/full/src/main/resources/kustvakt.conf
+++ b/full/src/main/resources/kustvakt.conf
@@ -47,6 +47,11 @@
### (see de.ids_mannheim.korap.constant.AuthenticationMethod for possible
### oauth.password.authentication values)
oauth.password.authentication = TEST
+oauth2.native.client.host = korap.ids-mannheim.de
+oauth2.max.attempts = 3
+# -- scopes separated by space
+oauth2.default.scopes = read_username read_email
+oauth2.client.credentials.scopes = read_client_info
# JWT
security.jwt.issuer=korap.ids-mannheim.de
diff --git a/full/src/test/java/de/ids_mannheim/korap/config/SpringJerseyTest.java b/full/src/test/java/de/ids_mannheim/korap/config/SpringJerseyTest.java
index 619b795..246e14e 100644
--- a/full/src/test/java/de/ids_mannheim/korap/config/SpringJerseyTest.java
+++ b/full/src/test/java/de/ids_mannheim/korap/config/SpringJerseyTest.java
@@ -11,6 +11,7 @@
import org.springframework.context.support.GenericApplicationContext;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
+import org.springframework.web.context.ContextLoaderListener;
import org.springframework.web.context.support.AbstractRefreshableWebApplicationContext;
import com.sun.jersey.spi.spring.container.servlet.SpringServlet;
@@ -72,8 +73,9 @@
return new WebAppDescriptor.Builder(classPackages)
.servletClass(SpringServlet.class)
.contextListenerClass(StaticContextLoaderListener.class)
- // .contextParam("contextConfigLocation",
- // "classpath:test-config.xml")
+// .contextListenerClass(ContextLoaderListener.class)
+// .contextParam("contextConfigLocation",
+// "classpath:test-config.xml")
.build();
}
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 ca4d0ee..216d7a1 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
@@ -18,12 +18,14 @@
import com.fasterxml.jackson.databind.JsonNode;
import com.google.common.net.HttpHeaders;
import com.sun.jersey.api.client.ClientResponse;
+import com.sun.jersey.api.uri.UriComponent;
import com.sun.jersey.core.util.MultivaluedMapImpl;
import de.ids_mannheim.korap.authentication.http.HttpAuthorizationHandler;
import de.ids_mannheim.korap.config.Attributes;
import de.ids_mannheim.korap.config.SpringJerseyTest;
import de.ids_mannheim.korap.exceptions.KustvaktException;
+import de.ids_mannheim.korap.oauth2.constant.OAuth2Error;
import de.ids_mannheim.korap.utils.JsonUtils;
/**
@@ -34,7 +36,7 @@
@Autowired
private HttpAuthorizationHandler handler;
-
+
private ClientResponse requestAuthorizationConfidentialClient (
MultivaluedMap<String, String> form) throws KustvaktException {
@@ -127,6 +129,25 @@
node.at("/error_description").asText());
}
+ @Test
+ public void testAuthorizeInvalidScope () throws KustvaktException {
+ MultivaluedMap<String, String> form = new MultivaluedMapImpl();
+ form.add("response_type", "code");
+ form.add("client_id", "fCBbQkAyYzI4NzUxMg");
+ form.add("username", "dory");
+ form.add("password", "password");
+ form.add("scope", "read_address");
+
+ ClientResponse response = requestAuthorizationConfidentialClient(form);
+ assertEquals(Status.BAD_REQUEST.getStatusCode(), response.getStatus());
+
+ String entity = response.getEntity(String.class);
+ JsonNode node = JsonUtils.readTree(entity);
+ assertEquals(OAuth2Error.INVALID_SCOPE, node.at("/error").asText());
+ assertEquals("read_address is an invalid scope",
+ node.at("/error_description").asText());
+ }
+
private ClientResponse requestToken (MultivaluedMap<String, String> form)
throws KustvaktException {
return resource().path("oauth2").path("token")
@@ -139,24 +160,30 @@
@Test
public void testRequestTokenAuthorizationConfidential ()
throws KustvaktException {
-
+
MultivaluedMap<String, String> authForm = new MultivaluedMapImpl();
authForm.add("response_type", "code");
authForm.add("client_id", "fCBbQkAyYzI4NzUxMg");
authForm.add("username", "dory");
authForm.add("password", "password");
authForm.add("scope", "read_username");
-
- ClientResponse response = requestAuthorizationConfidentialClient(authForm);
+
+ ClientResponse response =
+ requestAuthorizationConfidentialClient(authForm);
URI redirectUri = response.getLocation();
- String code = redirectUri.getQuery().split("=")[1];
-
+ MultivaluedMap<String, String> params =
+ UriComponent.decodeQuery(redirectUri, true);
+ String code = params.get("code").get(0);
+ String scopes = params.get("scope").get(0);
+
+ assertEquals(scopes, "read_username");
+
MultivaluedMap<String, String> tokenForm = new MultivaluedMapImpl();
tokenForm.add("grant_type", "authorization_code");
tokenForm.add("client_id", "fCBbQkAyYzI4NzUxMg");
tokenForm.add("client_secret", "secret");
tokenForm.add("code", code);
-
+
response = requestToken(tokenForm);
String entity = response.getEntity(String.class);
JsonNode node = JsonUtils.readTree(entity);
@@ -165,8 +192,115 @@
assertEquals(TokenType.BEARER.toString(),
node.at("/token_type").asText());
assertNotNull(node.at("/expires_in").asText());
+
+ testRequestTokenWithUsedAuthorization(tokenForm);
}
-
+
+ private void testRequestTokenWithUsedAuthorization (
+ MultivaluedMap<String, String> form) throws KustvaktException {
+ 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_GRANT,
+ node.at("/error").asText());
+ assertEquals("Invalid authorization",
+ node.at("/error_description").asText());
+ }
+
+ @Test
+ public void testRequestTokenInvalidAuthorizationCode ()
+ throws KustvaktException {
+ MultivaluedMap<String, String> tokenForm = new MultivaluedMapImpl();
+ tokenForm.add("grant_type", "authorization_code");
+ tokenForm.add("client_id", "fCBbQkAyYzI4NzUxMg");
+ tokenForm.add("client_secret", "secret");
+ tokenForm.add("code", "blahblah");
+
+ ClientResponse response = requestToken(tokenForm);
+ 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());
+ }
+
+ @Test
+ public void testRequestTokenAuthorizationReplyAttack ()
+ throws KustvaktException {
+ String uri = "https://korap.ids-mannheim.de/confidential/redirect";
+ MultivaluedMap<String, String> authForm = new MultivaluedMapImpl();
+ authForm.add("response_type", "code");
+ authForm.add("client_id", "fCBbQkAyYzI4NzUxMg");
+ authForm.add("username", "dory");
+ authForm.add("password", "password");
+ authForm.add("scope", "read_username");
+ authForm.add("redirect_uri", uri);
+
+ ClientResponse response =
+ requestAuthorizationConfidentialClient(authForm);
+ URI redirectUri = response.getLocation();
+ MultivaluedMap<String, String> params =
+ UriComponent.decodeQuery(redirectUri, true);
+ String code = params.get("code").get(0);
+
+ testRequestTokenAuthorizationInvalidClient(code);
+ testRequestTokenAuthorizationInvalidRedirectUri(code);
+ testRequestTokenAuthorizationRevoked(code, uri);
+ }
+
+ private void testRequestTokenAuthorizationInvalidClient (String code)
+ throws KustvaktException {
+ MultivaluedMap<String, String> tokenForm = new MultivaluedMapImpl();
+ tokenForm.add("grant_type", "authorization_code");
+ tokenForm.add("client_id", "fCBbQkAyYzI4NzUxMg");
+ tokenForm.add("client_secret", "blah");
+ tokenForm.add("code", code);
+
+ ClientResponse response = requestToken(tokenForm);
+ String entity = response.getEntity(String.class);
+ JsonNode node = JsonUtils.readTree(entity);
+ assertEquals(OAuth2Error.INVALID_CLIENT, node.at("/error").asText());
+ }
+
+ private void testRequestTokenAuthorizationInvalidRedirectUri (String code)
+ throws KustvaktException {
+ MultivaluedMap<String, String> tokenForm = new MultivaluedMapImpl();
+ tokenForm.add("grant_type", "authorization_code");
+ tokenForm.add("client_id", "fCBbQkAyYzI4NzUxMg");
+ tokenForm.add("client_secret", "secret");
+ tokenForm.add("code", code);
+ tokenForm.add("redirect_uri", "https://blahblah.com");
+
+ ClientResponse response = requestToken(tokenForm);
+ String entity = response.getEntity(String.class);
+ JsonNode node = JsonUtils.readTree(entity);
+ assertEquals(OAuth2Error.INVALID_GRANT, node.at("/error").asText());
+ }
+
+ private void testRequestTokenAuthorizationRevoked (String code, String uri)
+ throws KustvaktException {
+ MultivaluedMap<String, String> tokenForm = new MultivaluedMapImpl();
+ tokenForm.add("grant_type", "authorization_code");
+ tokenForm.add("client_id", "fCBbQkAyYzI4NzUxMg");
+ tokenForm.add("client_secret", "secret");
+ tokenForm.add("code", code);
+ tokenForm.add("redirect_uri", uri);
+
+ ClientResponse response = requestToken(tokenForm);
+ String entity = response.getEntity(String.class);
+ JsonNode node = JsonUtils.readTree(entity);
+ assertEquals(OAuthError.TokenResponse.INVALID_GRANT,
+ node.at("/error").asText());
+ assertEquals("Invalid authorization",
+ node.at("/error_description").asText());
+ }
+
+
@Test
public void testRequestTokenPasswordGrantConfidential ()
throws KustvaktException {
@@ -228,7 +362,7 @@
assertEquals("Missing parameters: client_id",
node.at("/error_description").asText());
}
-
+
@Test
public void testRequestTokenPasswordGrantPublic ()
throws KustvaktException {
@@ -293,6 +427,30 @@
}
@Test
+ public void testRequestTokenClientCredentialsGrantReducedScope ()
+ throws KustvaktException {
+
+ MultivaluedMap<String, String> form = new MultivaluedMapImpl();
+ form.add("grant_type", "client_credentials");
+ form.add("client_id", "fCBbQkAyYzI4NzUxMg");
+ form.add("client_secret", "secret");
+ form.add("scope", "read_username read_client_info");
+
+ ClientResponse response = requestToken(form);
+ String entity = response.getEntity(String.class);
+ assertEquals(Status.OK.getStatusCode(), response.getStatus());
+
+ JsonNode node = JsonUtils.readTree(entity);
+ // length?
+ 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());
+ assertEquals("read_client_info", node.at("/scope").asText());
+ }
+
+ @Test
public void testRequestTokenMissingGrantType () throws KustvaktException {
MultivaluedMap<String, String> form = new MultivaluedMapImpl();
ClientResponse response = requestToken(form);
diff --git a/full/src/test/resources/kustvakt-test.conf b/full/src/test/resources/kustvakt-test.conf
index b26384b..d9eda87 100644
--- a/full/src/test/resources/kustvakt-test.conf
+++ b/full/src/test/resources/kustvakt-test.conf
@@ -48,6 +48,10 @@
### oauth.password.authentication values)
oauth.password.authentication = TEST
oauth.native.client.host=korap.ids-mannheim.de
+oauth2.max.attempts = 2
+# -- scopes separated by space
+oauth2.default.scopes = read_username read_email
+oauth2.client.credentials.scopes = read_client_info
# JWT
security.jwt.issuer=korap.ids-mannheim.de