2828#include " iceberg/catalog/rest/auth/auth_manager_internal.h"
2929#include " iceberg/catalog/rest/auth/auth_properties.h"
3030#include " iceberg/catalog/rest/auth/auth_session.h"
31+ #include " iceberg/catalog/rest/auth/auth_session_cache_internal.h"
3132#include " iceberg/catalog/rest/auth/auth_session_internal.h"
3233#include " iceberg/catalog/rest/auth/oauth2_util.h"
34+ #include " iceberg/catalog/rest/auth/token_refresh_scheduler.h"
3335#include " iceberg/catalog/session_context.h"
3436#include " iceberg/util/base64.h"
3537#include " iceberg/util/macros.h"
@@ -156,13 +158,13 @@ class OAuth2Manager : public AuthManager {
156158 start_time_ = std::chrono::steady_clock::now ();
157159 ICEBERG_ASSIGN_OR_RAISE (
158160 auth_response_, OAuth2Util::FetchToken (*init_client, *init_session, config));
159- // TODO(lishuxu): Match Java OAuth2Util.AuthSession.fromTokenResponse here .
161+ // TODO(lishuxu): Build the initialization session from the full token response .
160162 return AuthSession::MakeDefault (
161163 OAuth2Util::AuthHeaders (auth_response_->access_token ));
162164 }
163165
164166 if (!config.token ().empty ()) {
165- // TODO(lishuxu): Match Java OAuth2Util.AuthSession.fromAccessToken here .
167+ // TODO(lishuxu): Preserve configured access-token expiration in the init session .
166168 return AuthSession::MakeDefault (OAuth2Util::AuthHeaders (config.token ()));
167169 }
168170
@@ -176,6 +178,28 @@ class OAuth2Manager : public AuthManager {
176178 ICEBERG_PRECHECK (shared_client != nullptr ,
177179 " OAuth2 catalog session HTTP client must not be null" );
178180 refresh_client_ = std::move (shared_client);
181+
182+ // Initialize catalog-level configuration
183+ keep_refreshed_ = config.keep_refreshed ();
184+ exchange_enabled_ = config.exchange_enabled ();
185+ session_timeout_ =
186+ std::chrono::milliseconds (config.Get (AuthProperties::kSessionTimeoutMs ));
187+
188+ // Create session cache
189+ ICEBERG_ASSIGN_OR_RAISE (
190+ session_cache_,
191+ internal::AuthSessionCache::Make (
192+ session_timeout_, [](std::shared_ptr<AuthSession> session) {
193+ if (auto oauth2 =
194+ std::dynamic_pointer_cast<internal::OAuth2Session>(session)) {
195+ oauth2->StopRefreshing ();
196+ }
197+ }));
198+
199+ // Start periodic sweep to reclaim idle sessions
200+ auto sweep_interval = std::min (session_timeout_, std::chrono::milliseconds (60000 ));
201+ session_cache_->StartPeriodicSweep (TokenRefreshScheduler::Instance (), sweep_interval);
202+
179203 // Reuse the token response and start time from the init phase.
180204 if (auth_response_.has_value ()) {
181205 return internal::MakeOAuth2Session (
@@ -184,8 +208,8 @@ class OAuth2Manager : public AuthManager {
184208 config.optional_oauth_params (), refresh_client_, start_time_);
185209 }
186210
187- // TODO(lishuxu): Honor token-refresh-enabled for catalog bearer tokens, matching
188- // Java. If token is provided, use it directly.
211+ // TODO(lishuxu): Honor token-refresh-enabled for catalog bearer tokens.
212+ // If token is provided, use it directly.
189213 if (!config.token ().empty ()) {
190214 OAuthTokenResponse token_response{
191215 .access_token = config.token (),
@@ -217,21 +241,31 @@ class OAuth2Manager : public AuthManager {
217241
218242 Result<std::shared_ptr<AuthSession>> ContextualSession (
219243 const SessionContext& context, std::shared_ptr<AuthSession> parent) override {
220- // TODO(lishuxu): Add child-session caching and refresh, matching Java
221- // AuthSessionCache.
244+ // Use session_id as cache key for contextual sessions
245+ std::string cache_key = context. session_id . empty () ? " " : " ctx: " + context. session_id ;
222246 return MaybeCreateChildSession (context.credentials , /* allow_credential=*/ true ,
223- std::move (parent));
247+ std::move (parent), cache_key );
224248 }
225249
226250 Result<std::shared_ptr<AuthSession>> TableSession (
227251 [[maybe_unused]] const TableIdentifier& table,
228252 const std::unordered_map<std::string, std::string>& properties,
229253 std::shared_ptr<AuthSession> parent) override {
254+ // Use token value as cache key for table sessions
255+ auto token_it = properties.find (AuthProperties::kToken .key ());
256+ std::string cache_key;
257+ if (token_it != properties.end () && !token_it->second .empty ()) {
258+ cache_key = " tbl:" + token_it->second ;
259+ }
230260 return MaybeCreateChildSession (FilterTableSessionProperties (properties),
231- /* allow_credential=*/ false , std::move (parent));
261+ /* allow_credential=*/ false , std::move (parent),
262+ cache_key);
232263 }
233264
234265 Status Close () override {
266+ if (session_cache_) {
267+ session_cache_->Close ();
268+ }
235269 refresh_client_.reset ();
236270 return {};
237271 }
@@ -267,7 +301,8 @@ class OAuth2Manager : public AuthManager {
267301
268302 Result<std::shared_ptr<AuthSession>> MaybeCreateChildSession (
269303 const std::unordered_map<std::string, std::string>& credentials,
270- bool allow_credential, std::shared_ptr<AuthSession> parent) {
304+ bool allow_credential, std::shared_ptr<AuthSession> parent,
305+ const std::string& cache_key) {
271306 auto token_it = credentials.find (AuthProperties::kToken .key ());
272307 auto credential_it = credentials.find (AuthProperties::kCredential .key ());
273308 auto typed_token = FindPreferredTypedToken (credentials);
@@ -283,42 +318,78 @@ class OAuth2Manager : public AuthManager {
283318 ICEBERG_PRECHECK (parent_info.has_value (),
284319 " OAuth2 child session requires OAuth2 parent metadata" );
285320
321+ // Use cache if we have a valid cache_key
322+ if (!cache_key.empty () && session_cache_) {
323+ return session_cache_->Get (cache_key,
324+ [&]() -> Result<std::shared_ptr<AuthSession>> {
325+ return CreateChildSessionUncached (
326+ credentials, allow_credential, *parent_info);
327+ });
328+ }
329+
330+ // No cache, create session directly
331+ return CreateChildSessionUncached (credentials, allow_credential, *parent_info);
332+ }
333+
334+ Result<std::shared_ptr<AuthSession>> CreateChildSessionUncached (
335+ const std::unordered_map<std::string, std::string>& credentials,
336+ bool allow_credential, const OAuth2SessionInfo& parent_info) {
337+ auto token_it = credentials.find (AuthProperties::kToken .key ());
338+ auto credential_it = credentials.find (AuthProperties::kCredential .key ());
339+ auto typed_token = FindPreferredTypedToken (credentials);
340+
286341 if (token_it != credentials.end ()) {
287- ICEBERG_ASSIGN_OR_RAISE (auto config,
288- ChildConfig (*parent_info, parent_info->credential ));
342+ // Token child session: don't include parent credential in config.
343+ // Token-based sessions are not refreshed; when token exchange is enabled,
344+ // refresh will use the token itself rather than the parent credential.
345+ auto properties = parent_info.optional_oauth_params ;
346+ properties[AuthProperties::kScope .key ()] = parent_info.scope ;
347+ properties[AuthProperties::kOAuth2ServerUri .key ()] = parent_info.oauth2_server_uri ;
348+ ICEBERG_ASSIGN_OR_RAISE (auto config, AuthProperties::FromProperties (properties));
289349 return MakeSession (AccessTokenResponse (token_it->second ), config,
290350 /* keep_refreshed=*/ false );
291351 }
292352
293353 if (allow_credential && credential_it != credentials.end ()) {
294354 ICEBERG_ASSIGN_OR_RAISE (auto config,
295- ChildConfig (*parent_info, credential_it->second ));
296- ICEBERG_ASSIGN_OR_RAISE (auto response,
297- OAuth2Util::FetchToken (*refresh_client_, *parent, config));
298- return MakeSession (response, config, /* keep_refreshed=*/ false );
355+ ChildConfig (parent_info, credential_it->second ));
356+ // Use parent session headers for FetchToken authentication
357+ auto temp_parent = AuthSession::MakeDefault (parent_info.headers );
358+ ICEBERG_ASSIGN_OR_RAISE (
359+ auto response, OAuth2Util::FetchToken (*refresh_client_, *temp_parent, config));
360+ // Credential child sessions use keep_refreshed from catalog level
361+ return MakeSession (response, config, keep_refreshed_);
299362 }
300363
301364 std::optional<std::string> actor_token;
302365 std::optional<std::string> actor_token_type;
303- if (!parent_info-> token .empty ()) {
304- actor_token = parent_info-> token ;
305- actor_token_type = parent_info-> issued_token_type ;
366+ if (!parent_info. token .empty ()) {
367+ actor_token = parent_info. token ;
368+ actor_token_type = parent_info. issued_token_type ;
306369 }
370+ // Use parent session headers for ExchangeToken authentication
371+ auto temp_parent = AuthSession::MakeDefault (parent_info.headers );
307372 ICEBERG_ASSIGN_OR_RAISE (
308373 auto response,
309- OAuth2Util::ExchangeToken (*refresh_client_, *parent , {}, typed_token->second ,
374+ OAuth2Util::ExchangeToken (*refresh_client_, *temp_parent , {}, typed_token->second ,
310375 typed_token->first , actor_token, actor_token_type,
311- parent_info-> scope , parent_info-> oauth2_server_uri ,
312- parent_info-> optional_oauth_params ));
376+ parent_info. scope , parent_info. oauth2_server_uri ,
377+ parent_info. optional_oauth_params ));
313378 ICEBERG_ASSIGN_OR_RAISE (auto config,
314- ChildConfig (* parent_info, parent_info-> credential ));
379+ ChildConfig (parent_info, parent_info. credential ));
315380 return MakeSession (response, config, /* keep_refreshed=*/ false );
316381 }
317382
318383 // / Token response and start time captured by InitSession.
319384 std::optional<OAuthTokenResponse> auth_response_;
320385 std::optional<std::chrono::steady_clock::time_point> start_time_;
321386 std::shared_ptr<HttpClient> refresh_client_;
387+
388+ // Catalog-level configuration initialized in CatalogSession
389+ bool keep_refreshed_ = true ;
390+ bool exchange_enabled_ = true ;
391+ std::chrono::milliseconds session_timeout_{3'600'000 };
392+ std::shared_ptr<internal::AuthSessionCache> session_cache_;
322393};
323394
324395Result<std::unique_ptr<AuthManager>> MakeOAuth2Manager (
0 commit comments