blob: 8caafdcd94b4c345162f3e4cdb051cc906eeb1fc [file] [log] [blame]
package de.ids_mannheim.korap.web.controller;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import java.net.URI;
import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;
import java.text.ParseException;
import java.util.Date;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;
import org.apache.http.HttpStatus;
import org.apache.http.entity.ContentType;
import org.apache.oltu.oauth2.common.message.types.TokenType;
import org.junit.Ignore;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.MultiValueMap;
import org.springframework.web.util.UriComponentsBuilder;
import com.fasterxml.jackson.databind.JsonNode;
import com.google.common.net.HttpHeaders;
import com.nimbusds.jose.JOSEException;
import com.nimbusds.jose.JWSVerifier;
import com.nimbusds.jose.crypto.RSASSAVerifier;
import com.nimbusds.jose.jwk.JWKSet;
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;
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.FullConfiguration;
import de.ids_mannheim.korap.config.SpringJerseyTest;
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.utils.JsonUtils;
public class OAuth2OpenIdControllerTest extends SpringJerseyTest {
@Autowired
private HttpAuthorizationHandler handler;
@Autowired
private FullConfiguration config;
private String redirectUri =
"https://korap.ids-mannheim.de/confidential/redirect";
private String username = "dory";
private ClientResponse sendAuthorizationRequest (
MultivaluedMap<String, String> form) throws KustvaktException {
return resource().path("oauth2").path("openid").path("authorize")
.header(Attributes.AUTHORIZATION,
handler.createBasicAuthorizationHeaderValue(username,
"password"))
.header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
.header(HttpHeaders.CONTENT_TYPE,
ContentType.APPLICATION_FORM_URLENCODED)
.entity(form).post(ClientResponse.class);
}
private ClientResponse sendTokenRequest (
MultivaluedMap<String, String> form) throws KustvaktException {
return resource().path("oauth2").path("openid").path("token")
.header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
.header(HttpHeaders.CONTENT_TYPE,
ContentType.APPLICATION_FORM_URLENCODED)
.entity(form).post(ClientResponse.class);
}
@Test
public void testRequestAuthorizationCode ()
throws UniformInterfaceException, ClientHandlerException,
KustvaktException {
MultivaluedMap<String, String> form = new MultivaluedMapImpl();
form.add("response_type", "code");
form.add("client_id", "fCBbQkAyYzI4NzUxMg");
testRequestAuthorizationCodeWithoutOpenID(form, redirectUri);
form.add("scope", "openid");
testRequestAuthorizationCodeMissingRedirectUri(form);
testRequestAuthorizationCodeInvalidRedirectUri(form);
form.add("redirect_uri", redirectUri);
form.add("state", "thisIsMyState");
ClientResponse response = sendAuthorizationRequest(form);
URI location = response.getLocation();
assertEquals(redirectUri, location.getScheme() + "://"
+ location.getHost() + location.getPath());
MultiValueMap<String, String> params =
UriComponentsBuilder.fromUri(location).build().getQueryParams();
assertNotNull(params.getFirst("code"));
assertEquals("thisIsMyState", params.getFirst("state"));
}
@Ignore
// cannot be tested dynamically
public void testRequestAuthorizationCodeAuthenticationTooOld ()
throws KustvaktException {
MultivaluedMap<String, String> form = new MultivaluedMapImpl();
form.add("response_type", "code");
form.add("client_id", "fCBbQkAyYzI4NzUxMg");
form.add("redirect_uri", redirectUri);
form.add("scope", "openid");
form.add("max_age", "1");
ClientResponse response =
resource().path("oauth2").path("openid").path("authorize")
.header(Attributes.AUTHORIZATION,
"Bearer ")
.header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
.header(HttpHeaders.CONTENT_TYPE,
ContentType.APPLICATION_FORM_URLENCODED)
.entity(form).post(ClientResponse.class);
assertEquals(HttpStatus.SC_UNAUTHORIZED, response.getStatus());
String entity = response.getEntity(String.class);
JsonNode node = JsonUtils.readTree(entity);
assertEquals(StatusCodes.USER_REAUTHENTICATION_REQUIRED,
node.at("/errors/0/0").asInt());
assertEquals(
"User reauthentication is required because the authentication "
+ "time is too old according to max_age",
node.at("/errors/0/1").asText());
}
private void testRequestAuthorizationCodeWithoutOpenID (
MultivaluedMap<String, String> form, String redirectUri)
throws KustvaktException {
ClientResponse response = sendAuthorizationRequest(form);
URI location = response.getLocation();
// System.out.println(location.toString());
assertEquals(redirectUri, location.getScheme() + "://"
+ location.getHost() + location.getPath());
}
private void testRequestAuthorizationCodeMissingRedirectUri (
MultivaluedMap<String, String> form) throws KustvaktException {
ClientResponse response = sendAuthorizationRequest(form);
String entity = response.getEntity(String.class);
JsonNode node = JsonUtils.readTree(entity);
assertEquals(OAuth2Error.INVALID_REQUEST, node.at("/error").asText());
assertEquals("redirect_uri is required",
node.at("/error_description").asText());
}
private void testRequestAuthorizationCodeInvalidRedirectUri (
MultivaluedMap<String, String> form) throws KustvaktException {
form.add("redirect_uri", "blah");
ClientResponse response = sendAuthorizationRequest(form);
String entity = response.getEntity(String.class);
JsonNode node = JsonUtils.readTree(entity);
assertEquals(OAuth2Error.INVALID_REQUEST, node.at("/error").asText());
assertEquals("Invalid redirect URI",
node.at("/error_description").asText());
form.remove("redirect_uri");
}
@Test
public void testRequestAuthorizationCodeMissingClientID ()
throws KustvaktException {
MultivaluedMap<String, String> form = new MultivaluedMapImpl();
form.add("scope", "openid");
form.add("redirect_uri", redirectUri);
// error response is represented in JSON because redirect URI
// cannot be verified without client id
// Besides client_id is a mandatory parameter in a normal
// OAuth2 authorization request, thus it is checked first,
// before redirect_uri. see
// com.nimbusds.oauth2.sdk.AuthorizationRequest
ClientResponse response = sendAuthorizationRequest(form);
String entity = response.getEntity(String.class);
JsonNode node = JsonUtils.readTree(entity);
assertEquals(OAuth2Error.INVALID_REQUEST, node.at("/error").asText());
assertEquals("Invalid request: Missing \"client_id\" parameter",
node.at("/error_description").asText());
}
@Test
public void testRequestAuthorizationCodeMissingResponseType ()
throws KustvaktException {
MultivaluedMap<String, String> form = new MultivaluedMapImpl();
form.add("scope", "openid");
form.add("redirect_uri", redirectUri);
form.add("client_id", "blah");
// client_id has not been verified yet
// MUST NOT automatically redirect the user-agent to the
// invalid redirection URI.
ClientResponse response = sendAuthorizationRequest(form);
String entity = response.getEntity(String.class);
JsonNode node = JsonUtils.readTree(entity);
assertEquals(OAuth2Error.INVALID_REQUEST, node.at("/error").asText());
assertEquals("Invalid request: Missing \"response_type\" parameter",
node.at("/error_description").asText());
}
private void testRequestAuthorizationCodeUnsupportedResponseType (
MultivaluedMap<String, String> form, String type)
throws KustvaktException {
ClientResponse response = sendAuthorizationRequest(form);
URI location = response.getLocation();
assertEquals(MediaType.APPLICATION_FORM_URLENCODED,
response.getType().toString());
MultiValueMap<String, String> params =
UriComponentsBuilder.fromUri(location).build().getQueryParams();
assertEquals("invalid_request", params.getFirst("error"));
assertEquals("unsupported+response_type%3A+" + type,
params.getFirst("error_description"));
}
/**
* We don't support implicit grant. Implicit grant allows
* response_type:
* <ul>
* <li>id_token</li>
* <li>id_token token</li>
* </ul>
*
* @throws KustvaktException
*/
@Test
public void testRequestAuthorizationCodeUnsupportedImplicitFlow ()
throws KustvaktException {
MultivaluedMap<String, String> form = new MultivaluedMapImpl();
form.add("scope", "openid");
form.add("redirect_uri", redirectUri);
form.add("response_type", "id_token");
form.add("client_id", "fCBbQkAyYzI4NzUxMg");
form.add("nonce", "nonce");
testRequestAuthorizationCodeUnsupportedResponseType(form, "id_token");
form.remove("response_type");
form.add("response_type", "id_token token");
testRequestAuthorizationCodeUnsupportedResponseType(form, "id_token");
}
/**
* Hybrid flow is not supported. Hybrid flow allows
* response_type:
* <ul>
* <li>code id_token</li>
* <li>code token</li>
* <li>code id_token token</li>
* </ul>
*
* @throws KustvaktExceptiony);
* assertTrue(signedJWT.verify(verifier));
*/
@Test
public void testRequestAuthorizationCodeUnsupportedHybridFlow ()
throws KustvaktException {
MultivaluedMap<String, String> form = new MultivaluedMapImpl();
form.add("scope", "openid");
form.add("redirect_uri", redirectUri);
form.add("response_type", "code id_token");
form.add("client_id", "fCBbQkAyYzI4NzUxMg");
form.add("nonce", "nonce");
testRequestAuthorizationCodeUnsupportedResponseType(form, "id_token");
form.remove("response_type");
form.add("response_type", "code token");
testRequestAuthorizationCodeUnsupportedResponseType(form, "token");
}
@Test
public void testRequestAccessTokenWithAuthorizationCode ()
throws KustvaktException, ParseException, InvalidKeySpecException,
NoSuchAlgorithmException, JOSEException {
String client_id = "fCBbQkAyYzI4NzUxMg";
String nonce = "thisIsMyNonce";
MultivaluedMap<String, String> form = new MultivaluedMapImpl();
form.add("response_type", "code");
form.add("client_id", client_id);
form.add("redirect_uri", redirectUri);
form.add("scope", "openid");
form.add("state", "thisIsMyState");
form.add("nonce", nonce);
ClientResponse response = sendAuthorizationRequest(form);
URI location = response.getLocation();
MultiValueMap<String, String> params =
UriComponentsBuilder.fromUri(location).build().getQueryParams();
assertEquals("thisIsMyState", params.getFirst("state"));
String code = params.getFirst("code");
MultivaluedMap<String, String> tokenForm = new MultivaluedMapImpl();
testRequestAccessTokenMissingGrant(tokenForm);
tokenForm.add("grant_type", "authorization_code");
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());
assertEquals(TokenType.BEARER.toString(),
node.at("/token_type").asText());
assertNotNull(node.at("/expires_in").asText());
String id_token = node.at("/id_token").asText();
assertNotNull(id_token);
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 {
JWKSet keySet = config.getPublicKeySet();
RSAKey publicKey = (RSAKey) keySet.getKeyByKeyId(config.getRsaKeyId());
SignedJWT signedJWT = SignedJWT.parse(id_token);
JWSVerifier verifier = new RSASSAVerifier(publicKey);
assertTrue(signedJWT.verify(verifier));
JWTClaimsSet claimsSet = signedJWT.getJWTClaimsSet();
assertEquals(client_id, claimsSet.getAudience().get(0));
assertEquals(username, claimsSet.getSubject());
assertEquals(config.getIssuerURI().toString(), claimsSet.getIssuer());
assertTrue(new Date().before(claimsSet.getExpirationTime()));
assertNotNull(claimsSet.getClaim(Attributes.AUTHENTICATION_TIME));
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")
.path("jwks").get(ClientResponse.class);
String entity = response.getEntity(String.class);
JsonNode node = JsonUtils.readTree(entity);
assertEquals(1, node.at("/keys").size());
node = node.at("/keys/0");
assertEquals("RSA", node.at("/kty").asText());
assertEquals(config.getRsaKeyId(), node.at("/kid").asText());
assertNotNull(node.at("/e").asText());
assertNotNull(node.at("/n").asText());
}
@Test
public void testOpenIDConfiguration () throws KustvaktException {
ClientResponse response = resource().path("oauth2").path("openid")
.path("config").get(ClientResponse.class);
String entity = response.getEntity(String.class);
JsonNode node = JsonUtils.readTree(entity);
assertNotNull(node.at("/issuer"));
assertNotNull(node.at("/authorization_endpoint"));
assertNotNull(node.at("/token_endpoint"));
assertNotNull(node.at("/response_types_supported"));
assertNotNull(node.at("/subject_types_supported"));
assertNotNull(node.at("/id_token_signing_alg_values_supported"));
}
}