Forráskód Böngészése

Extract the ID Token JwtDecoderFactory to enable user customization

This commit ensures that the JwtDecoderFactory is not a private field inside
the Oidc authentication provider by extracting this class and giving the
possibility to customize the way different providers are validated.

Fixes: gh-6379
Rafael Dominguez 6 éve
szülő
commit
fe5f10e9a2

+ 2 - 37
oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/OidcAuthorizationCodeAuthenticationProvider.java

@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2018 the original author or authors.
+ * Copyright 2002-2019 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.
@@ -27,11 +27,9 @@ import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest;
 import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserService;
 import org.springframework.security.oauth2.client.registration.ClientRegistration;
 import org.springframework.security.oauth2.client.userinfo.OAuth2UserService;
-import org.springframework.security.oauth2.core.DelegatingOAuth2TokenValidator;
 import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
 import org.springframework.security.oauth2.core.OAuth2AuthorizationException;
 import org.springframework.security.oauth2.core.OAuth2Error;
-import org.springframework.security.oauth2.core.OAuth2TokenValidator;
 import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
 import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
 import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponse;
@@ -43,16 +41,10 @@ import org.springframework.security.oauth2.jwt.Jwt;
 import org.springframework.security.oauth2.jwt.JwtDecoder;
 import org.springframework.security.oauth2.jwt.JwtDecoderFactory;
 import org.springframework.security.oauth2.jwt.JwtException;
-import org.springframework.security.oauth2.jwt.JwtTimestampValidator;
-import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
 import org.springframework.util.Assert;
-import org.springframework.util.StringUtils;
 
 import java.util.Collection;
 import java.util.Map;
