Skip to content

Commit 3d3eb64

Browse files
committed
Enhance OIDC Token Validation: Added detailed documentation, improved timeout configurations, and introduced new validation methods. Updated the OidcBearerAuthenticationHandler and OnlineTokenValidator to support enhanced error handling and resource management. Added comprehensive unit tests for Offline and Online token validators to ensure robust functionality.
1 parent 1018d25 commit 3d3eb64

9 files changed

Lines changed: 1947 additions & 95 deletions

File tree

src/main/java/org/apache/sling/auth/oauth_client/impl/OidcBearerAuthenticationHandler.java

Lines changed: 116 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
import org.apache.sling.auth.oauth_client.spi.UserInfoProcessor;
4444
import org.apache.sling.jcr.resource.api.JcrResourceConstants;
4545
import org.jetbrains.annotations.NotNull;
46+
import org.jetbrains.annotations.Nullable;
4647
import org.osgi.framework.BundleContext;
4748
import org.osgi.service.component.annotations.Activate;
4849
import org.osgi.service.component.annotations.Component;
@@ -56,7 +57,22 @@
5657

5758
/**
5859
* Authentication handler that validates bearer tokens from the Authorization header.
59-
* Valid tokens are cached to improve performance.
60+
*
61+
* <p>This handler extracts bearer tokens from the HTTP Authorization header, validates them
62+
* using a configured {@link TokenValidator}, and creates authentication credentials for
63+
* valid tokens.</p>
64+
*
65+
* <h2>Features</h2>
66+
* <ul>
67+
* <li>Token validation via pluggable {@link TokenValidator} services</li>
68+
* <li>Optional user info fetching from the OIDC UserInfo endpoint</li>
69+
* <li>Token caching for improved performance</li>
70+
* <li>Integration with {@link UserInfoProcessor} for custom credential creation</li>
71+
* </ul>
72+
*
73+
* @see TokenValidator
74+
* @see UserInfoProcessor
75+
* @since 0.1.7
6076
*/
6177
@Component(service = AuthenticationHandler.class, immediate = true)
6278
@Designate(ocd = OidcBearerAuthenticationHandler.Config.class, factory = true)
@@ -67,27 +83,45 @@ public class OidcBearerAuthenticationHandler extends DefaultAuthenticationFeedba
6783
private static final String AUTH_TYPE = "oidc-bearer";
6884
private static final String BEARER_PREFIX = "Bearer ";
6985

86+
/**
87+
* Default HTTP connection timeout in milliseconds.
88+
*/
89+
private static final int DEFAULT_HTTP_CONNECT_TIMEOUT_MS = 5000;
90+
91+
/**
92+
* Default HTTP read timeout in milliseconds.
93+
*/
94+
private static final int DEFAULT_HTTP_READ_TIMEOUT_MS = 5000;
95+
96+
@NotNull
7097
private final Map<String, ClientConnection> connections;
98+
99+
@NotNull
71100
private final Map<String, TokenValidator> tokenValidators;
101+
102+
@NotNull
72103
private final Map<String, UserInfoProcessor> userInfoProcessors;
104+
105+
@NotNull
73106
private final String idp;
107+
108+
@NotNull
74109
private final String connectionName;
110+
111+
@NotNull
75112
private final String validatorName;
113+
114+
@NotNull
76115
private final String[] path;
116+
77117
private final long cacheTtlSeconds;
78118
private final int cacheMaxSize;
79119
private final boolean fetchUserInfo;
80-
81-
/**
82-
* Gets the configured connection.
83-
*
84-
* @return the connection to use, or null if not found
85-
*/
86-
private ClientConnection getConnection() {
87-
return connections.get(connectionName);
88-
}
120+
private final int httpConnectTimeoutMs;
121+
private final int httpReadTimeoutMs;
89122

90123
// Cache structure: token -> CachedToken
124+
@NotNull
91125
private final Map<String, CachedToken> tokenCache = new ConcurrentHashMap<>();
92126

