Skip to content

Commit f703594

Browse files
committed
feat(saml): refactor SAML metadata fetching to improve error handling and registration building
1 parent b1b41f3 commit f703594

File tree

5 files changed

+64
-433
lines changed

5 files changed

+64
-433
lines changed

backend/src/main/java/com/park/utmstack/config/SecurityConfiguration.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -128,8 +128,7 @@ public void configure(HttpSecurity http) throws Exception {
128128
.and()
129129
.saml2Login()
130130
.successHandler(new Saml2LoginSuccessHandler(tokenProvider,
131-
userRepository,
132-
saml2LoginFailureHandler()))
131+
userRepository))
133132
.failureHandler(new Saml2LoginFailureHandler())
134133
.and()
135134
.apply(securityConfigurerAdapterForJwt())

backend/src/main/java/com/park/utmstack/config/saml/SamlMetadataFetcher.java

Lines changed: 35 additions & 133 deletions
Original file line numberDiff line numberDiff line change
@@ -2,154 +2,56 @@
22

33
import com.park.utmstack.domain.idp_provider.IdentityProviderConfig;
44
import lombok.extern.slf4j.Slf4j;
5+
import org.springframework.security.saml2.core.Saml2X509Credential;
56
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistration;
67
import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrations;
7-
import org.springframework.stereotype.Component;
88

9+
import java.security.PrivateKey;
10+
import java.security.cert.X509Certificate;
911
import java.time.Duration;
1012
import java.util.concurrent.*;
1113