-import java.util.concurrent.ConcurrentHashMap;
-
-import static org.springframework.security.oauth2.jwt.JwtProcessors.withJwkSetUri;
 
 /**
  * An implementation of an {@link AuthenticationProvider}
@@ -82,10 +74,9 @@ public class OidcAuthorizationCodeAuthenticationProvider implements Authenticati
 	private static final String INVALID_STATE_PARAMETER_ERROR_CODE = "invalid_state_parameter";
 	private static final String INVALID_REDIRECT_URI_PARAMETER_ERROR_CODE = "invalid_redirect_uri_parameter";
 	private static final String INVALID_ID_TOKEN_ERROR_CODE = "invalid_id_token";
-	private static final String MISSING_SIGNATURE_VERIFIER_ERROR_CODE = "missing_signature_verifier";
 	private final OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> accessTokenResponseClient;
 	private final OAuth2UserService<OidcUserRequest, OidcUser> userService;
-	private JwtDecoderFactory<ClientRegistration> jwtDecoderFactory = new DefaultJwtDecoderFactory();
+	private JwtDecoderFactory<ClientRegistration> jwtDecoderFactory = new OidcIdTokenDecoderFactory();
 	private GrantedAuthoritiesMapper authoritiesMapper = (authorities -> authorities);
 
 	/**
@@ -219,30 +210,4 @@ public class OidcAuthorizationCodeAuthenticationProvider implements Authenticati
 		OidcIdToken idToken = new OidcIdToken(jwt.getTokenValue(), jwt.getIssuedAt(), jwt.getExpiresAt(), jwt.getClaims());
 		return idToken;
 	}
-
-	private static class DefaultJwtDecoderFactory implements JwtDecoderFactory<ClientRegistration> {
-		private final Map<String, JwtDecoder> jwtDecoders = new ConcurrentHashMap<>();
-
-		@Override
-		public JwtDecoder createDecoder(ClientRegistration clientRegistration) {
-			return this.jwtDecoders.computeIfAbsent(clientRegistration.getRegistrationId(), key -> {
-				if (!StringUtils.hasText(clientRegistration.getProviderDetails().getJwkSetUri())) {
-					OAuth2Error oauth2Error = new OAuth2Error(
-							MISSING_SIGNATURE_VERIFIER_ERROR_CODE,
-							"Failed to find a Signature Verifier for Client Registration: '" +
-									clientRegistration.getRegistrationId() +
-									"'. Check to ensure you have configured the JwkSet URI.",
-							null
-					);
-					throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
-				}
-				String jwkSetUri = clientRegistration.getProviderDetails().getJwkSetUri();
-				NimbusJwtDecoder jwtDecoder = new NimbusJwtDecoder(withJwkSetUri(jwkSetUri).build());
-				OAuth2TokenValidator<Jwt> jwtValidator = new DelegatingOAuth2TokenValidator<>(
-						new JwtTimestampValidator(), new OidcIdTokenValidator(clientRegistration));
-				jwtDecoder.setJwtValidator(jwtValidator);
-				return jwtDecoder;
-			});
-		}
-	}
 }

+ 2 - 36
oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/OidcAuthorizationCodeReactiveAuthenticationManager.java

@@ -1,5 +1,5 @@
 /*
- * Copyright 2002-2018 the original author or authors.
+ * Copyright 2002-2019 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.
@@ -26,12 +26,10 @@ import org.springframework.security.oauth2.client.endpoint.ReactiveOAuth2AccessT
 import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest;
 import org.springframework.security.oauth2.client.registration.ClientRegistration;
 import org.springframework.security.oauth2.client.userinfo.ReactiveOAuth2UserService;
-import org.springframework.security.oauth2.core.DelegatingOAuth2TokenValidator;
 import org.springframework.security.oauth2.core.OAuth2AccessToken;
 import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
 import org.springframework.security.oauth2.core.OAuth2AuthorizationException;
 import org.springframework.security.oauth2.core.OAuth2Error;
-import org.springframework.security.oauth2.core.OAuth2TokenValidator;
 import org.springframework.security.oauth2.core.endpoint.OAuth2AccessTokenResponse;
 import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
 import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponse;
@@ -39,19 +37,14 @@ import org.springframework.security.oauth2.core.oidc.OidcIdToken;
 import org.springframework.security.oauth2.core.oidc.endpoint.OidcParameterNames;
 import org.springframework.security.oauth2.core.oidc.user.OidcUser;
 import org.springframework.security.oauth2.core.user.OAuth2User;
-import org.springframework.security.oauth2.jwt.Jwt;
 import org.springframework.security.oauth2.jwt.JwtException;
-import org.springframework.security.oauth2.jwt.JwtTimestampValidator;
-import org.springframework.security.oauth2.jwt.NimbusReactiveJwtDecoder;
 import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder;
 import org.springframework.security.oauth2.jwt.ReactiveJwtDecoderFactory;
 import org.springframework.util.Assert;
-import org.springframework.util.StringUtils;
 import reactor.core.publisher.Mono;
 
 import java.util.Collection;
 import java.util.Map;
-import java.util.concurrent.ConcurrentHashMap;
 
 /**
  * An implementation of an {@link org.springframework.security.authentication.AuthenticationProvider} for OAuth 2.0 Login,
@@ -83,7 +76,6 @@ public class OidcAuthorizationCodeReactiveAuthenticationManager implements
 	private static final String INVALID_STATE_PARAMETER_ERROR_CODE = "invalid_state_parameter";
 	private static final String INVALID_REDIRECT_URI_PARAMETER_ERROR_CODE = "invalid_redirect_uri_parameter";
 	private static final String INVALID_ID_TOKEN_ERROR_CODE = "invalid_id_token";
-	private static final String MISSING_SIGNATURE_VERIFIER_ERROR_CODE = "missing_signature_verifier";
 
 	private final ReactiveOAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> accessTokenResponseClient;
 
@@ -91,7 +83,7 @@ public class OidcAuthorizationCodeReactiveAuthenticationManager implements
 
 	private GrantedAuthoritiesMapper authoritiesMapper = (authorities -> authorities);
 
-	private ReactiveJwtDecoderFactory<ClientRegistration> jwtDecoderFactory = new DefaultJwtDecoderFactory();
+	private ReactiveJwtDecoderFactory<ClientRegistration> jwtDecoderFactory = new ReactiveOidcIdTokenDecoderFactory();
 
 	public OidcAuthorizationCodeReactiveAuthenticationManager(
 			ReactiveOAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> accessTokenResponseClient,
@@ -199,30 +191,4 @@ public class OidcAuthorizationCodeReactiveAuthenticationManager implements
 		return jwtDecoder.decode(rawIdToken)
 				.map(jwt -> new OidcIdToken(jwt.getTokenValue(), jwt.getIssuedAt(), jwt.getExpiresAt(), jwt.getClaims()));
 	}
-
-	private static class DefaultJwtDecoderFactory implements ReactiveJwtDecoderFactory<ClientRegistration> {
-		private final Map<String, ReactiveJwtDecoder> jwtDecoders = new ConcurrentHashMap<>();
-
-		@Override
-		public ReactiveJwtDecoder createDecoder(ClientRegistration clientRegistration) {
-			return this.jwtDecoders.computeIfAbsent(clientRegistration.getRegistrationId(), key -> {
-				if (!StringUtils.hasText(clientRegistration.getProviderDetails().getJwkSetUri())) {
-					OAuth2Error oauth2Error = new OAuth2Error(
-							MISSING_SIGNATURE_VERIFIER_ERROR_CODE,
-							"Failed to find a Signature Verifier for Client Registration: '" +
-									clientRegistration.getRegistrationId() +
-									"'. Check to ensure you have configured the JwkSet URI.",
-							null
-					);
-					throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
-				}
-				NimbusReactiveJwtDecoder jwtDecoder = new NimbusReactiveJwtDecoder(
-						clientRegistration.getProviderDetails().getJwkSetUri());
-				OAuth2TokenValidator<Jwt> jwtValidator = new DelegatingOAuth2TokenValidator<>(
-						new JwtTimestampValidator(), new OidcIdTokenValidator(clientRegistration));
-				jwtDecoder.setJwtValidator(jwtValidator);
-				return jwtDecoder;
-			});
-		}
-	}
 }

+ 81 - 0
oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/OidcIdTokenDecoderFactory.java

@@ -0,0 +1,81 @@
+/*
+ * Copyright 2002-2019 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.client.oidc.authentication;
+
+import org.springframework.security.oauth2.client.registration.ClientRegistration;
+import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
+import org.springframework.security.oauth2.core.OAuth2Error;
+import org.springframework.security.oauth2.core.OAuth2TokenValidator;
+import org.springframework.security.oauth2.jwt.Jwt;
+import org.springframework.security.oauth2.jwt.JwtDecoder;
+import org.springframework.security.oauth2.jwt.JwtDecoderFactory;
+import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
+import org.springframework.util.Assert;
+import org.springframework.util.StringUtils;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.function.Function;
+
+import static org.springframework.security.oauth2.jwt.JwtProcessors.withJwkSetUri;
+
+/**
+ * Provides a default or custom implementation for {@link OAuth2TokenValidator}
+ *
+ * @author Joe Grandja
+ * @author Rafael Dominguez
+ * @since 5.2
+ *
+ * @see OAuth2TokenValidator
+ * @see Jwt
+ */
+public final class OidcIdTokenDecoderFactory implements JwtDecoderFactory<ClientRegistration> {
+	private static final String MISSING_SIGNATURE_VERIFIER_ERROR_CODE = "missing_signature_verifier";
+	private final Map<String, JwtDecoder> jwtDecoders = new ConcurrentHashMap<>();
+	private Function<ClientRegistration, OAuth2TokenValidator<Jwt>> jwtValidatorFactory = OidcIdTokenValidator::new;
+
+	@Override
+	public JwtDecoder createDecoder(ClientRegistration clientRegistration) {
+		Assert.notNull(clientRegistration, "clientRegistration cannot be null.");
+		return this.jwtDecoders.computeIfAbsent(clientRegistration.getRegistrationId(), key -> {
+			if (!StringUtils.hasText(clientRegistration.getProviderDetails().getJwkSetUri())) {
+				OAuth2Error oauth2Error = new OAuth2Error(
+						MISSING_SIGNATURE_VERIFIER_ERROR_CODE,
+						"Failed to find a Signature Verifier for Client Registration: '" +
+								clientRegistration.getRegistrationId() +
+								"'. Check to ensure you have configured the JwkSet URI.",
+						null
+				);
+				throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
+			}
+			String jwkSetUri = clientRegistration.getProviderDetails().getJwkSetUri();
+			NimbusJwtDecoder jwtDecoder = new NimbusJwtDecoder(withJwkSetUri(jwkSetUri).build());
+			OAuth2TokenValidator<Jwt> jwtValidator = jwtValidatorFactory.apply(clientRegistration);
+			jwtDecoder.setJwtValidator(jwtValidator);
+			return jwtDecoder;
+		});
+	}
+
+	/**
+	 * Allows user customization for the {@link OAuth2TokenValidator}
+	 *
+	 * @param jwtValidatorFactory
+	 */
+	public final void setJwtValidatorFactory(Function<ClientRegistration, OAuth2TokenValidator<Jwt>> jwtValidatorFactory) {
+		Assert.notNull(jwtValidatorFactory, "jwtValidatorFactory cannot be null.");
+		this.jwtValidatorFactory = jwtValidatorFactory;
+	}
+}

