Remap Nimbus JSON Parsing Errors

When Nimbus fails to parse either a JWK response or a JWT response,
the error message contains information that either should or cannot be
included in a Bearer Token response.

For example, if the response from a JWK endpoint is invalid JSON, then
Nimbus will send the entire response from the authentication server in
the resulting exception message.

This commit captures these exceptions and removes the parsing detail,
replacing it with more generic information about the nature of the
error.

Fixes: gh-5517
This commit is contained in:
Josh Cummings 2018-06-29 14:08:42 -06:00 committed by Rob Winch
parent 28afb4e3d7
commit 6e67c0dcea
2 changed files with 83 additions and 11 deletions

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2017 the original author or authors.
* Copyright 2002-2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -15,7 +15,15 @@
*/
package org.springframework.security.oauth2.jwt;
import java.net.MalformedURLException;
import java.net.URL;
import java.text.ParseException;
import java.time.Instant;
import java.util.LinkedHashMap;
import java.util.Map;
import com.nimbusds.jose.JWSAlgorithm;
import com.nimbusds.jose.RemoteKeySourceException;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.jwk.source.RemoteJWKSet;
import com.nimbusds.jose.proc.JWSKeySelector;
@ -29,15 +37,10 @@ import com.nimbusds.jwt.JWTParser;
import com.nimbusds.jwt.SignedJWT;
import com.nimbusds.jwt.proc.ConfigurableJWTProcessor;
import com.nimbusds.jwt.proc.DefaultJWTProcessor;
import org.springframework.security.oauth2.jose.jws.JwsAlgorithms;
import org.springframework.util.Assert;
import java.net.MalformedURLException;
import java.net.URL;
import java.time.Instant;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* An implementation of a {@link JwtDecoder} that "decodes" a
* JSON Web Token (JWT) and additionally verifies it's digital signature if the JWT is a
@ -48,6 +51,7 @@ import java.util.Map;
* <b>NOTE:</b> This implementation uses the Nimbus JOSE + JWT SDK internally.
*
* @author Joe Grandja
* @author Josh Cummings
* @since 5.0
* @see JwtDecoder
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7519">JSON Web Token (JWT)</a>
@ -56,6 +60,9 @@ import java.util.Map;
* @see <a target="_blank" href="https://connect2id.com/products/nimbus-jose-jwt">Nimbus JOSE + JWT SDK</a>
*/
public final class NimbusJwtDecoderJwkSupport implements JwtDecoder {
private static final String DECODING_ERROR_MESSAGE_TEMPLATE =
"An error occurred while attempting to decode the Jwt: %s";
private final URL jwkSetUrl;
private final JWSAlgorithm jwsAlgorithm;
private final ConfigurableJWTProcessor<SecurityContext> jwtProcessor;
@ -108,7 +115,7 @@ public final class NimbusJwtDecoderJwkSupport implements JwtDecoder {
try {
return JWTParser.parse(token);
} catch (Exception ex) {
throw new JwtException("An error occurred while attempting to decode the Jwt: " + ex.getMessage(), ex);
throw new JwtException(String.format(DECODING_ERROR_MESSAGE_TEMPLATE, ex.getMessage()), ex);
}
}
@ -135,8 +142,18 @@ public final class NimbusJwtDecoderJwkSupport implements JwtDecoder {
jwt = new Jwt(token, issuedAt, expiresAt, headers, jwtClaimsSet.getClaims());
} catch (RemoteKeySourceException ex) {
if (ex.getCause() instanceof ParseException) {
throw new JwtException(String.format(DECODING_ERROR_MESSAGE_TEMPLATE, "Malformed Jwk set"));
} else {
throw new JwtException(String.format(DECODING_ERROR_MESSAGE_TEMPLATE, ex.getMessage()), ex);
}
} catch (Exception ex) {
throw new JwtException("An error occurred while attempting to decode the Jwt: " + ex.getMessage(), ex);
if (ex.getCause() instanceof ParseException) {
throw new JwtException(String.format(DECODING_ERROR_MESSAGE_TEMPLATE, "Malformed payload"));
} else {
throw new JwtException(String.format(DECODING_ERROR_MESSAGE_TEMPLATE, ex.getMessage()), ex);
}
}
return jwt;

View File

@ -22,10 +22,14 @@ import com.nimbusds.jwt.JWTClaimsSet;
import com.nimbusds.jwt.JWTParser;
import com.nimbusds.jwt.SignedJWT;
import com.nimbusds.jwt.proc.DefaultJWTProcessor;
import okhttp3.mockwebserver.MockResponse;
import okhttp3.mockwebserver.MockWebServer;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.powermock.core.classloader.annotations.PowerMockIgnore;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;
import org.springframework.security.oauth2.jose.jws.JwsAlgorithms;
import static org.assertj.core.api.AssertionsForClassTypes.assertThatCode;
@ -46,11 +50,17 @@ import static org.powermock.api.mockito.PowerMockito.whenNew;
*/
@RunWith(PowerMockRunner.class)
@PrepareForTest({NimbusJwtDecoderJwkSupport.class, JWTParser.class})
@PowerMockIgnore("okhttp3.*")
public class NimbusJwtDecoderJwkSupportTests {
private static final String JWK_SET_URL = "https://provider.com/oauth2/keys";
private static final String JWS_ALGORITHM = JwsAlgorithms.RS256;
private String unsignedToken = "eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJleHAiOi0yMDMzMjI0OTcsImp0aSI6IjEyMyIsInR5cCI6IkpXVCJ9.";
private static final String JWK_SET = "{\"keys\":[{\"p\":\"49neceJFs8R6n7WamRGy45F5Tv0YM-R2ODK3eSBUSLOSH2tAqjEVKOkLE5fiNA3ygqq15NcKRadB2pTVf-Yb5ZIBuKzko8bzYIkIqYhSh_FAdEEr0vHF5fq_yWSvc6swsOJGqvBEtuqtJY027u-G2gAQasCQdhyejer68zsTn8M\",\"kty\":\"RSA\",\"q\":\"tWR-ysspjZ73B6p2vVRVyHwP3KQWL5KEQcdgcmMOE_P_cPs98vZJfLhxobXVmvzuEWBpRSiqiuyKlQnpstKt94Cy77iO8m8ISfF3C9VyLWXi9HUGAJb99irWABFl3sNDff5K2ODQ8CmuXLYM25OwN3ikbrhEJozlXg_NJFSGD4E\",\"d\":\"FkZHYZlw5KSoqQ1i2RA2kCUygSUOf1OqMt3uomtXuUmqKBm_bY7PCOhmwbvbn4xZYEeHuTR8Xix-0KpHe3NKyWrtRjkq1T_un49_1LLVUhJ0dL-9_x0xRquVjhl_XrsRXaGMEHs8G9pLTvXQ1uST585gxIfmCe0sxPZLvwoic-bXf64UZ9BGRV3lFexWJQqCZp2S21HfoU7wiz6kfLRNi-K4xiVNB1gswm_8o5lRuY7zB9bRARQ3TS2G4eW7p5sxT3CgsGiQD3_wPugU8iDplqAjgJ5ofNJXZezoj0t6JMB_qOpbrmAM1EnomIPebSLW7Ky9SugEd6KMdL5lW6AuAQ\",\"e\":\"AQAB\",\"use\":\"sig\",\"kid\":\"one\",\"qi\":\"wdkFu_tV2V1l_PWUUimG516Zvhqk2SWDw1F7uNDD-Lvrv_WNRIJVzuffZ8WYiPy8VvYQPJUrT2EXL8P0ocqwlaSTuXctrORcbjwgxDQDLsiZE0C23HYzgi0cofbScsJdhcBg7d07LAf7cdJWG0YVl1FkMCsxUlZ2wTwHfKWf-v4\",\"dp\":\"uwnPxqC-IxG4r33-SIT02kZC1IqC4aY7PWq0nePiDEQMQWpjjNH50rlq9EyLzbtdRdIouo-jyQXB01K15-XXJJ60dwrGLYNVqfsTd0eGqD1scYJGHUWG9IDgCsxyEnuG3s0AwbW2UolWVSsU2xMZGb9PurIUZECeD1XDZwMp2s0\",\"dq\":\"hra786AunB8TF35h8PpROzPoE9VJJMuLrc6Esm8eZXMwopf0yhxfN2FEAvUoTpLJu93-UH6DKenCgi16gnQ0_zt1qNNIVoRfg4rw_rjmsxCYHTVL3-RDeC8X_7TsEySxW0EgFTHh-nr6I6CQrAJjPM88T35KHtdFATZ7BCBB8AE\",\"n\":\"oXJ8OyOv_eRnce4akdanR4KYRfnC2zLV4uYNQpcFn6oHL0dj7D6kxQmsXoYgJV8ZVDn71KGmuLvolxsDncc2UrhyMBY6DVQVgMSVYaPCTgW76iYEKGgzTEw5IBRQL9w3SRJWd3VJTZZQjkXef48Ocz06PGF3lhbz4t5UEZtdF4rIe7u-977QwHuh7yRPBQ3sII-cVoOUMgaXB9SHcGF2iZCtPzL_IffDUcfhLQteGebhW8A6eUHgpD5A1PQ-JCw_G7UOzZAjjDjtNM2eqm8j-Ms_gqnm4MiCZ4E-9pDN77CAAPVN7kuX6ejs9KBXpk01z48i9fORYk9u7rAkh1HuQw\"}]}";
private static final String MALFORMED_JWK_SET = "malformed";
private static final String SIGNED_JWT = "eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJ0ZXN0LXN1YmplY3QiLCJzY3AiOlsibWVzc2FnZTpyZWFkIl0sImV4cCI6NDY4Mzg5Nzc3Nn0.LtMVtIiRIwSyc3aX35Zl0JVwLTcQZAB3dyBOMHNaHCKUljwMrf20a_gT79LfhjDzE_fUVUmFiAO32W1vFnYpZSVaMDUgeIOIOpxfoe9shj_uYenAwIS-_UxqGVIJiJoXNZh_MK80ShNpvsQwamxWEEOAMBtpWNiVYNDMdfgho9n3o5_Z7Gjy8RLBo1tbDREbO9kTFwGIxm_EYpezmRCRq4w1DdS6UDW321hkwMxPnCMSWOvp-hRpmgY2yjzLgPJ6Aucmg9TJ8jloAP1DjJoF1gRR7NTAk8LOGkSjTzVYDYMbCF51YdpojhItSk80YzXiEsv1mTz4oMM49jXBmfXFMA";
private static final String MALFORMED_JWT = "eyJhbGciOiJSUzI1NiJ9.eyJuYmYiOnt9LCJleHAiOjQ2ODQyMjUwODd9.guoQvujdWvd3xw7FYQEn4D6-gzM_WqFvXdmvAUNSLbxG7fv2_LLCNujPdrBHJoYPbOwS1BGNxIKQWS1tylvqzmr1RohQ-RZ2iAM1HYQzboUlkoMkcd8ENM__ELqho8aNYBfqwkNdUOyBFoy7Syu_w2SoJADw2RTjnesKO6CVVa05bW118pDS4xWxqC4s7fnBjmZoTn4uQ-Kt9YSQZQk8YQxkJSiyanozzgyfgXULA6mPu1pTNU3FVFaK1i1av_xtH_zAPgb647ZeaNe4nahgqC5h8nhOlm8W2dndXbwAt29nd2ZWBsru_QwZz83XSKLhTPFz-mPBByZZDsyBbIHf9A";
private static final String UNSIGNED_JWT = "eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJleHAiOi0yMDMzMjI0OTcsImp0aSI6IjEyMyIsInR5cCI6IkpXVCJ9.";
@Test
public void constructorWhenJwkSetUrlIsNullThenThrowIllegalArgumentException() {
@ -102,8 +112,53 @@ public class NimbusJwtDecoderJwkSupportTests {
public void decodeWhenPlainJwtThenExceptionDoesNotMentionClass() throws Exception {
NimbusJwtDecoderJwkSupport jwtDecoder = new NimbusJwtDecoderJwkSupport(JWK_SET_URL, JWS_ALGORITHM);
assertThatCode(() -> jwtDecoder.decode(this.unsignedToken))
assertThatCode(() -> jwtDecoder.decode(UNSIGNED_JWT))
.isInstanceOf(JwtException.class)
.hasMessageContaining("Unsupported algorithm of none");
}
@Test
public void decodeWhenJwtIsMalformedThenReturnsStockException() throws Exception {
try ( MockWebServer server = new MockWebServer() ) {
server.enqueue(new MockResponse().setBody(JWK_SET));
String jwkSetUrl = server.url("/.well-known/jwks.json").toString();
NimbusJwtDecoderJwkSupport decoder = new NimbusJwtDecoderJwkSupport(jwkSetUrl);
assertThatCode(() -> decoder.decode(MALFORMED_JWT))
.isInstanceOf(JwtException.class)
.hasMessage("An error occurred while attempting to decode the Jwt: Malformed payload");
}
}
@Test
public void decodeWhenJwkResponseIsMalformedThenReturnsStockException() throws Exception {
try ( MockWebServer server = new MockWebServer() ) {
server.enqueue(new MockResponse().setBody(MALFORMED_JWK_SET));
String jwkSetUrl = server.url("/.well-known/jwks.json").toString();
NimbusJwtDecoderJwkSupport decoder = new NimbusJwtDecoderJwkSupport(jwkSetUrl);
assertThatCode(() -> decoder.decode(SIGNED_JWT))
.isInstanceOf(JwtException.class)
.hasMessage("An error occurred while attempting to decode the Jwt: Malformed Jwk set");
}
}
@Test
public void decodeWhenJwkEndpointIsUnresponsiveThenReturnsStockException() throws Exception {
try ( MockWebServer server = new MockWebServer() ) {
server.enqueue(new MockResponse().setBody(MALFORMED_JWK_SET));
String jwkSetUrl = server.url("/.well-known/jwks.json").toString();
NimbusJwtDecoderJwkSupport decoder = new NimbusJwtDecoderJwkSupport(jwkSetUrl);
server.shutdown();
assertThatCode(() -> decoder.decode(SIGNED_JWT))
.isInstanceOf(JwtException.class)
.hasMessage("An error occurred while attempting to decode the Jwt: " +
"Couldn't retrieve remote JWK set: Connection refused (Connection refused)");
}
}
}