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:
parent
c6ea447cc0
commit
7c524aa0c8
|
@ -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
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
eyJhbGciOiJSUzI1NiJ9.eyJleHAiOjQ2ODcxNzc5OTB9.RRQvqIZzLweq0iwWUZk1Dpiz6iUmT4bAVhGWqvWNWK3UwJ6aBIYsCRhdVeKQp-g1TxXovMALeAu_2oPmV0wOEEanesAKxjKYcJZQIe8HnVqgug6Ibs04uQ1mJ4RgfntPM-ebsJs-2tjFFkLEYJSkpq2o6SEFW9jBJyW8b8C5UJJahqynonA-Dw5GH1nin5bhhliLuFOmu0Ityt0uJ1Y_vuGsSA-ltVcY52jE4x6GH9NQxLX4ceO1bHSOmdspBoGsE_yo9-zsQw0g1_Iy7uqEjos3xrrboH6Z_u7pRL7AQJ7GNzZlinjYYPANQbYknieZD6beddTK7lvr4DYiPBmXzA
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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 "verifying"
|
||||
* 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);
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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() {}
|
||||
}
|
|
@ -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.
|
||||
*
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue