4343import org .apache .sling .auth .oauth_client .spi .UserInfoProcessor ;
4444import org .apache .sling .jcr .resource .api .JcrResourceConstants ;
4545import org .jetbrains .annotations .NotNull ;
46+ import org .jetbrains .annotations .Nullable ;
4647import org .osgi .framework .BundleContext ;
4748import org .osgi .service .component .annotations .Activate ;
4849import org .osgi .service .component .annotations .Component ;
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