Jwt Claim Validation

This introduces OAuth2TokenValidator which allows the customization of
validation steps that need to be performing when decoding a string
token to a Jwt.

At this point, two validators, JwtTimestampValidator and
JwtIssuerValidator, are available for use.

Fixes: gh-5133
This commit is contained in:
Josh Cummings 2018-07-24 11:47:30 -06:00 committed by Rob Winch
parent c6ea447cc0
commit 7c524aa0c8
16 changed files with 1225 additions and 25 deletions

View File

@ -20,7 +20,10 @@ import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.lang.reflect.Field;
import java.time.Clock;
import java.time.Duration;
import java.time.Instant;
import java.time.ZoneId;
import java.util.Collections;
import java.util.Map;
import java.util.stream.Collectors;
@ -60,12 +63,16 @@ import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult;
import org.springframework.security.oauth2.jose.jws.JwsAlgorithms;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.JwtClaimNames;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.JwtException;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoderJwkSupport;
import org.springframework.security.oauth2.jwt.JwtTimestampValidator;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
import org.springframework.security.oauth2.server.resource.web.BearerTokenAuthenticationEntryPoint;
import org.springframework.security.oauth2.server.resource.web.BearerTokenResolver;
@ -92,6 +99,7 @@ import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatCode;
import static org.hamcrest.CoreMatchers.containsString;
import static org.hamcrest.core.StringStartsWith.startsWith;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
@ -839,6 +847,57 @@ public class OAuth2ResourceServerConfigurerTests {
.isInstanceOf(IllegalArgumentException.class);
}
// -- token validator
@Test
public void requestWhenCustomJwtValidatorFailsThenCorrespondingErrorMessage()
throws Exception {
this.spring.register(WebServerConfig.class, CustomJwtValidatorConfig.class).autowire();
this.authz.enqueue(this.jwks("Default"));
String token = this.token("ValidNoScopes");
OAuth2TokenValidator<Jwt> jwtValidator =
this.spring.getContext().getBean(CustomJwtValidatorConfig.class)
.getJwtValidator();
OAuth2Error error = new OAuth2Error("custom-error", "custom-description", "custom-uri");
when(jwtValidator.validate(any(Jwt.class))).thenReturn(OAuth2TokenValidatorResult.failure(error));
this.mvc.perform(get("/")
.with(bearerToken(token)))
.andExpect(status().isUnauthorized())
.andExpect(header().string(HttpHeaders.WWW_AUTHENTICATE, containsString("custom-description")));
}
@Test
public void requestWhenClockSkewSetThenTimestampWindowRelaxedAccordingly()
throws Exception {
this.spring.register(WebServerConfig.class, UnexpiredJwtClockSkewConfig.class, BasicController.class).autowire();
this.authz.enqueue(this.jwks("Default"));
String token = this.token("ExpiresAt4687177990");
this.mvc.perform(get("/")
.with(bearerToken(token)))
.andExpect(status().isOk());
}
@Test
public void requestWhenClockSkewSetButJwtStillTooLateThenReportsExpired()
throws Exception {
this.spring.register(WebServerConfig.class, ExpiredJwtClockSkewConfig.class, BasicController.class).autowire();
this.authz.enqueue(this.jwks("Default"));
String token = this.token("ExpiresAt4687177990");
this.mvc.perform(get("/")
.with(bearerToken(token)))
.andExpect(status().isUnauthorized())
.andExpect(invalidTokenHeader("Jwt expired at"));
}
// -- In combination with other authentication providers
@Test
@ -1266,6 +1325,80 @@ public class OAuth2ResourceServerConfigurerTests {
}
}
@EnableWebSecurity
static class CustomJwtValidatorConfig extends WebSecurityConfigurerAdapter {
@Value("${mock.jwk-set-uri}") String uri;
private final OAuth2TokenValidator<Jwt> jwtValidator = mock(OAuth2TokenValidator.class);
@Override
protected void configure(HttpSecurity http) throws Exception {
NimbusJwtDecoderJwkSupport jwtDecoder =
new NimbusJwtDecoderJwkSupport(this.uri);
jwtDecoder.setJwtValidator(this.jwtValidator);
// @formatter:off
http
.oauth2()
.resourceServer()
.jwt()
.decoder(jwtDecoder);
// @formatter:on
}
public OAuth2TokenValidator<Jwt> getJwtValidator() {
return this.jwtValidator;
}
}
@EnableWebSecurity
static class UnexpiredJwtClockSkewConfig extends WebSecurityConfigurerAdapter {
@Value("${mock.jwk-set-uri}") String uri;
@Override
protected void configure(HttpSecurity http) throws Exception {
Clock nearlyAnHourFromTokenExpiry =
Clock.fixed(Instant.ofEpochMilli(4687181540000L), ZoneId.systemDefault());
JwtTimestampValidator jwtValidator = new JwtTimestampValidator(Duration.ofHours(1));
jwtValidator.setClock(nearlyAnHourFromTokenExpiry);
NimbusJwtDecoderJwkSupport jwtDecoder = new NimbusJwtDecoderJwkSupport(this.uri);
jwtDecoder.setJwtValidator(jwtValidator);
// @formatter:off
http
.oauth2()
.resourceServer()
.jwt()
.decoder(jwtDecoder);
// @formatter:on
}
}
@EnableWebSecurity
static class ExpiredJwtClockSkewConfig extends WebSecurityConfigurerAdapter {
@Value("${mock.jwk-set-uri}") String uri;
@Override
protected void configure(HttpSecurity http) throws Exception {
Clock justOverOneHourAfterExpiry =
Clock.fixed(Instant.ofEpochMilli(4687181595000L), ZoneId.systemDefault());
JwtTimestampValidator jwtValidator = new JwtTimestampValidator(Duration.ofHours(1));
jwtValidator.setClock(justOverOneHourAfterExpiry);
NimbusJwtDecoderJwkSupport jwtDecoder = new NimbusJwtDecoderJwkSupport(this.uri);
jwtDecoder.setJwtValidator(jwtValidator);
// @formatter:off
http
.oauth2()
.resourceServer()
.jwt()
.decoder(jwtDecoder);
// @formatter:on
}
}
@Configuration
static class JwtDecoderConfig {
@Bean

View File

@ -0,0 +1 @@
eyJhbGciOiJSUzI1NiJ9.eyJleHAiOjQ2ODcxNzc5OTB9.RRQvqIZzLweq0iwWUZk1Dpiz6iUmT4bAVhGWqvWNWK3UwJ6aBIYsCRhdVeKQp-g1TxXovMALeAu_2oPmV0wOEEanesAKxjKYcJZQIe8HnVqgug6Ibs04uQ1mJ4RgfntPM-ebsJs-2tjFFkLEYJSkpq2o6SEFW9jBJyW8b8C5UJJahqynonA-Dw5GH1nin5bhhliLuFOmu0Ityt0uJ1Y_vuGsSA-ltVcY52jE4x6GH9NQxLX4ceO1bHSOmdspBoGsE_yo9-zsQw0g1_Iy7uqEjos3xrrboH6Z_u7pRL7AQJ7GNzZlinjYYPANQbYknieZD6beddTK7lvr4DYiPBmXzA

View File

@ -0,0 +1,56 @@
/*
* 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.core;
import java.util.ArrayList;
import java.util.Collection;
import org.springframework.util.Assert;
/**
* A composite validator
*
* @param <T> the type of {@link AbstractOAuth2Token} this validator validates
*
* @author Josh Cummings
* @since 5.1
*/
public final class DelegatingOAuth2TokenValidator<T extends AbstractOAuth2Token>
implements OAuth2TokenValidator<T> {
private final Collection<OAuth2TokenValidator<T>> tokenValidators;
public DelegatingOAuth2TokenValidator(Collection<OAuth2TokenValidator<T>> tokenValidators) {
Assert.notNull(tokenValidators, "tokenValidators cannot be null");
this.tokenValidators = new ArrayList<>(tokenValidators);
}
/**
* {@inheritDoc}
*/
@Override
public OAuth2TokenValidatorResult validate(T token) {
Collection<OAuth2Error> errors = new ArrayList<>();
for ( OAuth2TokenValidator<T> validator : this.tokenValidators) {
errors.addAll(validator.validate(token).getErrors());
}
return OAuth2TokenValidatorResult.failure(errors);
}
}

View File

@ -0,0 +1,35 @@
/*
* 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.core;
/**
* Implementations of this interface are responsible for &quot;verifying&quot;
* the validity and/or constraints of the attributes contained in an OAuth 2.0 Token.
*
* @author Joe Grandja
* @author Josh Cummings
* @since 5.1
*/
public interface OAuth2TokenValidator<T extends AbstractOAuth2Token> {
/**
* Verify the validity and/or constraints of the provided OAuth 2.0 Token.
*
* @param token an OAuth 2.0 token
* @return OAuth2TokenValidationResult the success or failure detail of the validation
*/
OAuth2TokenValidatorResult validate(T token);
}

View File

@ -0,0 +1,92 @@
/*
* 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.core;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import org.springframework.util.Assert;
/**
* A result emitted from an {@link OAuth2TokenValidator} validation attempt
*
* @author Josh Cummings
* @since 5.1
*/
public final class OAuth2TokenValidatorResult {
static final OAuth2TokenValidatorResult NO_ERRORS = new OAuth2TokenValidatorResult(Collections.emptyList());
private final Collection<OAuth2Error> errors;
private OAuth2TokenValidatorResult(Collection<OAuth2Error> errors) {
Assert.notNull(errors, "errors cannot be null");
this.errors = new ArrayList<>(errors);
}
/**
* Say whether this result indicates success
*
* @return whether this result has errors
*/
public boolean hasErrors() {
return !this.errors.isEmpty();
}
/**
* Return error details regarding the validation attempt
*
* @return the collection of results in this result, if any; returns an empty list otherwise
*/
public Collection<OAuth2Error> getErrors() {
return this.errors;
}
/**
* Construct a successful {@link OAuth2TokenValidatorResult}
*
* @return an {@link OAuth2TokenValidatorResult} with no errors
*/
public static OAuth2TokenValidatorResult success() {
return NO_ERRORS;
}
/**
* Construct a failure {@link OAuth2TokenValidatorResult} with the provided detail
*
* @param errors the list of errors
* @return an {@link OAuth2TokenValidatorResult} with the errors specified
*/
public static OAuth2TokenValidatorResult failure(OAuth2Error... errors) {
return failure(Arrays.asList(errors));
}
/**
* Construct a failure {@link OAuth2TokenValidatorResult} with the provided detail
*
* @param errors the list of errors
* @return an {@link OAuth2TokenValidatorResult} with the errors specified
*/
public static OAuth2TokenValidatorResult failure(Collection<OAuth2Error> errors) {
if (errors.isEmpty()) {
return NO_ERRORS;
}
return new OAuth2TokenValidatorResult(errors);
}
}

View File

@ -0,0 +1,123 @@
/*
* 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.core;
import java.util.Arrays;
import java.util.Collections;
import org.junit.Test;
import org.springframework.security.oauth2.core.AbstractOAuth2Token;
import org.springframework.security.oauth2.core.DelegatingOAuth2TokenValidator;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatCode;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
/**
* Tests for verifying {@link DelegatingOAuth2TokenValidator}
*
* @author Josh Cummings
*/
public class DelegatingOAuth2TokenValidatorTests {
private static final OAuth2Error DETAIL = new OAuth2Error(
"error", "description", "uri");
@Test
public void validateWhenNoValidatorsConfiguredThenReturnsSuccessfulResult() {
DelegatingOAuth2TokenValidator<AbstractOAuth2Token> tokenValidator =
new DelegatingOAuth2TokenValidator<>(Collections.emptyList());
AbstractOAuth2Token token = mock(AbstractOAuth2Token.class);
assertThat(tokenValidator.validate(token).hasErrors()).isFalse();
}
@Test
public void validateWhenAnyValidatorFailsThenReturnsFailureResultContainingDetailFromFailingValidator() {
OAuth2TokenValidator<AbstractOAuth2Token> success = mock(OAuth2TokenValidator.class);
OAuth2TokenValidator<AbstractOAuth2Token> failure = mock(OAuth2TokenValidator.class);
when(success.validate(any(AbstractOAuth2Token.class)))
.thenReturn(OAuth2TokenValidatorResult.success());
when(failure.validate(any(AbstractOAuth2Token.class)))
.thenReturn(OAuth2TokenValidatorResult.failure(DETAIL));
DelegatingOAuth2TokenValidator<AbstractOAuth2Token> tokenValidator =
new DelegatingOAuth2TokenValidator<>(Arrays.asList(success, failure));
AbstractOAuth2Token token = mock(AbstractOAuth2Token.class);
OAuth2TokenValidatorResult result =
tokenValidator.validate(token);
assertThat(result.hasErrors()).isTrue();
assertThat(result.getErrors()).containsExactly(DETAIL);
}
@Test
public void validateWhenMultipleValidatorsFailThenReturnsFailureResultContainingAllDetails() {
OAuth2TokenValidator<AbstractOAuth2Token> firstFailure = mock(OAuth2TokenValidator.class);
OAuth2TokenValidator<AbstractOAuth2Token> secondFailure = mock(OAuth2TokenValidator.class);
OAuth2Error otherDetail = new OAuth2Error("another-error");
when(firstFailure.validate(any(AbstractOAuth2Token.class)))
.thenReturn(OAuth2TokenValidatorResult.failure(DETAIL));
when(secondFailure.validate(any(AbstractOAuth2Token.class)))
.thenReturn(OAuth2TokenValidatorResult.failure(otherDetail));
DelegatingOAuth2TokenValidator<AbstractOAuth2Token> tokenValidator =
new DelegatingOAuth2TokenValidator<>(Arrays.asList(firstFailure, secondFailure));
AbstractOAuth2Token token = mock(AbstractOAuth2Token.class);
OAuth2TokenValidatorResult result =
tokenValidator.validate(token);
assertThat(result.hasErrors()).isTrue();
assertThat(result.getErrors()).containsExactly(DETAIL, otherDetail);
}
@Test
public void validateWhenAllValidatorsSucceedThenReturnsSuccessfulResult() {
OAuth2TokenValidator<AbstractOAuth2Token> firstSuccess = mock(OAuth2TokenValidator.class);
OAuth2TokenValidator<AbstractOAuth2Token> secondSuccess = mock(OAuth2TokenValidator.class);
when(firstSuccess.validate(any(AbstractOAuth2Token.class)))
.thenReturn(OAuth2TokenValidatorResult.success());
when(secondSuccess.validate(any(AbstractOAuth2Token.class)))
.thenReturn(OAuth2TokenValidatorResult.success());
DelegatingOAuth2TokenValidator<AbstractOAuth2Token> tokenValidator =
new DelegatingOAuth2TokenValidator<>(Arrays.asList(firstSuccess, secondSuccess));
AbstractOAuth2Token token = mock(AbstractOAuth2Token.class);
OAuth2TokenValidatorResult result =
tokenValidator.validate(token);
assertThat(result.hasErrors()).isFalse();
assertThat(result.getErrors()).isEmpty();
}
@Test
public void constructorWhenInvokedWithNullValidatorListThenThrowsIllegalArgumentException() {
assertThatCode(() -> new DelegatingOAuth2TokenValidator<>(null))
.isInstanceOf(IllegalArgumentException.class);
}
}

View File

@ -0,0 +1,55 @@
/*
* 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.core;
import org.junit.Test;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for verifying {@link OAuth2TokenValidatorResult}
*
* @author Josh Cummings
*/
public class OAuth2TokenValidatorResultTests {
private static final OAuth2Error DETAIL = new OAuth2Error(
"error", "description", "uri");
@Test
public void successWhenInvokedThenReturnsSuccessfulResult() {
OAuth2TokenValidatorResult success = OAuth2TokenValidatorResult.success();
assertThat(success.hasErrors()).isFalse();
}
@Test
public void failureWhenInvokedWithDetailReturnsFailureResultIncludingDetail() {
OAuth2TokenValidatorResult failure = OAuth2TokenValidatorResult.failure(DETAIL);
assertThat(failure.hasErrors()).isTrue();
assertThat(failure.getErrors()).containsExactly(DETAIL);
}
@Test
public void failureWhenInvokedWithMultipleDetailsReturnsFailureResultIncludingAll() {
OAuth2TokenValidatorResult failure = OAuth2TokenValidatorResult.failure(DETAIL, DETAIL);
assertThat(failure.hasErrors()).isTrue();
assertThat(failure.getErrors()).containsExactly(DETAIL, DETAIL);
}
}

View File

@ -0,0 +1,72 @@
/*
* 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.jwt;
import java.net.MalformedURLException;
import java.net.URL;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult;
import org.springframework.util.Assert;
/**
* Validates the "iss" claim in a {@link Jwt}, that is matches a configured value
*
* @author Josh Cummings
* @since 5.1
*/
public final class JwtIssuerValidator implements OAuth2TokenValidator<Jwt> {
private static OAuth2Error INVALID_ISSUER =
new OAuth2Error(
OAuth2ErrorCodes.INVALID_REQUEST,
"This iss claim is not equal to the configured issuer",
"https://tools.ietf.org/html/rfc6750#section-3.1");
private final URL issuer;
/**
* Constructs a {@link JwtIssuerValidator} using the provided parameters
*
* @param issuer - The issuer that each {@link Jwt} should have.
*/
public JwtIssuerValidator(String issuer) {
Assert.notNull(issuer, "issuer cannot be null");
try {
this.issuer = new URL(issuer);
} catch (MalformedURLException ex) {
throw new IllegalArgumentException(
"Invalid Issuer URL " + issuer + " : " + ex.getMessage(),
ex);
}
}
/**
* {@inheritDoc}
*/
@Override
public OAuth2TokenValidatorResult validate(Jwt token) {
Assert.notNull(token, "token cannot be null");
if (this.issuer.equals(token.getIssuer())) {
return OAuth2TokenValidatorResult.success();
} else {
return OAuth2TokenValidatorResult.failure(INVALID_ISSUER);
}
}
}

View File

@ -0,0 +1,109 @@
/*
* 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.jwt;
import java.time.Clock;
import java.time.Duration;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.util.Assert;
/**
* An implementation of {@see OAuth2TokenValidator} for verifying claims in a Jwt-based access token
*
* <p>
* Because clocks can differ between the Jwt source, say the Authorization Server, and its destination, say the
* Resource Server, there is a default clock leeway exercised when deciding if the current time is within the Jwt's
* specified operating window
*
* @author Josh Cummings
* @since 5.1
* @see Jwt
* @see OAuth2TokenValidator
* @see <a target="_blank" href="https://tools.ietf.org/html/rfc7519">JSON Web Token (JWT)</a>
*/
public final class JwtTimestampValidator implements OAuth2TokenValidator<Jwt> {
private static final Duration DEFAULT_MAX_CLOCK_SKEW = Duration.of(60, ChronoUnit.SECONDS);
private final Duration maxClockSkew;
private Clock clock = Clock.systemUTC();
/**
* A basic instance with no custom verification and the default max clock skew
*/
public JwtTimestampValidator() {
this(DEFAULT_MAX_CLOCK_SKEW);
}
public JwtTimestampValidator(Duration maxClockSkew) {
Assert.notNull(maxClockSkew, "maxClockSkew cannot be null");
this.maxClockSkew = maxClockSkew;
}
/**
* {@inheritDoc}
*/
@Override
public OAuth2TokenValidatorResult validate(Jwt jwt) {
Assert.notNull(jwt, "jwt cannot be null");
Instant expiry = jwt.getExpiresAt();
if (expiry != null) {
if (Instant.now(this.clock).minus(maxClockSkew).isAfter(expiry)) {
OAuth2Error error = new OAuth2Error(
OAuth2ErrorCodes.INVALID_REQUEST,
String.format("Jwt expired at %s", jwt.getExpiresAt()),
"https://tools.ietf.org/html/rfc6750#section-3.1");
return OAuth2TokenValidatorResult.failure(error);
}
}
Instant notBefore = jwt.getNotBefore();
if (notBefore != null) {
if (Instant.now(this.clock).plus(maxClockSkew).isBefore(notBefore)) {
OAuth2Error error = new OAuth2Error(
OAuth2ErrorCodes.INVALID_REQUEST,
String.format("Jwt used before %s", jwt.getNotBefore()),
"https://tools.ietf.org/html/rfc6750#section-3.1");
return OAuth2TokenValidatorResult.failure(error);
}
}
return OAuth2TokenValidatorResult.success();
}
/**
* '
* Use this {@link Clock} with {@link Instant#now()} for assessing
* timestamp validity
*
* @param clock
*/
public void setClock(Clock clock) {
Assert.notNull(clock, "clock cannot be null");
this.clock = clock;
}
}

View File

@ -0,0 +1,68 @@
/*
* 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.jwt;
import java.util.ArrayList;
import java.util.Collection;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult;
import org.springframework.util.Assert;
/**
* An exception that results from an unsuccessful
* {@link OAuth2TokenValidatorResult}
*
* @author Josh Cummings
* @since 5.1
*/
public class JwtValidationException extends JwtException {
private final Collection<OAuth2Error> errors;
/**
* Constructs a {@link JwtValidationException} using the provided parameters
*
* While each {@link OAuth2Error} does contain an error description, this constructor
* can take an overarching description that encapsulates the composition of failures
*
* That said, it is appropriate to pass one of the messages from the error list in as
* the exception description, for example:
*
* <pre>
* if ( result.hasErrors() ) {
* Collection<OAuth2Error> errors = result.getErrors();
* throw new JwtValidationException(errors.iterator().next().getDescription(), errors);
* }
* </pre>
*
* @param message - the exception message
* @param errors - a list of {@link OAuth2Error}s with extra detail about the validation result
*/
public JwtValidationException(String message, Collection<OAuth2Error> errors) {
super(message);
Assert.notEmpty(errors, "errors cannot be empty");
this.errors = new ArrayList<>(errors);
}
/**
* Return the list of {@link OAuth2Error}s associated with this exception
* @return the list of {@link OAuth2Error}s associated with this exception
*/
public Collection<OAuth2Error> getErrors() {
return this.errors;
}
}

View File

@ -0,0 +1,46 @@
/*
* 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.jwt;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import org.springframework.security.oauth2.core.DelegatingOAuth2TokenValidator;
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
/**
* @author Josh Cummings
* @since 5.1
*/
public final class JwtValidators {
/**
* Create a {@link Jwt} Validator that contains all standard validators as well as
* any supplied in the parameter list.
*
* @param jwtValidators - additional validators to include in the delegating validator
* @return - a delegating validator containing all standard validators as well as any supplied
*/
public static OAuth2TokenValidator<Jwt> createDelegatingJwtValidator(OAuth2TokenValidator<Jwt>... jwtValidators) {
Collection<OAuth2TokenValidator<Jwt>> validators = new ArrayList<>();
validators.add(new JwtTimestampValidator());
validators.addAll(Arrays.asList(jwtValidators));
return new DelegatingOAuth2TokenValidator<>(validators);
}
private JwtValidators() {}
}

View File

@ -15,6 +15,15 @@
*/
package org.springframework.security.oauth2.jwt;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.text.ParseException;
import java.time.Instant;
import java.util.Collections;
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;
@ -30,25 +39,19 @@ 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.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.RequestEntity;
import org.springframework.http.ResponseEntity;
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult;
import org.springframework.security.oauth2.jose.jws.JwsAlgorithms;
import org.springframework.util.Assert;
import org.springframework.web.client.RestOperations;
import org.springframework.web.client.RestTemplate;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.text.ParseException;
import java.time.Instant;
import java.util.Collections;
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
@ -75,6 +78,8 @@ public final class NimbusJwtDecoderJwkSupport implements JwtDecoder {
private final ConfigurableJWTProcessor<SecurityContext> jwtProcessor;
private final RestOperationsResourceRetriever jwkSetRetriever = new RestOperationsResourceRetriever();
private OAuth2TokenValidator<Jwt> jwtValidator = JwtValidators.createDelegatingJwtValidator();
/**
* Constructs a {@code NimbusJwtDecoderJwkSupport} using the provided parameters.
*
@ -104,17 +109,31 @@ public final class NimbusJwtDecoderJwkSupport implements JwtDecoder {
new JWSVerificationKeySelector<>(this.jwsAlgorithm, jwkSource);
this.jwtProcessor = new DefaultJWTProcessor<>();
this.jwtProcessor.setJWSKeySelector(jwsKeySelector);
// Spring Security validates the claim set independent from Nimbus
this.jwtProcessor.setJWTClaimsSetVerifier((claims, context) -> {});
}
@Override
public Jwt decode(String token) throws JwtException {
JWT jwt = this.parse(token);
if (jwt instanceof SignedJWT) {
return this.createJwt(token, jwt);
Jwt createdJwt = this.createJwt(token, jwt);
return this.validateJwt(createdJwt);
}
throw new JwtException("Unsupported algorithm of " + jwt.getHeader().getAlgorithm());
}
/**
* Use this {@link Jwt} Validator
*
* @param jwtValidator - the Jwt Validator to use
*/
public void setJwtValidator(OAuth2TokenValidator<Jwt> jwtValidator) {
Assert.notNull(jwtValidator, "jwtValidator cannot be null");
this.jwtValidator = jwtValidator;
}
private JWT parse(String token) {
try {
return JWTParser.parse(token);
@ -163,6 +182,18 @@ public final class NimbusJwtDecoderJwkSupport implements JwtDecoder {
return jwt;
}
private Jwt validateJwt(Jwt jwt){
OAuth2TokenValidatorResult result = this.jwtValidator.validate(jwt);
if (result.hasErrors()) {
String description = result.getErrors().iterator().next().getDescription();
throw new JwtValidationException(
String.format(DECODING_ERROR_MESSAGE_TEMPLATE, description),
result.getErrors());
}
return jwt;
}
/**
* Sets the {@link RestOperations} used when requesting the JSON Web Key (JWK) Set.
*

View File

@ -0,0 +1,92 @@
/*
* 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.jwt;
import java.time.Instant;
import java.util.Collections;
import java.util.Map;
import org.junit.Test;
import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult;
import org.springframework.security.oauth2.jose.jws.JwsAlgorithms;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.JwtClaimNames;
import org.springframework.security.oauth2.jwt.JwtIssuerValidator;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatCode;
/**
* @author Josh Cummings
* @since 5.1
*/
public class JwtIssuerValidatorTests {
private static final String MOCK_TOKEN = "token";
private static final Instant MOCK_ISSUED_AT = Instant.MIN;
private static final Instant MOCK_EXPIRES_AT = Instant.MAX;
private static final Map<String, Object> MOCK_HEADERS =
Collections.singletonMap("alg", JwsAlgorithms.RS256);
private static final String ISSUER = "https://issuer";
private final JwtIssuerValidator validator = new JwtIssuerValidator(ISSUER);
@Test
public void validateWhenIssuerMatchesThenReturnsSuccess() {
Jwt jwt = new Jwt(
MOCK_TOKEN,
MOCK_ISSUED_AT,
MOCK_EXPIRES_AT,
MOCK_HEADERS,
Collections.singletonMap("iss", ISSUER));
assertThat(this.validator.validate(jwt))
.isEqualTo(OAuth2TokenValidatorResult.success());
}
@Test
public void validateWhenIssuerMismatchesThenReturnsError() {
Jwt jwt = new Jwt(
MOCK_TOKEN,
MOCK_ISSUED_AT,
MOCK_EXPIRES_AT,
MOCK_HEADERS,
Collections.singletonMap(JwtClaimNames.ISS, "https://other"));
OAuth2TokenValidatorResult result = this.validator.validate(jwt);
assertThat(result.getErrors()).isNotEmpty();
}
@Test
public void validateWhenJwtIsNullThenThrowsIllegalArgumentException() {
assertThatCode(() -> this.validator.validate(null))
.isInstanceOf(IllegalArgumentException.class);
}
@Test
public void constructorWhenMalformedIssuerIsGivenThenThrowsIllegalArgumentException() {
assertThatCode(() -> new JwtIssuerValidator("issuer"))
.isInstanceOf(IllegalArgumentException.class);
}
@Test
public void constructorWhenNullIssuerIsGivenThenThrowsIllegalArgumentException() {
assertThatCode(() -> new JwtIssuerValidator(null))
.isInstanceOf(IllegalArgumentException.class);
}
}

View File

@ -0,0 +1,230 @@
/*
* 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.oauth2.jwt;
import java.time.Clock;
import java.time.Duration;
import java.time.Instant;
import java.time.ZoneId;
import java.util.Collection;
import java.util.Collections;
import java.util.Map;
import java.util.stream.Collectors;
import org.junit.Test;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult;
import org.springframework.security.oauth2.jose.jws.JwsAlgorithms;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.JwtClaimNames;
import org.springframework.security.oauth2.jwt.JwtTimestampValidator;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatCode;
/**
* Tests verifying {@link JwtTimestampValidator}
*
* @author Josh Cummings
*/
public class JwtTimestampValidatorTests {
private static final Clock MOCK_NOW = Clock.fixed(Instant.ofEpochMilli(0), ZoneId.systemDefault());
private static final String MOCK_TOKEN_VALUE = "token";
private static final Instant MOCK_ISSUED_AT = Instant.MIN;
private static final Map<String, Object> MOCK_HEADER = Collections.singletonMap("alg", JwsAlgorithms.RS256);
private static final Map<String, Object> MOCK_CLAIM_SET = Collections.singletonMap("some", "claim");
@Test
public void validateWhenJwtIsExpiredThenErrorMessageIndicatesExpirationTime() {
Instant oneHourAgo = Instant.now().minusSeconds(3600);
Jwt jwt = new Jwt(
MOCK_TOKEN_VALUE,
MOCK_ISSUED_AT,
oneHourAgo,
MOCK_HEADER,
MOCK_CLAIM_SET);
JwtTimestampValidator jwtValidator = new JwtTimestampValidator();
Collection<OAuth2Error> details = jwtValidator.validate(jwt).getErrors();
Collection<String> messages = details.stream().map(OAuth2Error::getDescription).collect(Collectors.toList());
assertThat(messages).contains("Jwt expired at " + oneHourAgo);
}
@Test
public void validateWhenJwtIsTooEarlyThenErrorMessageIndicatesNotBeforeTime() {
Instant oneHourFromNow = Instant.now().plusSeconds(3600);
Jwt jwt = new Jwt(
MOCK_TOKEN_VALUE,
MOCK_ISSUED_AT,
null,
MOCK_HEADER,
Collections.singletonMap(JwtClaimNames.NBF, oneHourFromNow));
JwtTimestampValidator jwtValidator = new JwtTimestampValidator();
Collection<OAuth2Error> details = jwtValidator.validate(jwt).getErrors();
Collection<String> messages = details.stream().map(OAuth2Error::getDescription).collect(Collectors.toList());
assertThat(messages).contains("Jwt used before " + oneHourFromNow);
}
@Test
public void validateWhenConfiguredWithClockSkewThenValidatesUsingThatSkew() {
Duration oneDayOff = Duration.ofDays(1);
JwtTimestampValidator jwtValidator = new JwtTimestampValidator(oneDayOff);
Instant now = Instant.now();
Instant almostOneDayAgo = now.minus(oneDayOff).plusSeconds(10);
Instant almostOneDayFromNow = now.plus(oneDayOff).minusSeconds(10);
Instant justOverOneDayAgo = now.minus(oneDayOff).minusSeconds(10);
Instant justOverOneDayFromNow = now.plus(oneDayOff).plusSeconds(10);
Jwt jwt = new Jwt(
MOCK_TOKEN_VALUE,
MOCK_ISSUED_AT,
almostOneDayAgo,
MOCK_HEADER,
Collections.singletonMap(JwtClaimNames.NBF, almostOneDayFromNow));
assertThat(jwtValidator.validate(jwt).hasErrors()).isFalse();
jwt = new Jwt(
MOCK_TOKEN_VALUE,
MOCK_ISSUED_AT,
justOverOneDayAgo,
MOCK_HEADER,
MOCK_CLAIM_SET);
OAuth2TokenValidatorResult result = jwtValidator.validate(jwt);
Collection<String> messages =
result.getErrors().stream().map(OAuth2Error::getDescription).collect(Collectors.toList());
assertThat(result.hasErrors()).isTrue();
assertThat(messages).contains("Jwt expired at " + justOverOneDayAgo);
jwt = new Jwt(
MOCK_TOKEN_VALUE,
MOCK_ISSUED_AT,
null,
MOCK_HEADER,
Collections.singletonMap(JwtClaimNames.NBF, justOverOneDayFromNow));
result = jwtValidator.validate(jwt);
messages =
result.getErrors().stream().map(OAuth2Error::getDescription).collect(Collectors.toList());
assertThat(result.hasErrors()).isTrue();
assertThat(messages).contains("Jwt used before " + justOverOneDayFromNow);
}
@Test
public void validateWhenConfiguredWithFixedClockThenValidatesUsingFixedTime() {
Jwt jwt = new Jwt(
MOCK_TOKEN_VALUE,
MOCK_ISSUED_AT,
Instant.now(MOCK_NOW),
MOCK_HEADER,
Collections.singletonMap("some", "claim"));
JwtTimestampValidator jwtValidator = new JwtTimestampValidator(Duration.ofNanos(0));
jwtValidator.setClock(MOCK_NOW);
assertThat(jwtValidator.validate(jwt).hasErrors()).isFalse();
jwt = new Jwt(
MOCK_TOKEN_VALUE,
MOCK_ISSUED_AT,
null,
MOCK_HEADER,
Collections.singletonMap(JwtClaimNames.NBF, Instant.now(MOCK_NOW)));
assertThat(jwtValidator.validate(jwt).hasErrors()).isFalse();
}
@Test
public void validateWhenNeitherExpiryNorNotBeforeIsSpecifiedThenReturnsSuccessfulResult() {
Jwt jwt = new Jwt(
MOCK_TOKEN_VALUE,
MOCK_ISSUED_AT,
null,
MOCK_HEADER,
MOCK_CLAIM_SET);
JwtTimestampValidator jwtValidator = new JwtTimestampValidator();
assertThat(jwtValidator.validate(jwt).hasErrors()).isFalse();
}
@Test
public void validateWhenNotBeforeIsValidAndExpiryIsNotSpecifiedThenReturnsSuccessfulResult() {
Jwt jwt = new Jwt(
MOCK_TOKEN_VALUE,
MOCK_ISSUED_AT,
null,
MOCK_HEADER,
Collections.singletonMap(JwtClaimNames.NBF, Instant.MIN));
JwtTimestampValidator jwtValidator = new JwtTimestampValidator();
assertThat(jwtValidator.validate(jwt).hasErrors()).isFalse();
}
@Test
public void validateWhenExpiryIsValidAndNotBeforeIsNotSpecifiedThenReturnsSuccessfulResult() {
Jwt jwt = new Jwt(
MOCK_TOKEN_VALUE,
MOCK_ISSUED_AT,
Instant.MAX,
MOCK_HEADER,
MOCK_CLAIM_SET);
JwtTimestampValidator jwtValidator = new JwtTimestampValidator();
assertThat(jwtValidator.validate(jwt).hasErrors()).isFalse();
}
@Test
public void validateWhenBothExpiryAndNotBeforeAreValidThenReturnsSuccessfulResult() {
Jwt jwt = new Jwt(
MOCK_TOKEN_VALUE,
MOCK_ISSUED_AT,
Instant.now(MOCK_NOW),
MOCK_HEADER,
Collections.singletonMap(JwtClaimNames.NBF, Instant.now(MOCK_NOW)));
JwtTimestampValidator jwtValidator = new JwtTimestampValidator(Duration.ofNanos(0));
jwtValidator.setClock(MOCK_NOW);
assertThat(jwtValidator.validate(jwt).hasErrors()).isFalse();
}
@Test
public void setClockWhenInvokedWithNullThenThrowsIllegalArgumentException() {
JwtTimestampValidator jwtValidator = new JwtTimestampValidator();
assertThatCode(() -> jwtValidator.setClock(null))
.isInstanceOf(IllegalArgumentException.class);
}
@Test
public void constructorWhenInvokedWithNullDurationThenThrowsIllegalArgumentException() {
assertThatCode(() -> new JwtTimestampValidator(null))
.isInstanceOf(IllegalArgumentException.class);
}
}

View File

@ -15,6 +15,8 @@
*/
package org.springframework.security.oauth2.jwt;
import java.util.Arrays;
import com.nimbusds.jose.JWSAlgorithm;
import com.nimbusds.jose.JWSHeader;
import com.nimbusds.jwt.JWT;
@ -30,16 +32,25 @@ 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.http.RequestEntity;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult;
import org.springframework.security.oauth2.jose.jws.JwsAlgorithms;
import org.springframework.web.client.RestTemplate;
import static org.assertj.core.api.AssertionsForClassTypes.assertThatCode;
import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.powermock.api.mockito.PowerMockito.*;
import static org.powermock.api.mockito.PowerMockito.mockStatic;
import static org.powermock.api.mockito.PowerMockito.spy;
import static org.powermock.api.mockito.PowerMockito.when;
import static org.powermock.api.mockito.PowerMockito.whenNew;
/**
* Tests for {@link NimbusJwtDecoderJwkSupport}.
@ -174,4 +185,47 @@ public class NimbusJwtDecoderJwkSupportTests {
server.shutdown();
}
}
@Test
public void decodeWhenJwtFailsValidationThenReturnsCorrespondingErrorMessage() 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);
OAuth2Error failure = new OAuth2Error("mock-error", "mock-description", "mock-uri");
OAuth2TokenValidator<Jwt> jwtValidator = mock(OAuth2TokenValidator.class);
when(jwtValidator.validate(any(Jwt.class))).thenReturn(OAuth2TokenValidatorResult.failure(failure));
decoder.setJwtValidator(jwtValidator);
assertThatCode(() -> decoder.decode(SIGNED_JWT))
.isInstanceOf(JwtValidationException.class)
.hasMessageContaining("mock-description");
}
}
@Test
public void decodeWhenJwtValidationHasTwoErrorsThenJwtExceptionMessageShowsFirstError() 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);
OAuth2Error firstFailure = new OAuth2Error("mock-error", "mock-description", "mock-uri");
OAuth2Error secondFailure = new OAuth2Error("another-error", "another-description", "another-uri");
OAuth2TokenValidatorResult result = OAuth2TokenValidatorResult.failure(firstFailure, secondFailure);
OAuth2TokenValidator<Jwt> jwtValidator = mock(OAuth2TokenValidator.class);
when(jwtValidator.validate(any(Jwt.class))).thenReturn(result);
decoder.setJwtValidator(jwtValidator);
assertThatCode(() -> decoder.decode(SIGNED_JWT))
.isInstanceOf(JwtValidationException.class)
.hasMessageContaining("mock-description")
.hasFieldOrPropertyWithValue("errors", Arrays.asList(firstFailure, secondFailure));
}
}
}

View File

@ -61,6 +61,9 @@ public final class JwtAuthenticationProvider implements AuthenticationProvider {
private final JwtConverter jwtConverter = new JwtConverter();
private static final OAuth2Error DEFAULT_INVALID_TOKEN =
invalidToken("An error occurred while attempting to decode the Jwt: Invalid token");
public JwtAuthenticationProvider(JwtDecoder jwtDecoder) {
Assert.notNull(jwtDecoder, "jwtDecoder cannot be null");
@ -84,15 +87,10 @@ public final class JwtAuthenticationProvider implements AuthenticationProvider {
try {
jwt = this.jwtDecoder.decode(bearer.getToken());
} catch (JwtException failed) {
OAuth2Error invalidToken;
try {
invalidToken = invalidToken(failed.getMessage());
} catch ( IllegalArgumentException malformed ) {
// some third-party library error messages are not suitable for RFC 6750's error message charset
invalidToken = invalidToken("An error occurred while attempting to decode the Jwt: Invalid token");
}
throw new OAuth2AuthenticationException(invalidToken, failed);
OAuth2Error invalidToken = invalidToken(failed.getMessage());
throw new OAuth2AuthenticationException(invalidToken, invalidToken.getDescription(), failed);
}
JwtAuthenticationToken token = this.jwtConverter.convert(jwt);
token.setDetails(bearer.getDetails());
@ -108,10 +106,15 @@ public final class JwtAuthenticationProvider implements AuthenticationProvider {
}
private static OAuth2Error invalidToken(String message) {
return new BearerTokenError(
BearerTokenErrorCodes.INVALID_TOKEN,
HttpStatus.UNAUTHORIZED,
message,
"https://tools.ietf.org/html/rfc6750#section-3.1");
try {
return new BearerTokenError(
BearerTokenErrorCodes.INVALID_TOKEN,
HttpStatus.UNAUTHORIZED,
message,
"https://tools.ietf.org/html/rfc6750#section-3.1");
} catch (IllegalArgumentException malformed) {
// some third-party library error messages are not suitable for RFC 6750's error message charset
return DEFAULT_INVALID_TOKEN;
}
}
}