+ 79 - 0
oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/authentication/ReactiveOidcIdTokenDecoderFactory.java

@@ -0,0 +1,79 @@
+/*
+ * Copyright 2002-2019 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.client.oidc.authentication;
+
+import org.springframework.security.oauth2.client.registration.ClientRegistration;
+import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
+import org.springframework.security.oauth2.core.OAuth2Error;
+import org.springframework.security.oauth2.core.OAuth2TokenValidator;
+import org.springframework.security.oauth2.jwt.Jwt;
+import org.springframework.security.oauth2.jwt.NimbusReactiveJwtDecoder;
+import org.springframework.security.oauth2.jwt.ReactiveJwtDecoder;
+import org.springframework.security.oauth2.jwt.ReactiveJwtDecoderFactory;
+import org.springframework.util.Assert;
+import org.springframework.util.StringUtils;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.function.Function;
+
+/**
+ * Provides a default or custom reactive implementation for {@link OAuth2TokenValidator}
+ *
+ * @author Joe Grandja
+ * @author Rafael Dominguez
+ * @since 5.2
+ *
+ * @see OAuth2TokenValidator
+ * @see Jwt
+ */
+public final class ReactiveOidcIdTokenDecoderFactory implements ReactiveJwtDecoderFactory<ClientRegistration> {
+	private static final String MISSING_SIGNATURE_VERIFIER_ERROR_CODE = "missing_signature_verifier";
+	private final Map<String, ReactiveJwtDecoder> jwtDecoders = new ConcurrentHashMap<>();
+	private Function<ClientRegistration, OAuth2TokenValidator<Jwt>> jwtValidatorFactory = OidcIdTokenValidator::new;
+
+	@Override
+	public ReactiveJwtDecoder createDecoder(ClientRegistration clientRegistration) {
+		Assert.notNull(clientRegistration, "clientRegistration cannot be null.");
+		return this.jwtDecoders.computeIfAbsent(clientRegistration.getRegistrationId(), key -> {
+			if (!StringUtils.hasText(clientRegistration.getProviderDetails().getJwkSetUri())) {
+				OAuth2Error oauth2Error = new OAuth2Error(
+						MISSING_SIGNATURE_VERIFIER_ERROR_CODE,
+						"Failed to find a Signature Verifier for Client Registration: '" +
+								clientRegistration.getRegistrationId() +
+								"'. Check to ensure you have configured the JwkSet URI.",
+						null
+				);
+				throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
+			}
+			NimbusReactiveJwtDecoder jwtDecoder = new NimbusReactiveJwtDecoder(
+					clientRegistration.getProviderDetails().getJwkSetUri());
+			OAuth2TokenValidator<Jwt> jwtValidator = jwtValidatorFactory.apply(clientRegistration);
+			jwtDecoder.setJwtValidator(jwtValidator);
+			return jwtDecoder;
+		});
+	}
+
+	/**
+	 * Allows user customization for the {@link OAuth2TokenValidator}
+	 *
+	 * @param jwtValidatorFactory
+	 */
+	public final void setJwtValidatorFactory(Function<ClientRegistration, OAuth2TokenValidator<Jwt>> jwtValidatorFactory) {
+		Assert.notNull(jwtValidatorFactory, "jwtValidatorFactory cannot be null.");
+		this.jwtValidatorFactory = jwtValidatorFactory;
+	}
+}

