Browse Source
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-5133pull/5645/head
16 changed files with 1225 additions and 25 deletions
@ -0,0 +1 @@
@@ -0,0 +1 @@
|
||||
eyJhbGciOiJSUzI1NiJ9.eyJleHAiOjQ2ODcxNzc5OTB9.RRQvqIZzLweq0iwWUZk1Dpiz6iUmT4bAVhGWqvWNWK3UwJ6aBIYsCRhdVeKQp-g1TxXovMALeAu_2oPmV0wOEEanesAKxjKYcJZQIe8HnVqgug6Ibs04uQ1mJ4RgfntPM-ebsJs-2tjFFkLEYJSkpq2o6SEFW9jBJyW8b8C5UJJahqynonA-Dw5GH1nin5bhhliLuFOmu0Ityt0uJ1Y_vuGsSA-ltVcY52jE4x6GH9NQxLX4ceO1bHSOmdspBoGsE_yo9-zsQw0g1_Iy7uqEjos3xrrboH6Z_u7pRL7AQJ7GNzZlinjYYPANQbYknieZD6beddTK7lvr4DYiPBmXzA |
||||
@ -0,0 +1,56 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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() {} |
||||
} |
||||
@ -0,0 +1,92 @@
@@ -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 @@
@@ -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); |
||||
} |
||||
} |
||||
Loading…
Reference in new issue