Skip to content

Commit a7d111b

Browse files
committed
Consider using RetryTemplate for resolving provider configuration
Closes gh-18610 Signed-off-by: Evgeniy Cheban <mister.cheban@gmail.com>
1 parent 22a9858 commit a7d111b

File tree

1 file changed

+78
-20
lines changed

1 file changed

+78
-20
lines changed

oauth2/oauth2-jose/src/main/java/org/springframework/security/oauth2/jwt/JwtDecoderProviderConfigurationUtils.java

Lines changed: 78 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,14 @@
3333
import com.nimbusds.jose.proc.JWSVerificationKeySelector;
3434
import com.nimbusds.jose.proc.SecurityContext;
3535
import com.nimbusds.jwt.proc.ConfigurableJWTProcessor;
36+
import org.apache.commons.logging.Log;
37+
import org.apache.commons.logging.LogFactory;
3638

3739
import org.springframework.core.ParameterizedTypeReference;
40+
import org.springframework.core.retry.RetryException;
41+
import org.springframework.core.retry.RetryPolicy;
42+
import org.springframework.core.retry.RetryTemplate;
43+
import org.springframework.core.retry.Retryable;
3844
import org.springframework.http.RequestEntity;
3945
import org.springframework.http.ResponseEntity;
4046
import org.springframework.http.client.SimpleClientHttpRequestFactory;
@@ -55,10 +61,13 @@
5561
*
5662
* @author Thomas Vitale
5763
* @author Rafiullah Hamedy
64+
* @author Evgeniy Cheban
5865
* @since 5.2
5966
*/
6067
final class JwtDecoderProviderConfigurationUtils {
6168

69+
private static final Log LOG = LogFactory.getLog(JwtDecoderProviderConfigurationUtils.class);
70+
6271
private static final String OIDC_METADATA_PATH = "/.well-known/openid-configuration";
6372

6473
private static final String OAUTH_METADATA_PATH = "/.well-known/oauth-authorization-server";
@@ -158,28 +167,26 @@ private static String getMetadataIssuer(Map<String, Object> configuration) {
158167
}
159168

160169
private static Map<String, Object> getConfiguration(String issuer, RestOperations rest, UriComponents... uris) {
161-
String errorMessage = "Unable to resolve the Configuration with the provided Issuer of " + "\"" + issuer + "\"";
162-
for (UriComponents uri : uris) {
163-
try {
164-
RequestEntity<Void> request = RequestEntity.get(uri.toUriString()).build();
165-
ResponseEntity<Map<String, Object>> response = rest.exchange(request, STRING_OBJECT_MAP);
166-
Map<String, Object> configuration = response.getBody();
167-
Assert.notNull(configuration, "configuration must not be null");
168-
Assert.isTrue(configuration.get("jwks_uri") != null, "The public JWK set URI must not be null");
169-
return configuration;
170-
}
171-
catch (IllegalArgumentException ex) {
172-
throw ex;
173-
}
174-
catch (RuntimeException ex) {
175-
if (!(ex instanceof HttpClientErrorException
176-
&& ((HttpClientErrorException) ex).getStatusCode().is4xxClientError())) {
177-
throw new IllegalArgumentException(errorMessage, ex);
178-
}
179-
// else try another endpoint
170+
// @formatter:off
171+
RetryPolicy retryPolicy = RetryPolicy.builder()
172+
.excludes(IllegalArgumentException.class)
173+
.maxRetries(uris.length)
174+
.build();
175+
// @formatter:on
176+
RetryTemplate retryTemplate = new RetryTemplate(retryPolicy);
177+
try {
178+
return retryTemplate.execute(new RetryableConfiguration(rest, issuer, uris));
179+
}
180+
catch (RetryException ex) {
181+
Throwable lastException = ex.getLastException();
182+
LOG.error("""
183+
Unable to resolve the Configuration with the provided Issuer of "%s", after: %d attempts
184+
""".formatted(issuer, ex.getRetryCount()), lastException);
185+
if (lastException instanceof RuntimeException) {
186+
throw (RuntimeException) lastException;
180187
}
188+
throw new RuntimeException(lastException.getMessage(), lastException);
181189
}
182-
throw new IllegalArgumentException(errorMessage);
183190
}
184191

185192
static UriComponents oidc(String issuer) {
@@ -209,4 +216,55 @@ static UriComponents oauth(String issuer) {
209216
// @formatter:on
210217
}
211218

219+
private static final class RetryableConfiguration implements Retryable<Map<String, Object>> {
220+
221+
private int currentUriIndex = 0;
222+
223+
private final RestOperations rest;
224+
225+
private final String issuer;
226+
227+
private final UriComponents[] uris;
228+
229+
private RetryableConfiguration(RestOperations rest, String issuer, UriComponents[] uris) {
230+
this.rest = rest;
231+
this.issuer = issuer;
232+
this.uris = uris;
233+
}
234+
235+
@Override
236+
public Map<String, Object> execute() {
237+
if (this.currentUriIndex < this.uris.length) {
238+
try {
239+
UriComponents uri = this.uris[this.currentUriIndex++];
240+
RequestEntity<Void> request = RequestEntity.get(uri.toUriString()).build();
241+
ResponseEntity<Map<String, Object>> response;
242+
response = this.rest.exchange(request, STRING_OBJECT_MAP);
243+
Map<String, Object> configuration = response.getBody();
244+
Assert.notNull(configuration, "configuration must not be null");
245+
Assert.isTrue(configuration.get("jwks_uri") != null, "The public JWK set URI must not be null");
246+
return configuration;
247+
}
248+
catch (IllegalArgumentException ex) {
249+
throw ex;
250+
}
251+
catch (RuntimeException ex) {
252+
if (!(ex instanceof HttpClientErrorException err && err.getStatusCode().is4xxClientError())) {
253+
throw new IllegalArgumentException(getErrorMessage(), ex);
254+
}
255+
// else try another endpoint
256+
throw ex;
257+
}
258+
}
259+
throw new IllegalArgumentException(getErrorMessage());
260+
}
261+
262+
private String getErrorMessage() {
263+
return """
264+
Unable to resolve the Configuration with the provided Issuer of "%s"
265+
""".formatted(this.issuer);
266+
}
267+
268+
}
269+
212270
}

0 commit comments

Comments
 (0)