93127
@ObjectClassDefinition(
@@ -99,23 +133,27 @@ private ClientConnection getConnection() {
99133
description =
100134
"Repository path for which this authentication handler should be used by Sling. If this is "
101135
+ "empty, the authentication handler will be disabled. By default this is set to \"/\".")
136+
@NotNull
102137
String[] path() default {"/"};
103138

104139
@AttributeDefinition(
105140
name = "Sync Handler Configuration Name",
106141
description = "Name of Sync Handler Configuration")
142+
@NotNull
107143
String idp() default "oidc-bearer";
108144

109145
@AttributeDefinition(
110146
name = "Connection Name",
111147
description =
112148
"Name of the OIDC connection to use for bearer token validation. REQUIRED: Must be configured with a valid connection name.")
149+
@NotNull
113150
String connectionName();
114151

115152
@AttributeDefinition(
116153
name = "Token Validator Name",
117154
description =
118155
"Name of the token validator service to use for token validation. REQUIRED: Must be configured with a valid validator name (e.g., an OfflineTokenValidator or OnlineTokenValidator instance).")
156+
@NotNull
119157
String validatorName();
120158

121159
@AttributeDefinition(
@@ -135,17 +173,49 @@ private ClientConnection getConnection() {
135173
description = "Maximum number of tokens to cache. Default is 1000.")
136174
int cacheMaxSize() default 1000;
137175

176+
@AttributeDefinition(
177+
name = "HTTP Connect Timeout (ms)",
178+
description =
179+
"Timeout in milliseconds for establishing HTTP connections (e.g., to UserInfo endpoint). Default: 5000ms.")
180+
int httpConnectTimeoutMs() default 5000;
181+
182+
@AttributeDefinition(
183+
name = "HTTP Read Timeout (ms)",
184+
description =
185+
"Timeout in milliseconds for reading HTTP responses (e.g., from UserInfo endpoint). Default: 5000ms.")
186+
int httpReadTimeoutMs() default 5000;
187+
138188
@AttributeDefinition(name = "Service Ranking", description = "Service ranking for this authentication handler")
139189
int service_ranking() default 0;
140190
}
141191

192+
/**
193+
* Gets the configured connection.
194+
*
195+
* @return the connection to use, or {@code null} if not found
196+
*/
197+
@Nullable
198+
private ClientConnection getConnection() {
199+
return connections.get(connectionName);
200+
}
201+
202+
/**
203+
* Activates the bearer authentication handler with the given configuration.
204+
*
205+
* @param bundleContext the OSGi bundle context
206+
* @param connections the available client connections
207+
* @param tokenValidators the available token validators
208+
* @param userInfoProcessors the available user info processors
209+
* @param config the OSGi configuration
210+
* @throws IllegalArgumentException if the configuration is invalid
211+
*/
142212
@Activate
143213
public OidcBearerAuthenticationHandler(
144214
@NotNull BundleContext bundleContext,
145-
@Reference List<ClientConnection> connections,
146-
@Reference List<TokenValidator> tokenValidators,
147-
@Reference(policyOption = ReferencePolicyOption.GREEDY) List<UserInfoProcessor> userInfoProcessors,
148-
Config config) {
215+
@NotNull @Reference List<ClientConnection> connections,
216+
@NotNull @Reference List<TokenValidator> tokenValidators,
217+
@NotNull @Reference(policyOption = ReferencePolicyOption.GREEDY) List<UserInfoProcessor> userInfoProcessors,
218+
@NotNull Config config) {
149219

150220
this.connections = connections.stream().collect(Collectors.toMap(ClientConnection::name, Function.identity()));
151221
this.tokenValidators =
@@ -160,6 +230,12 @@ public OidcBearerAuthenticationHandler(
160230
this.cacheMaxSize = config.cacheMaxSize();
161231
this.fetchUserInfo = config.fetchUserInfo();
162232

233+
// Initialize HTTP timeouts with validation
234+
int connectTimeout = config.httpConnectTimeoutMs();
235+
int readTimeout = config.httpReadTimeoutMs();
236+
this.httpConnectTimeoutMs = connectTimeout > 0 ? connectTimeout : DEFAULT_HTTP_CONNECT_TIMEOUT_MS;
237+
this.httpReadTimeoutMs = readTimeout > 0 ? readTimeout : DEFAULT_HTTP_READ_TIMEOUT_MS;
238+
163239
// Validate that connectionName is configured
164240
if (connectionName == null || connectionName.isEmpty()) {
165241
throw new IllegalArgumentException("Connection name not configured");
@@ -199,14 +275,18 @@ public OidcBearerAuthenticationHandler(
199275
null);
200276

201277
logger.info(
202-
"OidcBearerAuthenticationHandler successfully activated with connection: {}, validator: {}, cache TTL: {}s, max size: {}",
278+
"OidcBearerAuthenticationHandler successfully activated with connection: {}, validator: {}, "
279+
+ "cache TTL: {}s, max size: {}, HTTP timeouts: connect={}ms, read={}ms",
203280
connectionName,
204281
validatorName,
205282
cacheTtlSeconds,
206-
cacheMaxSize);
283+
cacheMaxSize,
284+
httpConnectTimeoutMs,
285+
httpReadTimeoutMs);
207286
}
208287

209288
@Override
289+
@Nullable
210290
public AuthenticationInfo extractCredentials(
211291
@NotNull HttpServletRequest request, @NotNull HttpServletResponse response) {
212292
logger.debug("extractCredentials: checking for bearer token");
@@ -283,8 +363,9 @@ public AuthenticationInfo extractCredentials(
283363
*
284364
* @param token the bearer token to use for authentication
285365
* @param connection the OIDC connection
286-
* @return user info as JSON string, or null if fetch fails
366+
* @return user info as JSON string, or {@code null} if fetch fails
287367
*/
368+
@Nullable
288369
private String fetchUserInfoJson(@NotNull String token, @NotNull ClientConnection connection) {
289370
try {
290371
// Get userInfo URL from connection
@@ -301,14 +382,14 @@ private String fetchUserInfoJson(@NotNull String token, @NotNull ClientConnectio
301382

302383
logger.debug("Fetching user info from: {}", userInfoUrl);
303384

304-
// Make HTTP request to UserInfo endpoint
385+
// Make HTTP request to UserInfo endpoint with configurable timeouts
305386
java.net.HttpURLConnection urlConnection =
306387
(java.net.HttpURLConnection) new URL(userInfoUrl).openConnection();
307388
urlConnection.setRequestMethod("GET");
308389
urlConnection.setRequestProperty("Authorization", "Bearer " + token);
309390
urlConnection.setRequestProperty("Accept", "application/json");
310-
urlConnection.setConnectTimeout(5000);
311-
urlConnection.setReadTimeout(5000);
391+
urlConnection.setConnectTimeout(httpConnectTimeoutMs);
392+
urlConnection.setReadTimeout(httpReadTimeoutMs);
312393

313394
int responseCode = urlConnection.getResponseCode();
314395
if (responseCode != 200) {
@@ -355,15 +436,16 @@ private String fetchUserInfoJson(@NotNull String token, @NotNull ClientConnectio
355436
*
356437
* @param subject the subject from the token
357438
* @param connection the OIDC connection used for validation
358-
* @param userInfoJson the user info JSON (may be null)
439+
* @param userInfoJson the user info JSON (may be {@code null})
359440
* @param tokenClaims the token claims as a map
360441
* @param token the raw token string
361442
* @return AuthenticationInfo object
362443
*/
363-
private @NotNull AuthenticationInfo createAuthenticationInfoWithProcessor(
444+
@NotNull
445+
private AuthenticationInfo createAuthenticationInfoWithProcessor(
364446
@NotNull String subject,
365447
@NotNull ClientConnection connection,
366-
String userInfoJson,
448+
@Nullable String userInfoJson,
367449
@NotNull Map<String, Object> tokenClaims,
368450
@NotNull String token) {
369451

@@ -406,7 +488,8 @@ private String fetchUserInfoJson(@NotNull String token, @NotNull ClientConnectio
406488
* @param token the raw token string
407489
* @return AuthenticationInfo object
408490
*/
409-
private @NotNull AuthenticationInfo createAuthenticationInfoFallback(
491+
@NotNull
492+
private AuthenticationInfo createAuthenticationInfoFallback(
410493
@NotNull String subject, @NotNull Map<String, Object> tokenClaims, @NotNull String token) {
411494
AuthenticationInfo authInfo = new AuthenticationInfo(AUTH_TYPE, subject);
412495

@@ -426,14 +509,15 @@ private String fetchUserInfoJson(@NotNull String token, @NotNull ClientConnectio
426509
}
427510

428511
/**
429-
* Creates an AuthenticationInfo object from the validated token.
512+
* Creates an AuthenticationInfo object from the validated token (used for cached tokens).
430513
*
431514
* @param subject the subject from the token
432515
* @param claimsSet the JWT claims set
433516
* @param token the raw token string
434517
* @return the AuthenticationInfo object
435518
*/
436-
private @NotNull AuthenticationInfo createAuthenticationInfo(
519+
@NotNull
520+
private AuthenticationInfo createAuthenticationInfo(
437521
@NotNull String subject, @NotNull JWTClaimsSet claimsSet, @NotNull String token) {
438522
AuthenticationInfo authInfo = new AuthenticationInfo(AUTH_TYPE, subject);
439523

@@ -517,7 +601,7 @@ public boolean requestCredentials(@NotNull HttpServletRequest request, @NotNull
517601
}
518602

519603
@Override
520-
public void dropCredentials(HttpServletRequest request, HttpServletResponse response) {
604+
public void dropCredentials(@Nullable HttpServletRequest request, @Nullable HttpServletResponse response) {
521605
// For bearer tokens, we don't need to do anything special on logout
522606
// The client should discard the token
523607
logger.debug("dropCredentials called");
@@ -527,12 +611,16 @@ public void dropCredentials(HttpServletRequest request, HttpServletResponse resp
527611
* Internal class to represent a cached token with expiration.
528612
*/
529613
private static class CachedToken {
614+
@NotNull
530615
final String subject;
616+
617+
@NotNull
531618
final JWTClaimsSet claimsSet;
619+
532620
final long cachedAt;
533621
final long ttlMillis;
534622

535-
CachedToken(String subject, JWTClaimsSet claimsSet, long ttlSeconds) {
623+
CachedToken(@NotNull String subject, @NotNull JWTClaimsSet claimsSet, long ttlSeconds) {
536624
this.subject = subject;
537625
this.claimsSet = claimsSet;
538626
this.cachedAt = System.currentTimeMillis();

0 commit comments

Comments
 (0)