+ 99 - 0
oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/authentication/OidcIdTokenDecoderFactoryTests.java

@@ -0,0 +1,99 @@
+/*
+ * Copyright 2002-2019 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.client.oidc.authentication;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.springframework.security.oauth2.client.registration.ClientRegistration;
+import org.springframework.security.oauth2.client.registration.TestClientRegistrations;
+import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
+import org.springframework.security.oauth2.core.OAuth2TokenValidator;
+import org.springframework.security.oauth2.jwt.Jwt;
+
+import java.time.Duration;
+import java.util.function.Function;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+/**
+ * @author Joe Grandja
+ * @author Rafael Dominguez
+ * @since 5.2
+ */
+public class OidcIdTokenDecoderFactoryTests {
+
+	private ClientRegistration.Builder registration = TestClientRegistrations.clientRegistration()
+			.scope("openid");
+
+	private OidcIdTokenDecoderFactory idTokenDecoderFactory;
+
+	@Before
+	public void setUp() {
+		idTokenDecoderFactory = new OidcIdTokenDecoderFactory();
+	}
+
+	@Test
+	public void setJwtValidatorFactoryWhenNullThenThrowIllegalArgumentException(){
+		assertThatThrownBy(()-> idTokenDecoderFactory.setJwtValidatorFactory(null))
+				.isInstanceOf(IllegalArgumentException.class);
+	}
+
+	@Test
+	public void createDecoderWhenClientRegistrationNullThenThrowIllegalArgumentException(){
+		assertThatThrownBy(() -> idTokenDecoderFactory.createDecoder(null))
+				.isInstanceOf(IllegalArgumentException.class);
+	}
+
+	@Test
+	public void createDecoderWhenJwkSetUriEmptyThenThrowOAuth2AuthenticationException(){
+		assertThatThrownBy(()-> idTokenDecoderFactory.createDecoder(registration.jwkSetUri(null).build()))
+		.isInstanceOf(OAuth2AuthenticationException.class);
+	}
+
+	@Test
+	public void createDecoderWhenClientRegistrationValidThenReturnDecoder(){
+		assertThat(idTokenDecoderFactory.createDecoder(registration.build()))
+				.isNotNull();
+	}
+
+	@Test
+	public void createDecoderWhenCustomJwtValidatorFactorySetThenApplied(){
+		Function<ClientRegistration, OAuth2TokenValidator<Jwt>> customValidator = mock(Function.class);
+		idTokenDecoderFactory.setJwtValidatorFactory(customValidator);
+
+		when(customValidator.apply(any(ClientRegistration.class)))
+				.thenReturn(customJwtValidatorFactory.apply(registration.build()));
+
+		idTokenDecoderFactory.createDecoder(registration.build());
+
+		verify(customValidator).apply(any(ClientRegistration.class));
+	}
+
+	private Function<ClientRegistration, OAuth2TokenValidator<Jwt>> customJwtValidatorFactory = (c) -> {
+		OidcIdTokenValidator idTokenValidator = new OidcIdTokenValidator(c);
+		if (c.getRegistrationId().equals("registration-id1")) {
+			idTokenValidator.setClockSkew(Duration.ofSeconds(30));
+		} else if (c.getRegistrationId().equals("registration-id2")) {
+			idTokenValidator.setClockSkew(Duration.ofSeconds(70));
+		}
+		return idTokenValidator;
+	};
+}

