blob: 8fcccf2f53f512ef5e6ba25cad17e2d54be470ff [file] [log] [blame]
margarethaec247dd2018-06-12 21:55:46 +02001package de.ids_mannheim.korap.web.controller;
2
3import static org.junit.Assert.assertEquals;
margarethada3c7852018-06-14 20:35:11 +02004import static org.junit.Assert.assertNotNull;
margaretha5225ed02018-06-25 18:38:40 +02005import static org.junit.Assert.assertTrue;
margarethaec247dd2018-06-12 21:55:46 +02006
7import java.net.URI;
margaretha5225ed02018-06-25 18:38:40 +02008import java.security.NoSuchAlgorithmException;
margaretha5225ed02018-06-25 18:38:40 +02009import java.security.spec.InvalidKeySpecException;
margaretha5225ed02018-06-25 18:38:40 +020010import java.text.ParseException;
11import java.util.Date;
margarethaec247dd2018-06-12 21:55:46 +020012
margaretha56fd5582018-06-18 22:14:51 +020013import javax.ws.rs.core.MediaType;
margarethaec247dd2018-06-12 21:55:46 +020014import javax.ws.rs.core.MultivaluedMap;
15
16import org.apache.http.entity.ContentType;
margarethab36b1a32018-06-20 20:13:07 +020017import org.apache.oltu.oauth2.common.message.types.TokenType;
margarethaec247dd2018-06-12 21:55:46 +020018import org.junit.Test;
19import org.springframework.beans.factory.annotation.Autowired;
margarethada3c7852018-06-14 20:35:11 +020020import org.springframework.util.MultiValueMap;
21import org.springframework.web.util.UriComponentsBuilder;
margarethaec247dd2018-06-12 21:55:46 +020022
margarethada3c7852018-06-14 20:35:11 +020023import com.fasterxml.jackson.databind.JsonNode;
margarethaec247dd2018-06-12 21:55:46 +020024import com.google.common.net.HttpHeaders;
margaretha5225ed02018-06-25 18:38:40 +020025import com.nimbusds.jose.JOSEException;
26import com.nimbusds.jose.JWSVerifier;
27import com.nimbusds.jose.crypto.RSASSAVerifier;
margaretha19295962018-06-26 16:00:47 +020028import com.nimbusds.jose.jwk.JWKSet;
29import com.nimbusds.jose.jwk.RSAKey;
margarethaa2ce63d2018-06-28 10:11:43 +020030import com.nimbusds.jwt.JWTClaimsSet;
margaretha5225ed02018-06-25 18:38:40 +020031import com.nimbusds.jwt.SignedJWT;
margaretha249a0aa2018-06-28 22:25:14 +020032import com.nimbusds.oauth2.sdk.GrantType;
margarethaec247dd2018-06-12 21:55:46 +020033import com.sun.jersey.api.client.ClientHandlerException;
34import com.sun.jersey.api.client.ClientResponse;
35import com.sun.jersey.api.client.UniformInterfaceException;
36import com.sun.jersey.core.util.MultivaluedMapImpl;
37
38import de.ids_mannheim.korap.authentication.http.HttpAuthorizationHandler;
39import de.ids_mannheim.korap.config.Attributes;
margaretha5225ed02018-06-25 18:38:40 +020040import de.ids_mannheim.korap.config.FullConfiguration;
margarethaec247dd2018-06-12 21:55:46 +020041import de.ids_mannheim.korap.config.SpringJerseyTest;
42import de.ids_mannheim.korap.exceptions.KustvaktException;
margarethada3c7852018-06-14 20:35:11 +020043import de.ids_mannheim.korap.oauth2.constant.OAuth2Error;
44import de.ids_mannheim.korap.utils.JsonUtils;
margarethaec247dd2018-06-12 21:55:46 +020045
46public class OAuth2OpenIdControllerTest extends SpringJerseyTest {
47
48 @Autowired
margaretha5225ed02018-06-25 18:38:40 +020049 private FullConfiguration config;
margarethaec247dd2018-06-12 21:55:46 +020050
margarethada3c7852018-06-14 20:35:11 +020051 private String redirectUri =
52 "https://korap.ids-mannheim.de/confidential/redirect";
margaretha5225ed02018-06-25 18:38:40 +020053 private String username = "dory";
54
margarethada3c7852018-06-14 20:35:11 +020055 private ClientResponse sendAuthorizationRequest (
56 MultivaluedMap<String, String> form) throws KustvaktException {
margarethaee0cbfe2018-08-28 17:47:14 +020057 return resource().path(API_VERSION).path("oauth2").path("openid").path("authorize")
margarethaec247dd2018-06-12 21:55:46 +020058 .header(Attributes.AUTHORIZATION,
margaretha064eb6f2018-07-10 18:33:01 +020059 HttpAuthorizationHandler
60 .createBasicAuthorizationHeaderValue(username,
61 "password"))
margarethaec247dd2018-06-12 21:55:46 +020062 .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
63 .header(HttpHeaders.CONTENT_TYPE,
64 ContentType.APPLICATION_FORM_URLENCODED)
65 .entity(form).post(ClientResponse.class);
margarethaec247dd2018-06-12 21:55:46 +020066 }
margaretha5225ed02018-06-25 18:38:40 +020067
68 private ClientResponse sendTokenRequest (
69 MultivaluedMap<String, String> form) throws KustvaktException {
margarethaee0cbfe2018-08-28 17:47:14 +020070 return resource().path(API_VERSION).path("oauth2").path("openid").path("token")
margarethab36b1a32018-06-20 20:13:07 +020071 .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
72 .header(HttpHeaders.CONTENT_TYPE,
73 ContentType.APPLICATION_FORM_URLENCODED)
74 .entity(form).post(ClientResponse.class);
75 }
margarethaec247dd2018-06-12 21:55:46 +020076
margarethada3c7852018-06-14 20:35:11 +020077 @Test
78 public void testRequestAuthorizationCode ()
79 throws UniformInterfaceException, ClientHandlerException,
80 KustvaktException {
81
82 MultivaluedMap<String, String> form = new MultivaluedMapImpl();
83 form.add("response_type", "code");
84 form.add("client_id", "fCBbQkAyYzI4NzUxMg");
85
86 testRequestAuthorizationCodeWithoutOpenID(form, redirectUri);
87 form.add("scope", "openid");
88
89 testRequestAuthorizationCodeMissingRedirectUri(form);
90 testRequestAuthorizationCodeInvalidRedirectUri(form);
91 form.add("redirect_uri", redirectUri);
92
93 form.add("state", "thisIsMyState");
94
95 ClientResponse response = sendAuthorizationRequest(form);
96 URI location = response.getLocation();
97 assertEquals(redirectUri, location.getScheme() + "://"
98 + location.getHost() + location.getPath());
99
100 MultiValueMap<String, String> params =
101 UriComponentsBuilder.fromUri(location).build().getQueryParams();
102 assertNotNull(params.getFirst("code"));
103 assertEquals("thisIsMyState", params.getFirst("state"));
104 }
105
106 private void testRequestAuthorizationCodeWithoutOpenID (
107 MultivaluedMap<String, String> form, String redirectUri)
108 throws KustvaktException {
109 ClientResponse response = sendAuthorizationRequest(form);
110 URI location = response.getLocation();
111 // System.out.println(location.toString());
112 assertEquals(redirectUri, location.getScheme() + "://"
113 + location.getHost() + location.getPath());
114 }
115
116 private void testRequestAuthorizationCodeMissingRedirectUri (
117 MultivaluedMap<String, String> form) throws KustvaktException {
118 ClientResponse response = sendAuthorizationRequest(form);
119 String entity = response.getEntity(String.class);
120
121 JsonNode node = JsonUtils.readTree(entity);
122 assertEquals(OAuth2Error.INVALID_REQUEST, node.at("/error").asText());
123 assertEquals("redirect_uri is required",
124 node.at("/error_description").asText());
125 }
126
127 private void testRequestAuthorizationCodeInvalidRedirectUri (
128 MultivaluedMap<String, String> form) throws KustvaktException {
129 form.add("redirect_uri", "blah");
130 ClientResponse response = sendAuthorizationRequest(form);
131 String entity = response.getEntity(String.class);
132
133 JsonNode node = JsonUtils.readTree(entity);
134 assertEquals(OAuth2Error.INVALID_REQUEST, node.at("/error").asText());
135 assertEquals("Invalid redirect URI",
136 node.at("/error_description").asText());
137
138 form.remove("redirect_uri");
139 }
140
141 @Test
142 public void testRequestAuthorizationCodeMissingClientID ()
143 throws KustvaktException {
144 MultivaluedMap<String, String> form = new MultivaluedMapImpl();
145 form.add("scope", "openid");
146 form.add("redirect_uri", redirectUri);
147
148 // error response is represented in JSON because redirect URI
149 // cannot be verified without client id
150 // Besides client_id is a mandatory parameter in a normal
151 // OAuth2 authorization request, thus it is checked first,
152 // before redirect_uri. see
153 // com.nimbusds.oauth2.sdk.AuthorizationRequest
154
155 ClientResponse response = sendAuthorizationRequest(form);
156 String entity = response.getEntity(String.class);
157 JsonNode node = JsonUtils.readTree(entity);
158 assertEquals(OAuth2Error.INVALID_REQUEST, node.at("/error").asText());
159 assertEquals("Invalid request: Missing \"client_id\" parameter",
160 node.at("/error_description").asText());
161
162 }
163
164 @Test
165 public void testRequestAuthorizationCodeMissingResponseType ()
166 throws KustvaktException {
167 MultivaluedMap<String, String> form = new MultivaluedMapImpl();
168 form.add("scope", "openid");
169 form.add("redirect_uri", redirectUri);
170 form.add("client_id", "blah");
171
172 // client_id has not been verified yet
173 // MUST NOT automatically redirect the user-agent to the
174 // invalid redirection URI.
175
176 ClientResponse response = sendAuthorizationRequest(form);
177 String entity = response.getEntity(String.class);
178 JsonNode node = JsonUtils.readTree(entity);
179 assertEquals(OAuth2Error.INVALID_REQUEST, node.at("/error").asText());
180 assertEquals("Invalid request: Missing \"response_type\" parameter",
181 node.at("/error_description").asText());
182 }
183
margaretha5225ed02018-06-25 18:38:40 +0200184 private void testRequestAuthorizationCodeUnsupportedResponseType (
185 MultivaluedMap<String, String> form, String type)
margarethada3c7852018-06-14 20:35:11 +0200186 throws KustvaktException {
margarethada3c7852018-06-14 20:35:11 +0200187
188 ClientResponse response = sendAuthorizationRequest(form);
189 URI location = response.getLocation();
margaretha5225ed02018-06-25 18:38:40 +0200190 assertEquals(MediaType.APPLICATION_FORM_URLENCODED,
191 response.getType().toString());
margarethada3c7852018-06-14 20:35:11 +0200192
193 MultiValueMap<String, String> params =
194 UriComponentsBuilder.fromUri(location).build().getQueryParams();
195 assertEquals("invalid_request", params.getFirst("error"));
margaretha5225ed02018-06-25 18:38:40 +0200196 assertEquals("unsupported+response_type%3A+" + type,
margarethada3c7852018-06-14 20:35:11 +0200197 params.getFirst("error_description"));
198 }
margaretha5225ed02018-06-25 18:38:40 +0200199
200 /**
201 * We don't support implicit grant. Implicit grant allows
202 * response_type:
203 * <ul>
204 * <li>id_token</li>
205 * <li>id_token token</li>
206 * </ul>
207 *
208 * @throws KustvaktException
209 */
margarethab36b1a32018-06-20 20:13:07 +0200210 @Test
margaretha5225ed02018-06-25 18:38:40 +0200211 public void testRequestAuthorizationCodeUnsupportedImplicitFlow ()
212 throws KustvaktException {
213 MultivaluedMap<String, String> form = new MultivaluedMapImpl();
214 form.add("scope", "openid");
215 form.add("redirect_uri", redirectUri);
216 form.add("response_type", "id_token");
217 form.add("client_id", "fCBbQkAyYzI4NzUxMg");
218 form.add("nonce", "nonce");
219
220 testRequestAuthorizationCodeUnsupportedResponseType(form, "id_token");
221
222 form.remove("response_type");
223 form.add("response_type", "id_token token");
224 testRequestAuthorizationCodeUnsupportedResponseType(form, "id_token");
225 }
226
227 /**
228 * Hybrid flow is not supported. Hybrid flow allows
229 * response_type:
230 * <ul>
231 * <li>code id_token</li>
232 * <li>code token</li>
233 * <li>code id_token token</li>
234 * </ul>
235 *
margaretha19295962018-06-26 16:00:47 +0200236 * @throws KustvaktExceptiony);
237 * assertTrue(signedJWT.verify(verifier));
margaretha5225ed02018-06-25 18:38:40 +0200238 */
239
240 @Test
241 public void testRequestAuthorizationCodeUnsupportedHybridFlow ()
242 throws KustvaktException {
243 MultivaluedMap<String, String> form = new MultivaluedMapImpl();
244 form.add("scope", "openid");
245 form.add("redirect_uri", redirectUri);
246 form.add("response_type", "code id_token");
247 form.add("client_id", "fCBbQkAyYzI4NzUxMg");
248 form.add("nonce", "nonce");
249 testRequestAuthorizationCodeUnsupportedResponseType(form, "id_token");
250
251 form.remove("response_type");
252 form.add("response_type", "code token");
253 testRequestAuthorizationCodeUnsupportedResponseType(form, "token");
254 }
255
256 @Test
margaretha249a0aa2018-06-28 22:25:14 +0200257 public void testRequestAccessTokenWithAuthorizationCode ()
margaretha5225ed02018-06-25 18:38:40 +0200258 throws KustvaktException, ParseException, InvalidKeySpecException,
259 NoSuchAlgorithmException, JOSEException {
260 String client_id = "fCBbQkAyYzI4NzUxMg";
margarethaa2ce63d2018-06-28 10:11:43 +0200261 String nonce = "thisIsMyNonce";
margarethab36b1a32018-06-20 20:13:07 +0200262 MultivaluedMap<String, String> form = new MultivaluedMapImpl();
263 form.add("response_type", "code");
margaretha5225ed02018-06-25 18:38:40 +0200264 form.add("client_id", client_id);
margarethab36b1a32018-06-20 20:13:07 +0200265 form.add("redirect_uri", redirectUri);
266 form.add("scope", "openid");
267 form.add("state", "thisIsMyState");
margarethaa2ce63d2018-06-28 10:11:43 +0200268 form.add("nonce", nonce);
margarethab36b1a32018-06-20 20:13:07 +0200269
270 ClientResponse response = sendAuthorizationRequest(form);
271 URI location = response.getLocation();
272 MultiValueMap<String, String> params =
273 UriComponentsBuilder.fromUri(location).build().getQueryParams();
margarethaa2ce63d2018-06-28 10:11:43 +0200274 assertEquals("thisIsMyState", params.getFirst("state"));
margarethab36b1a32018-06-20 20:13:07 +0200275 String code = params.getFirst("code");
margaretha5225ed02018-06-25 18:38:40 +0200276
margarethab36b1a32018-06-20 20:13:07 +0200277 MultivaluedMap<String, String> tokenForm = new MultivaluedMapImpl();
margaretha249a0aa2018-06-28 22:25:14 +0200278 testRequestAccessTokenMissingGrant(tokenForm);
margarethab36b1a32018-06-20 20:13:07 +0200279 tokenForm.add("grant_type", "authorization_code");
margarethab36b1a32018-06-20 20:13:07 +0200280 tokenForm.add("code", code);
margaretha249a0aa2018-06-28 22:25:14 +0200281 testRequestAccessTokenMissingClientId(tokenForm);
282 tokenForm.add("client_id", client_id);
283 testRequestAccessTokenMissingClientSecret(tokenForm);
284 tokenForm.add("client_secret", "secret");
285 tokenForm.add("redirect_uri", redirectUri);
margaretha5225ed02018-06-25 18:38:40 +0200286
margarethab36b1a32018-06-20 20:13:07 +0200287 ClientResponse tokenResponse = sendTokenRequest(tokenForm);
288 String entity = tokenResponse.getEntity(String.class);
margaretha0a45be12018-07-12 15:06:30 +0200289
margarethab36b1a32018-06-20 20:13:07 +0200290 JsonNode node = JsonUtils.readTree(entity);
291 assertNotNull(node.at("/access_token").asText());
292 assertNotNull(node.at("/refresh_token").asText());
293 assertEquals(TokenType.BEARER.toString(),
294 node.at("/token_type").asText());
295 assertNotNull(node.at("/expires_in").asText());
margaretha5225ed02018-06-25 18:38:40 +0200296 String id_token = node.at("/id_token").asText();
297 assertNotNull(id_token);
margarethab36b1a32018-06-20 20:13:07 +0200298
margarethaa2ce63d2018-06-28 10:11:43 +0200299 verifyingIdToken(id_token, username, client_id, nonce);
margaretha5225ed02018-06-25 18:38:40 +0200300 }
301
margaretha249a0aa2018-06-28 22:25:14 +0200302 private void testRequestAccessTokenMissingGrant (
303 MultivaluedMap<String, String> tokenForm) throws KustvaktException {
304 ClientResponse response = sendTokenRequest(tokenForm);
305 String entity = response.getEntity(String.class);
306 JsonNode node = JsonUtils.readTree(entity);
307 assertEquals(OAuth2Error.INVALID_REQUEST, node.at("/error").asText());
308 assertEquals("Invalid request: Missing \"grant_type\" parameter",
309 node.at("/error_description").asText());
310 }
311
312 private void testRequestAccessTokenMissingClientId (
313 MultivaluedMap<String, String> tokenForm) throws KustvaktException {
314 ClientResponse response = sendTokenRequest(tokenForm);
315 String entity = response.getEntity(String.class);
316 JsonNode node = JsonUtils.readTree(entity);
317 assertEquals(OAuth2Error.INVALID_REQUEST, node.at("/error").asText());
318 assertEquals("Invalid request: Missing required \"client_id\" "
319 + "parameter", node.at("/error_description").asText());
320 }
321
322 private void testRequestAccessTokenMissingClientSecret (
323 MultivaluedMap<String, String> tokenForm) throws KustvaktException {
324 ClientResponse response = sendTokenRequest(tokenForm);
325 String entity = response.getEntity(String.class);
326 JsonNode node = JsonUtils.readTree(entity);
327 assertEquals(OAuth2Error.INVALID_REQUEST, node.at("/error").asText());
328 assertEquals("Missing parameters: client_secret",
329 node.at("/error_description").asText());
330 }
331
margaretha5225ed02018-06-25 18:38:40 +0200332 private void verifyingIdToken (String id_token, String username,
margarethaa2ce63d2018-06-28 10:11:43 +0200333 String client_id, String nonce) throws ParseException,
334 InvalidKeySpecException, NoSuchAlgorithmException, JOSEException {
margaretha19295962018-06-26 16:00:47 +0200335 JWKSet keySet = config.getPublicKeySet();
336 RSAKey publicKey = (RSAKey) keySet.getKeyByKeyId(config.getRsaKeyId());
margaretha5225ed02018-06-25 18:38:40 +0200337
338 SignedJWT signedJWT = SignedJWT.parse(id_token);
339 JWSVerifier verifier = new RSASSAVerifier(publicKey);
340 assertTrue(signedJWT.verify(verifier));
341
margarethaa2ce63d2018-06-28 10:11:43 +0200342 JWTClaimsSet claimsSet = signedJWT.getJWTClaimsSet();
343 assertEquals(client_id, claimsSet.getAudience().get(0));
344 assertEquals(username, claimsSet.getSubject());
345 assertEquals(config.getIssuerURI().toString(), claimsSet.getIssuer());
346 assertTrue(new Date().before(claimsSet.getExpirationTime()));
347 assertNotNull(claimsSet.getClaim(Attributes.AUTHENTICATION_TIME));
348 assertEquals(nonce, claimsSet.getClaim("nonce"));
margarethab36b1a32018-06-20 20:13:07 +0200349 }
margaretha19295962018-06-26 16:00:47 +0200350
margaretha249a0aa2018-06-28 22:25:14 +0200351 // no openid
352 @Test
353 public void testRequestAccessTokenWithPassword ()
354 throws KustvaktException, ParseException, InvalidKeySpecException,
355 NoSuchAlgorithmException, JOSEException {
356 // public client
margaretha835178d2018-08-15 19:04:03 +0200357 String client_id = "8bIDtZnH6NvRkW2Fq";
margaretha249a0aa2018-06-28 22:25:14 +0200358 MultivaluedMap<String, String> tokenForm = new MultivaluedMapImpl();
359 testRequestAccessTokenMissingGrant(tokenForm);
360
361 tokenForm.add("grant_type", GrantType.PASSWORD.toString());
362 testRequestAccessTokenMissingUsername(tokenForm);
363
364 tokenForm.add("username", username);
365 testRequestAccessTokenMissingPassword(tokenForm);
366
367 tokenForm.add("password", "pass");
368 tokenForm.add("client_id", client_id);
369
370 ClientResponse tokenResponse = sendTokenRequest(tokenForm);
371 String entity = tokenResponse.getEntity(String.class);
margaretha249a0aa2018-06-28 22:25:14 +0200372 JsonNode node = JsonUtils.readTree(entity);
margaretha249a0aa2018-06-28 22:25:14 +0200373
margaretha835178d2018-08-15 19:04:03 +0200374 assertEquals(OAuth2Error.UNAUTHORIZED_CLIENT,
375 node.at("/error").asText());
376 assertEquals("Password grant is not allowed for third party clients",
377 node.at("/error_description").asText());
378 }
margaretha249a0aa2018-06-28 22:25:14 +0200379
380 private void testRequestAccessTokenMissingUsername (
381 MultivaluedMap<String, String> tokenForm) throws KustvaktException {
382 ClientResponse response = sendTokenRequest(tokenForm);
383 String entity = response.getEntity(String.class);
384 JsonNode node = JsonUtils.readTree(entity);
385 assertEquals(OAuth2Error.INVALID_REQUEST, node.at("/error").asText());
386 assertEquals("Invalid request: Missing or empty \"username\" parameter",
387 node.at("/error_description").asText());
388 }
389
390 private void testRequestAccessTokenMissingPassword (
391 MultivaluedMap<String, String> tokenForm) throws KustvaktException {
392 ClientResponse response = sendTokenRequest(tokenForm);
393 String entity = response.getEntity(String.class);
394 JsonNode node = JsonUtils.readTree(entity);
395 assertEquals(OAuth2Error.INVALID_REQUEST, node.at("/error").asText());
396 assertEquals("Invalid request: Missing or empty \"password\" parameter",
397 node.at("/error_description").asText());
398 }
399
margaretha19295962018-06-26 16:00:47 +0200400 @Test
401 public void testPublicKeyAPI () throws KustvaktException {
margarethaee0cbfe2018-08-28 17:47:14 +0200402 ClientResponse response = resource().path(API_VERSION).path("oauth2").path("openid")
margaretha9c78e1a2018-06-27 14:12:35 +0200403 .path("jwks").get(ClientResponse.class);
margaretha19295962018-06-26 16:00:47 +0200404 String entity = response.getEntity(String.class);
405 JsonNode node = JsonUtils.readTree(entity);
margarethaa2ce63d2018-06-28 10:11:43 +0200406 assertEquals(1, node.at("/keys").size());
margaretha19295962018-06-26 16:00:47 +0200407 node = node.at("/keys/0");
408 assertEquals("RSA", node.at("/kty").asText());
409 assertEquals(config.getRsaKeyId(), node.at("/kid").asText());
410 assertNotNull(node.at("/e").asText());
411 assertNotNull(node.at("/n").asText());
412 }
margarethaa2ce63d2018-06-28 10:11:43 +0200413
margaretha9c78e1a2018-06-27 14:12:35 +0200414 @Test
415 public void testOpenIDConfiguration () throws KustvaktException {
margarethaee0cbfe2018-08-28 17:47:14 +0200416 ClientResponse response = resource().path(API_VERSION).path("oauth2").path("openid")
margaretha9c78e1a2018-06-27 14:12:35 +0200417 .path("config").get(ClientResponse.class);
418 String entity = response.getEntity(String.class);
419 JsonNode node = JsonUtils.readTree(entity);
420 assertNotNull(node.at("/issuer"));
421 assertNotNull(node.at("/authorization_endpoint"));
422 assertNotNull(node.at("/token_endpoint"));
423 assertNotNull(node.at("/response_types_supported"));
424 assertNotNull(node.at("/subject_types_supported"));
425 assertNotNull(node.at("/id_token_signing_alg_values_supported"));
426 }
margarethaec247dd2018-06-12 21:55:46 +0200427}