Implemented OAuth2 token request with password grant using Nimbus lib.
Change-Id: I0bde6a11c12c03badd18c32d77eebc24a1b3a443
diff --git a/full/Changes b/full/Changes
index dc048a4..d33aeca 100644
--- a/full/Changes
+++ b/full/Changes
@@ -1,5 +1,5 @@
version 0.60.4
-27/06/2018
+28/06/2018
- implemented OAuth2 authorization code request with OpenID Authentication (margaretha)
- enabled OAuth2 authorization without OpenID authentication using Nimbus library (margaretha)
- implemented response handler for OpenID authentication errors in authorization requests (margaretha)
@@ -12,6 +12,7 @@
- implemented OpenId configuration (margaretha)
- added authentication time and support for auth_time in id_token (margaretha)
- implemented support for nonce and max_age parameters in OpenID authentication (margaretha)
+ - implemented OAuth2 token request with password grant using Nimbus library (margaretha)
version 0.60.3
06/06/2018
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 913db9f..4e89d5e 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,7 @@
if (grantType.equals(GrantType.AUTHORIZATION_CODE.toString())) {
Authorization authorization =
- requestAccessTokenWithAuthorizationCode(
+ retrieveAuthorization(
oAuthRequest.getCode(),
oAuthRequest.getRedirectURI(),
oAuthRequest.getClientId(),
@@ -48,7 +48,7 @@
return createsAccessTokenResponse(authorization);
}
else if (grantType.equals(GrantType.PASSWORD.toString())) {
- ZonedDateTime authenticationTime = requestAccessTokenWithPassword(
+ ZonedDateTime authenticationTime = authenticateClientAndUser(
oAuthRequest.getUsername(), oAuthRequest.getPassword(),
oAuthRequest.getScopes(), oAuthRequest.getClientId(),
oAuthRequest.getClientSecret());
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 0d943c0..eca6fa2 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
@@ -5,6 +5,7 @@
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.Date;
+import java.util.HashSet;
import java.util.Set;
import org.springframework.stereotype.Service;
@@ -21,6 +22,7 @@
import com.nimbusds.oauth2.sdk.AuthorizationGrant;
import com.nimbusds.oauth2.sdk.GrantType;
import com.nimbusds.oauth2.sdk.ParseException;
+import com.nimbusds.oauth2.sdk.ResourceOwnerPasswordCredentialsGrant;
import com.nimbusds.oauth2.sdk.Scope;
import com.nimbusds.oauth2.sdk.TokenRequest;
import com.nimbusds.oauth2.sdk.auth.ClientAuthentication;
@@ -28,6 +30,7 @@
import com.nimbusds.oauth2.sdk.auth.ClientSecretBasic;
import com.nimbusds.oauth2.sdk.auth.ClientSecretPost;
import com.nimbusds.oauth2.sdk.id.Audience;
+import com.nimbusds.oauth2.sdk.id.ClientID;
import com.nimbusds.oauth2.sdk.id.Issuer;
import com.nimbusds.oauth2.sdk.id.Subject;
import com.nimbusds.oauth2.sdk.token.AccessToken;
@@ -71,26 +74,18 @@
GrantType grantType = grant.getType();
ClientAuthentication clientAuthentication =
tokenRequest.getClientAuthentication();
- String[] clientCredentials =
- extractClientCredentials(clientAuthentication);
+ ClientID clientId = tokenRequest.getClientID();
if (grantType.equals(GrantType.AUTHORIZATION_CODE)) {
- AuthorizationCodeGrant codeGrant = (AuthorizationCodeGrant) grant;
- String authorizationCode =
- codeGrant.getAuthorizationCode().getValue();
- URI redirectionURI = codeGrant.getRedirectionURI();
- String redirectURI = null;
- if (redirectionURI != null) {
- redirectURI = redirectionURI.toString();
- }
- Authorization authorization =
- requestAccessTokenWithAuthorizationCode(authorizationCode,
- redirectURI, clientCredentials[0],
- clientCredentials[1]);
- return createsAccessTokenResponse(authorization);
+ return requestAccessTokenWithAuthorizationCode(grant,
+ clientAuthentication, clientId);
}
else if (grantType.equals(GrantType.PASSWORD)) {
-
+ ResourceOwnerPasswordCredentialsGrant passwordGrant =
+ (ResourceOwnerPasswordCredentialsGrant) grant;
+ return requestAccessTokenWithPassword(passwordGrant.getUsername(),
+ passwordGrant.getPassword().getValue(),
+ tokenRequest.getScope(), clientAuthentication, clientId);
}
else if (grantType.equals(GrantType.CLIENT_CREDENTIALS)) {
@@ -103,6 +98,75 @@
return null;
}
+ private AccessTokenResponse requestAccessTokenWithPassword (String username,
+ String password, Scope scope,
+ ClientAuthentication clientAuthentication, ClientID clientId)
+ throws KustvaktException {
+
+ Set<String> scopes = null;
+ if (scope != null) {
+ scopes = new HashSet<String>();
+ scopes.addAll(scope.toStringList());
+ }
+
+ ZonedDateTime authenticationTime;
+ String clientIdStr = null;
+ if (clientAuthentication == null) {
+ if (clientId == null) {
+ throw new KustvaktException(StatusCodes.MISSING_PARAMETER,
+ "Missing parameters: client_id",
+ OAuth2Error.INVALID_REQUEST);
+ }
+ else {
+ authenticationTime = authenticateClientAndUser(username,
+ password, scopes, clientId.getValue(), null);
+ clientIdStr = clientId.getValue();
+ }
+ }
+ else {
+ String[] clientCredentials =
+ extractClientCredentials(clientAuthentication);
+ clientIdStr = clientCredentials[0];
+ authenticationTime = authenticateClientAndUser(username, password,
+ scopes, clientCredentials[0], clientCredentials[1]);
+ }
+ return createsAccessTokenResponse(scope, clientIdStr, username,
+ authenticationTime, null);
+ }
+
+ private AccessTokenResponse requestAccessTokenWithAuthorizationCode (
+ AuthorizationGrant grant, ClientAuthentication clientAuthentication,
+ ClientID clientId) throws KustvaktException {
+ AuthorizationCodeGrant codeGrant = (AuthorizationCodeGrant) grant;
+ String authorizationCode = codeGrant.getAuthorizationCode().getValue();
+ URI redirectionURI = codeGrant.getRedirectionURI();
+ String redirectURI = null;
+ if (redirectionURI != null) {
+ redirectURI = redirectionURI.toString();
+ }
+
+ Authorization authorization = null;
+ if (clientAuthentication == null) {
+ if (clientId == null) {
+ throw new KustvaktException(StatusCodes.MISSING_PARAMETER,
+ "Missing parameters: client_id",
+ OAuth2Error.INVALID_REQUEST);
+ }
+ else {
+ authorization = retrieveAuthorization(authorizationCode,
+ redirectURI, clientId.getValue(), null);
+ }
+ }
+ else {
+ String[] clientCredentials =
+ extractClientCredentials(clientAuthentication);
+ authorization = retrieveAuthorization(authorizationCode,
+ redirectURI, clientCredentials[0], clientCredentials[1]);
+ }
+
+ return createsAccessTokenResponse(authorization);
+
+ }
private AccessTokenResponse createsAccessTokenResponse (
Authorization authorization) throws KustvaktException {
@@ -110,15 +174,24 @@
String[] scopeArray = scopes.stream().map(scope -> scope.toString())
.toArray(String[]::new);
Scope scope = new Scope(scopeArray);
+ return createsAccessTokenResponse(scope, authorization.getClientId(),
+ authorization.getUserId(),
+ authorization.getUserAuthenticationTime(),
+ authorization.getNonce());
+ }
+
+ private AccessTokenResponse createsAccessTokenResponse (Scope scope,
+ String clientId, String userId,
+ ZonedDateTime userAuthenticationTime, String nonce)
+ throws KustvaktException {
+
AccessToken accessToken =
new BearerAccessToken(config.getTokenTTL(), scope);
RefreshToken refreshToken = new RefreshToken();
- if (scope.contains("openid")) {
- JWTClaimsSet claims = createIdTokenClaims(
- authorization.getClientId(), authorization.getUserId(),
- authorization.getUserAuthenticationTime(),
- authorization.getNonce());
+ if (scope != null && scope.contains("openid")) {
+ JWTClaimsSet claims = createIdTokenClaims(clientId, userId,
+ userAuthenticationTime, nonce);
SignedJWT idToken = signIdToken(claims,
// default
new JWSHeader(JWSAlgorithm.RS256),
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 fd251be..d36a077 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
@@ -54,13 +54,13 @@
* @param clientId
* required if there is no authorization header
* @param clientSecret
- * clilent_secret, required if client_secret was issued
+ * client_secret, required if client_secret was issued
* for the client in client registration.
* @return an authorization
* @throws OAuthSystemException
* @throws KustvaktException
*/
- protected Authorization requestAccessTokenWithAuthorizationCode (
+ protected Authorization retrieveAuthorization (
String authorizationCode, String redirectURI, String clientId,
String clientSecret) throws KustvaktException {
@@ -107,7 +107,7 @@
* @throws KustvaktException
* @throws OAuthSystemException
*/
- protected ZonedDateTime requestAccessTokenWithPassword (String username,
+ protected ZonedDateTime authenticateClientAndUser (String username,
String password, Set<String> scopes, String clientId,
String clientSecret) throws KustvaktException {
diff --git a/full/src/main/java/de/ids_mannheim/korap/web/OpenIdResponseHandler.java b/full/src/main/java/de/ids_mannheim/korap/web/OpenIdResponseHandler.java
index 32152de..3729bd2 100644
--- a/full/src/main/java/de/ids_mannheim/korap/web/OpenIdResponseHandler.java
+++ b/full/src/main/java/de/ids_mannheim/korap/web/OpenIdResponseHandler.java
@@ -10,7 +10,6 @@
import javax.ws.rs.core.Response.Status;
import org.apache.http.HttpHeaders;
-import org.apache.http.HttpStatus;
import org.springframework.stereotype.Service;
import com.nimbusds.oauth2.sdk.AccessTokenResponse;
@@ -24,7 +23,6 @@
import com.nimbusds.openid.connect.sdk.AuthenticationErrorResponse;
import de.ids_mannheim.korap.exceptions.KustvaktException;
-import de.ids_mannheim.korap.exceptions.StatusCodes;
import de.ids_mannheim.korap.interfaces.db.AuditingIface;
import de.ids_mannheim.korap.oauth2.constant.OAuth2Error;
import net.minidev.json.JSONObject;
@@ -133,15 +131,14 @@
&& !errorCode.equals("[]")) {
errorObject = new ErrorObject(e.getEntity(), e.getMessage());
}
- else{
+ else {
throw throwit(e);
}
}
return errorObject;
}
- public Response createAuthorizationErrorResponse (ParseException e,
- boolean isAuthentication, State state) {
+ public Response createErrorResponse (ParseException e, State state) {
ErrorObject errorObject = e.getErrorObject();
if (errorObject == null) {
errorObject = com.nimbusds.oauth2.sdk.OAuth2Error.INVALID_REQUEST;
@@ -160,20 +157,25 @@
}
- public void createTokenErrorResponse (KustvaktException e) {
+ public Response createTokenErrorResponse (KustvaktException e) {
String errorCode = e.getEntity();
ErrorObject errorObject = tokenErrorObjectMap.get(errorCode);
if (errorObject == null) {
errorObject = errorObjectMap.get(errorCode);
- if (errorObject == null) {
+ if (errorCode != null && !errorCode.isEmpty()
+ && !errorCode.equals("[]")) {
errorObject = new ErrorObject(e.getEntity(), e.getMessage());
}
+ else {
+ throw throwit(e);
+ }
}
+ errorObject = errorObject.setDescription(e.getMessage());
TokenErrorResponse errorResponse = new TokenErrorResponse(errorObject);
Status status = determineErrorStatus(errorCode);
- createResponse(errorResponse, status);
+ return createResponse(errorResponse, status);
}
public Response createResponse (AccessTokenResponse tokenResponse,
diff --git a/full/src/main/java/de/ids_mannheim/korap/web/controller/OAuth2WithOpenIdController.java b/full/src/main/java/de/ids_mannheim/korap/web/controller/OAuth2WithOpenIdController.java
index a579866..141e8ec 100644
--- a/full/src/main/java/de/ids_mannheim/korap/web/controller/OAuth2WithOpenIdController.java
+++ b/full/src/main/java/de/ids_mannheim/korap/web/controller/OAuth2WithOpenIdController.java
@@ -142,8 +142,7 @@
isAuthentication, authTime);
}
catch (ParseException e) {
- return openIdResponseHandler.createAuthorizationErrorResponse(e,
- isAuthentication, state);
+ return openIdResponseHandler.createErrorResponse(e, state);
}
catch (KustvaktException e) {
return openIdResponseHandler.createAuthorizationErrorResponse(e,
@@ -186,15 +185,11 @@
Status.OK);
}
catch (ParseException e) {
- // TODO Auto-generated catch block
- e.printStackTrace();
+ return openIdResponseHandler.createErrorResponse(e, null);
}
catch (KustvaktException e) {
- openIdResponseHandler.createTokenErrorResponse(e);
+ return openIdResponseHandler.createTokenErrorResponse(e);
}
-
- return null;
-
}
/**
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 9032c67..efdab41 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
@@ -30,6 +30,7 @@
import com.nimbusds.jose.jwk.RSAKey;
import com.nimbusds.jwt.JWTClaimsSet;
import com.nimbusds.jwt.SignedJWT;
+import com.nimbusds.oauth2.sdk.GrantType;
import com.sun.jersey.api.client.ClientHandlerException;
import com.sun.jersey.api.client.ClientResponse;
import com.sun.jersey.api.client.UniformInterfaceException;
@@ -286,7 +287,7 @@
}
@Test
- public void testRequestAccessToken ()
+ public void testRequestAccessTokenWithAuthorizationCode ()
throws KustvaktException, ParseException, InvalidKeySpecException,
NoSuchAlgorithmException, JOSEException {
String client_id = "fCBbQkAyYzI4NzUxMg";
@@ -307,15 +308,17 @@
String code = params.getFirst("code");
MultivaluedMap<String, String> tokenForm = new MultivaluedMapImpl();
+ testRequestAccessTokenMissingGrant(tokenForm);
tokenForm.add("grant_type", "authorization_code");
- tokenForm.add("redirect_uri", redirectUri);
- tokenForm.add("client_id", client_id);
- tokenForm.add("client_secret", "secret");
tokenForm.add("code", code);
+ testRequestAccessTokenMissingClientId(tokenForm);
+ tokenForm.add("client_id", client_id);
+ testRequestAccessTokenMissingClientSecret(tokenForm);
+ tokenForm.add("client_secret", "secret");
+ tokenForm.add("redirect_uri", redirectUri);
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());
@@ -328,6 +331,36 @@
verifyingIdToken(id_token, username, client_id, nonce);
}
+ private void testRequestAccessTokenMissingGrant (
+ MultivaluedMap<String, String> tokenForm) throws KustvaktException {
+ ClientResponse response = sendTokenRequest(tokenForm);
+ String entity = response.getEntity(String.class);
+ JsonNode node = JsonUtils.readTree(entity);
+ assertEquals(OAuth2Error.INVALID_REQUEST, node.at("/error").asText());
+ assertEquals("Invalid request: Missing \"grant_type\" parameter",
+ node.at("/error_description").asText());
+ }
+
+ private void testRequestAccessTokenMissingClientId (
+ MultivaluedMap<String, String> tokenForm) throws KustvaktException {
+ ClientResponse response = sendTokenRequest(tokenForm);
+ String entity = response.getEntity(String.class);
+ JsonNode node = JsonUtils.readTree(entity);
+ assertEquals(OAuth2Error.INVALID_REQUEST, node.at("/error").asText());
+ assertEquals("Invalid request: Missing required \"client_id\" "
+ + "parameter", node.at("/error_description").asText());
+ }
+
+ private void testRequestAccessTokenMissingClientSecret (
+ MultivaluedMap<String, String> tokenForm) throws KustvaktException {
+ ClientResponse response = sendTokenRequest(tokenForm);
+ String entity = response.getEntity(String.class);
+ JsonNode node = JsonUtils.readTree(entity);
+ assertEquals(OAuth2Error.INVALID_REQUEST, node.at("/error").asText());
+ assertEquals("Missing parameters: client_secret",
+ node.at("/error_description").asText());
+ }
+
private void verifyingIdToken (String id_token, String username,
String client_id, String nonce) throws ParseException,
InvalidKeySpecException, NoSuchAlgorithmException, JOSEException {
@@ -347,6 +380,59 @@
assertEquals(nonce, claimsSet.getClaim("nonce"));
}
+
+ // no openid
+ @Test
+ public void testRequestAccessTokenWithPassword ()
+ throws KustvaktException, ParseException, InvalidKeySpecException,
+ NoSuchAlgorithmException, JOSEException {
+ // public client
+ String client_id = "iBr3LsTCxOj7D2o0A5m";
+ MultivaluedMap<String, String> tokenForm = new MultivaluedMapImpl();
+ testRequestAccessTokenMissingGrant(tokenForm);
+
+ tokenForm.add("grant_type", GrantType.PASSWORD.toString());
+ testRequestAccessTokenMissingUsername(tokenForm);
+
+ tokenForm.add("username", username);
+ testRequestAccessTokenMissingPassword(tokenForm);
+
+ tokenForm.add("password", "pass");
+ tokenForm.add("client_id", client_id);
+
+ ClientResponse tokenResponse = sendTokenRequest(tokenForm);
+ String entity = tokenResponse.getEntity(String.class);
+ System.out.println(entity);
+
+ 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 testRequestAccessTokenMissingUsername (
+ MultivaluedMap<String, String> tokenForm) throws KustvaktException {
+ ClientResponse response = sendTokenRequest(tokenForm);
+ String entity = response.getEntity(String.class);
+ JsonNode node = JsonUtils.readTree(entity);
+ assertEquals(OAuth2Error.INVALID_REQUEST, node.at("/error").asText());
+ assertEquals("Invalid request: Missing or empty \"username\" parameter",
+ node.at("/error_description").asText());
+ }
+
+ private void testRequestAccessTokenMissingPassword (
+ MultivaluedMap<String, String> tokenForm) throws KustvaktException {
+ ClientResponse response = sendTokenRequest(tokenForm);
+ String entity = response.getEntity(String.class);
+ JsonNode node = JsonUtils.readTree(entity);
+ assertEquals(OAuth2Error.INVALID_REQUEST, node.at("/error").asText());
+ assertEquals("Invalid request: Missing or empty \"password\" parameter",
+ node.at("/error_description").asText());
+ }
+
@Test
public void testPublicKeyAPI () throws KustvaktException {
ClientResponse response = resource().path("oauth2").path("openid")