+ 99 - 0
oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/authentication/ReactiveOidcIdTokenDecoderFactoryTests.java

@@ -0,0 +1,99 @@
+/*
+ * Copyright 2002-2019 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.client.oidc.authentication;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.springframework.security.oauth2.client.registration.ClientRegistration;
+import org.springframework.security.oauth2.client.registration.TestClientRegistrations;
+import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
+import org.springframework.security.oauth2.core.OAuth2TokenValidator;
+import org.springframework.security.oauth2.jwt.Jwt;
+
+import java.time.Duration;
+import java.util.function.Function;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+/**
+ * @author Joe Grandja
+ * @author Rafael Dominguez
+ * @since 5.2
+ */
+public class ReactiveOidcIdTokenDecoderFactoryTests {
+
+	private ClientRegistration.Builder registration = TestClientRegistrations.clientRegistration()
+			.scope("openid");
+
+	private ReactiveOidcIdTokenDecoderFactory idTokenDecoderFactory;
+
+	@Before
+	public void setUp() {
+		idTokenDecoderFactory = new ReactiveOidcIdTokenDecoderFactory();
+	}
+
+	@Test
+	public void setJwtValidatorFactoryWhenNullThenThrowIllegalArgumentException(){
+		assertThatThrownBy(()-> idTokenDecoderFactory.setJwtValidatorFactory(null))
+				.isInstanceOf(IllegalArgumentException.class);
+	}
+
+	@Test
+	public void createDecoderWhenClientRegistrationNullThenThrowIllegalArgumentException(){
+		assertThatThrownBy(() -> idTokenDecoderFactory.createDecoder(null))
+				.isInstanceOf(IllegalArgumentException.class);
+	}
+
+	@Test
+	public void createDecoderWhenJwkSetUriEmptyThenThrowOAuth2AuthenticationException(){
+		assertThatThrownBy(()-> idTokenDecoderFactory.createDecoder(registration.jwkSetUri(null).build()))
+				.isInstanceOf(OAuth2AuthenticationException.class);
+	}
+
+	@Test
+	public void createDecoderWhenClientRegistrationValidThenReturnDecoder(){
+		assertThat(idTokenDecoderFactory.createDecoder(registration.build()))
+				.isNotNull();
+	}
+
+	@Test
+	public void createDecoderWhenCustomJwtValidatorFactorySetThenApplied(){
+		Function<ClientRegistration, OAuth2TokenValidator<Jwt>> customValidator = mock(Function.class);
+		idTokenDecoderFactory.setJwtValidatorFactory(customValidator);
+
+		when(customValidator.apply(any(ClientRegistration.class)))
+				.thenReturn(customJwtValidatorFactory.apply(registration.build()));
+
+		idTokenDecoderFactory.createDecoder(registration.build());
+
+		verify(customValidator).apply(any(ClientRegistration.class));
+	}
+
+	private Function<ClientRegistration, OAuth2TokenValidator<Jwt>> customJwtValidatorFactory = (c) -> {
+			OidcIdTokenValidator idTokenValidator = new OidcIdTokenValidator(c);
+			if (c.getRegistrationId().equals("registration-id1")) {
+				idTokenValidator.setClockSkew(Duration.ofSeconds(30));
+			} else if (c.getRegistrationId().equals("registration-id2")) {
+				idTokenValidator.setClockSkew(Duration.ofSeconds(70));
+			}
+			return idTokenValidator;
+		};
+}