12-
/**
13-
* Responsible for fetching SAML metadata with timeout handling.
14-
* Separates the concern of async metadata fetching from registration building.
15-
*/
1614
@Slf4j
1715
public class SamlMetadataFetcher {
1816

19-
private static final Duration METADATA_FETCH_TIMEOUT = Duration.ofSeconds(10);
20-
21-
/**
22-
* Fetches SAML metadata with timeout protection.
23-
* Returns null if timeout, error, or interruption occurs.
24-
* Logs detailed error information instead of throwing exceptions.
25-
*
26-
* @param entity Provider configuration
27-
* @return RelyingPartyRegistration, or null if fetch fails
28-
*/
29-
public RelyingPartyRegistration fetchMetadataWithTimeout(IdentityProviderConfig entity) {
30-
ExecutorService timeoutExecutor = null;
31-
Future<RelyingPartyRegistration> future = null;
17+
private static final Duration TIMEOUT = Duration.ofSeconds(10);
18+
19+
private final ExecutorService executor = Executors.newFixedThreadPool(5, r -> {
20+
Thread t = new Thread(r);
21+
t.setName("saml-metadata-fetch");
22+
t.setDaemon(true);
23+
return t;
24+
});
25+
26+
public RelyingPartyRegistration fetch(IdentityProviderConfig entity,
27+
PrivateKey spKey,
28+
X509Certificate spCert) {
29+
30+
CompletableFuture<RelyingPartyRegistration> future =
31+
CompletableFuture.supplyAsync(() -> {
32+
try {
33+
return RelyingPartyRegistrations
34+
.fromMetadataLocation(entity.getMetadataUrl())
35+
.registrationId(entity.getName())
36+
.entityId(entity.getSpEntityId())
37+
.assertionConsumerServiceLocation(entity.getSpAcsUrl())
38+
.signingX509Credentials(c -> c.add(Saml2X509Credential.signing(spKey, spCert)))
39+
.build();
40+
} catch (Exception e) {
41+
throw new CompletionException(e);
42+
}
43+
}, executor);
3244

3345
try {
34-
timeoutExecutor = createMetadataFetchExecutor(entity);
35-
36-
future = CompletableFuture.supplyAsync(() -> {
37-
try {
38-
return RelyingPartyRegistrations
39-
.fromMetadataLocation(entity.getMetadataUrl())
40-
.registrationId(entity.getName())
41-
.build();
42-
} catch (Exception e) {
43-
throw new CompletionException(e);
44-
}
45-
}, timeoutExecutor);
46-
47-
return future.get(METADATA_FETCH_TIMEOUT.getSeconds(), TimeUnit.SECONDS);
48-
49-
} catch (TimeoutException e) {
50-
handleTimeoutException(entity, future, e);
51-
return null;
52-
53-
} catch (ExecutionException e) {
54-
handleExecutionException(entity, e);
55-
return null;
56-
57-
} catch (InterruptedException e) {
58-
handleInterruptedException(entity, e);
59-
return null;
60-
61-
} finally {
62-
cleanupExecutor(entity, timeoutExecutor);
63-
}
64-
}
46+
return future.get(TIMEOUT.getSeconds(), TimeUnit.SECONDS);
6547

66-
/**
67-
* Creates an executor for metadata fetching with proper naming and exception handling.
68-
*/
69-
private ExecutorService createMetadataFetchExecutor(IdentityProviderConfig entity) {
70-
return Executors.newSingleThreadExecutor(r -> {
71-
Thread t = new Thread(r);
72-
t.setName("saml-metadata-fetch-" + entity.getName());
73-
t.setDaemon(true);
74-
t.setUncaughtExceptionHandler((thread, throwable) ->
75-
log.error("Uncaught exception in SAML metadata fetch thread for {}: {}",
76-
entity.getName(), throwable.getMessage(), throwable)
77-
);
78-
return t;
79-
});
80-
}
81-
82-
/**
83-
* Handles timeout exception with detailed logging.
84-
*/
85-
private void handleTimeoutException(IdentityProviderConfig entity, Future<?> future, TimeoutException e) {
86-
if (future != null) {
48+
} catch (Exception e) {
8749
future.cancel(true);
50+
log.error("Metadata fetch failed for provider '{}': {}", entity.getName(), e.getMessage());
51+
return null;
8852
}
89-
log.error(
90-
"SAML metadata fetch TIMEOUT: Provider='{}', Timeout={}s, MetadataUrl='{}'. " +
91-
"This provider will not be available for SSO until it responds faster or the endpoint is fixed.",
92-
entity.getName(),
93-
METADATA_FETCH_TIMEOUT.getSeconds(),
94-
entity.getMetadataUrl(),
95-
e
96-
);
97-
}
98-
99-
/**
100-
* Handles execution exception with root cause extraction.
101-
*/
102-
private void handleExecutionException(IdentityProviderConfig entity, ExecutionException e) {
103-
Throwable rootCause = e.getCause() != null ? e.getCause() : e;
104-
log.error(
105-
"SAML metadata fetch FAILED: Provider='{}'. Root cause: {}. " +
106-
"Error details: {}. This provider will not be available for SSO.",
107-
entity.getName(),
108-
rootCause.getClass().getSimpleName(),
109-
rootCause.getMessage(),
110-
rootCause
111-
);
112-
}
113-
114-
/**
115-
* Handles interruption exception.
116-
*/
117-
private void handleInterruptedException(IdentityProviderConfig entity, InterruptedException e) {
118-
Thread.currentThread().interrupt();
119-
log.error(
120-
"SAML metadata fetch INTERRUPTED: Provider='{}'. " +
121-
"Current thread was interrupted. Thread status restored. " +
122-
"This provider will not be available for SSO.",
123-
entity.getName(),
124-
e
125-
);
12653
}
54+
}
12755

128-
/**
129-
* Safely shuts down the executor and logs any issues.
130-
*/
131-
private void cleanupExecutor(IdentityProviderConfig entity, ExecutorService executor) {
132-
if (executor != null) {
133-
try {
134-
executor.shutdownNow();
13556

136-
if (!executor.awaitTermination(2, TimeUnit.SECONDS)) {
137-
log.warn(
138-
"Executor for SAML provider '{}' did not terminate cleanly within 2 seconds. " +
139-
"Potential thread leak detected.",
140-
entity.getName()
141-
);
142-
}
143-
} catch (InterruptedException e) {
144-
Thread.currentThread().interrupt();
145-
log.error(
146-
"Interrupted while waiting for executor shutdown for SAML provider '{}'. " +
147-
"Thread status restored.",
148-
entity.getName(),
149-
e
150-
);
151-
}
152-
}
153-
}
154-
}
15557

backend/src/main/java/com/park/utmstack/config/saml/SamlProvidersLoader.java

Lines changed: 0 additions & 167 deletions
This file was deleted.

0 commit comments

Comments
 (0)