1919
2020#include " iceberg/catalog/rest/auth/auth_manager.h"
2121
22+ #include < array>
23+ #include < chrono>
2224#include < optional>
25+ #include < string_view>
26+ #include < utility>
2327
2428#include " iceberg/catalog/rest/auth/auth_manager_internal.h"
2529#include " iceberg/catalog/rest/auth/auth_properties.h"
2630#include " iceberg/catalog/rest/auth/auth_session.h"
31+ #include " iceberg/catalog/rest/auth/auth_session_internal.h"
2732#include " iceberg/catalog/rest/auth/oauth2_util.h"
33+ #include " iceberg/catalog/session_context.h"
2834#include " iceberg/util/base64.h"
2935#include " iceberg/util/macros.h"
3036
3137namespace iceberg ::rest::auth {
3238
39+ namespace {
40+
41+ constexpr std::string_view kAuthorizationHeader = " Authorization" ;
42+
43+ const std::array<std::string_view, 5 > kTokenPreferenceOrder = {
44+ AuthProperties::kIdTokenType , AuthProperties::kAccessTokenType ,
45+ AuthProperties::kJwtTokenType , AuthProperties::kSaml2TokenType ,
46+ AuthProperties::kSaml1TokenType ,
47+ };
48+
49+ std::optional<std::pair<std::string, std::string>> FindPreferredTypedToken (
50+ const std::unordered_map<std::string, std::string>& credentials) {
51+ for (std::string_view token_type : kTokenPreferenceOrder ) {
52+ auto token_it = credentials.find (std::string (token_type));
53+ if (token_it != credentials.end ()) {
54+ return std::pair{token_it->first , token_it->second };
55+ }
56+ }
57+ return std::nullopt ;
58+ }
59+
60+ std::unordered_map<std::string, std::string> FilterTableSessionProperties (
61+ const std::unordered_map<std::string, std::string>& properties) {
62+ std::unordered_map<std::string, std::string> filtered;
63+ if (auto token_it = properties.find (AuthProperties::kToken .key ());
64+ token_it != properties.end ()) {
65+ filtered.emplace (token_it->first , token_it->second );
66+ }
67+ for (std::string_view token_type : kTokenPreferenceOrder ) {
68+ auto token_it = properties.find (std::string (token_type));
69+ if (token_it != properties.end ()) {
70+ filtered.emplace (token_it->first , token_it->second );
71+ }
72+ }
73+ return filtered;
74+ }
75+
76+ } // namespace
77+
3378Result<std::shared_ptr<AuthSession>> AuthManager::InitSession (
34- HttpClient& init_client,
79+ std::shared_ptr< HttpClient> init_client,
3580 const std::unordered_map<std::string, std::string>& properties) {
3681 // By default, use the catalog session for initialization
37- return CatalogSession (init_client, properties);
82+ return CatalogSession (std::move ( init_client) , properties);
3883}
3984
4085Result<std::shared_ptr<AuthSession>> AuthManager::ContextualSession (
@@ -55,7 +100,7 @@ Result<std::shared_ptr<AuthSession>> AuthManager::TableSession(
55100class NoopAuthManager : public AuthManager {
56101 public:
57102 Result<std::shared_ptr<AuthSession>> CatalogSession (
58- [[maybe_unused]] HttpClient& client,
103+ [[maybe_unused]] std::shared_ptr< HttpClient> client,
59104 [[maybe_unused]] const std::unordered_map<std::string, std::string>& properties)
60105 override {
61106 return AuthSession::MakeDefault ({});
@@ -72,7 +117,7 @@ Result<std::unique_ptr<AuthManager>> MakeNoopAuthManager(
72117class BasicAuthManager : public AuthManager {
73118 public:
74119 Result<std::shared_ptr<AuthSession>> CatalogSession (
75- [[maybe_unused]] HttpClient& client,
120+ [[maybe_unused]] std::shared_ptr< HttpClient> client,
76121 const std::unordered_map<std::string, std::string>& properties) override {
77122 auto username_it = properties.find (AuthProperties::kBasicUsername );
78123 ICEBERG_PRECHECK (username_it != properties.end () && !username_it->second .empty (),
@@ -96,67 +141,184 @@ Result<std::unique_ptr<AuthManager>> MakeBasicAuthManager(
96141class OAuth2Manager : public AuthManager {
97142 public:
98143 Result<std::shared_ptr<AuthSession>> InitSession (
99- HttpClient& init_client,
144+ std::shared_ptr< HttpClient> init_client,
100145 const std::unordered_map<std::string, std::string>& properties) override {
146+ ICEBERG_PRECHECK (init_client != nullptr ,
147+ " OAuth2 initialization HTTP client must not be null" );
101148 ICEBERG_ASSIGN_OR_RAISE (auto config, AuthProperties::FromProperties (properties));
102149 // No token refresh during init (short-lived session).
103150 config.Set (AuthProperties::kKeepRefreshed , false );
104151
105152 // Credential takes priority: fetch a fresh token for the config request.
106153 if (!config.credential ().empty ()) {
107- auto init_session = AuthSession::MakeDefault (AuthHeaders (config.token ()));
108- ICEBERG_ASSIGN_OR_RAISE (init_token_response_,
109- FetchToken (init_client, *init_session, config));
110- return AuthSession::MakeDefault (AuthHeaders (init_token_response_->access_token ));
154+ auto init_session =
155+ AuthSession::MakeDefault (OAuth2Util::AuthHeaders (config.token ()));
156+ start_time_ = std::chrono::steady_clock::now ();
157+ ICEBERG_ASSIGN_OR_RAISE (
158+ auth_response_, OAuth2Util::FetchToken (*init_client, *init_session, config));
159+ // TODO(lishuxu): Match Java OAuth2Util.AuthSession.fromTokenResponse here.
160+ return AuthSession::MakeDefault (
161+ OAuth2Util::AuthHeaders (auth_response_->access_token ));
111162 }
112163
113164 if (!config.token ().empty ()) {
114- return AuthSession::MakeDefault (AuthHeaders (config.token ()));
165+ // TODO(lishuxu): Match Java OAuth2Util.AuthSession.fromAccessToken here.
166+ return AuthSession::MakeDefault (OAuth2Util::AuthHeaders (config.token ()));
115167 }
116168
117169 return AuthSession::MakeDefault ({});
118170 }
119171
120172 Result<std::shared_ptr<AuthSession>> CatalogSession (
121- HttpClient& client ,
173+ std::shared_ptr< HttpClient> shared_client ,
122174 const std::unordered_map<std::string, std::string>& properties) override {
123175 ICEBERG_ASSIGN_OR_RAISE (auto config, AuthProperties::FromProperties (properties));
124-
125- // Reuse token from init phase.
126- if (init_token_response_. has_value ()) {
127- auto token_response = std::move (*init_token_response_);
128- init_token_response_. reset ();
129- return AuthSession::MakeOAuth2 (token_response, config. oauth2_server_uri (),
130- config.client_id (), config.client_secret (),
131- config.scope (), config.keep_refreshed (),
132- config.optional_oauth_params (), client );
176+ ICEBERG_PRECHECK (shared_client != nullptr ,
177+ " OAuth2 catalog session HTTP client must not be null " );
178+ refresh_client_ = std::move (shared_client);
179+ // Reuse the token response and start time from the init phase.
180+ if (auth_response_. has_value ()) {
181+ return internal::MakeOAuth2Session (
182+ *auth_response_, config.oauth2_server_uri (), config.client_id (),
183+ config. client_secret (), config.scope (), config.keep_refreshed (),
184+ config.optional_oauth_params (), refresh_client_, start_time_ );
133185 }
134186
135- // If token is provided, use it directly.
187+ // TODO(lishuxu): Honor token-refresh-enabled for catalog bearer tokens, matching
188+ // Java. If token is provided, use it directly.
136189 if (!config.token ().empty ()) {
137- return AuthSession::MakeDefault (AuthHeaders (config.token ()));
190+ OAuthTokenResponse token_response{
191+ .access_token = config.token (),
192+ .token_type = " bearer" ,
193+ .issued_token_type = AuthProperties::kAccessTokenType ,
194+ };
195+ return AuthSession::MakeOAuth2 (token_response, config.oauth2_server_uri (),
196+ config.client_id (), config.client_secret (),
197+ config.scope (), /* keep_refreshed=*/ false ,
198+ config.optional_oauth_params (), refresh_client_);
138199 }
139200
140201 // Fetch a new token using client_credentials grant.
141202 if (!config.credential ().empty ()) {
142- auto base_session = AuthSession::MakeDefault (AuthHeaders (config.token ()));
203+ auto base_session =
204+ AuthSession::MakeDefault (OAuth2Util::AuthHeaders (config.token ()));
143205 OAuthTokenResponse token_response;
144- ICEBERG_ASSIGN_OR_RAISE (token_response, FetchToken (client, *base_session, config));
206+ ICEBERG_ASSIGN_OR_RAISE (
207+ token_response,
208+ OAuth2Util::FetchToken (*refresh_client_, *base_session, config));
145209 return AuthSession::MakeOAuth2 (token_response, config.oauth2_server_uri (),
146210 config.client_id (), config.client_secret (),
147211 config.scope (), config.keep_refreshed (),
148- config.optional_oauth_params (), client );
212+ config.optional_oauth_params (), refresh_client_ );
149213 }
150214
151- return AuthSession::MakeDefault ({} );
215+ return MakeSession ( AccessTokenResponse ( " " ), config, /* keep_refreshed= */ false );
152216 }
153217
154- // TODO(lishuxu): Override TableSession() for token exchange (RFC 8693).
155- // TODO(lishuxu): Override ContextualSession() for per-context exchange.
218+ Result<std::shared_ptr<AuthSession>> ContextualSession (
219+ const SessionContext& context, std::shared_ptr<AuthSession> parent) override {
220+ // TODO(lishuxu): Add child-session caching and refresh, matching Java
221+ // AuthSessionCache.
222+ return MaybeCreateChildSession (context.credentials , /* allow_credential=*/ true ,
223+ std::move (parent));
224+ }
225+
226+ Result<std::shared_ptr<AuthSession>> TableSession (
227+ [[maybe_unused]] const TableIdentifier& table,
228+ const std::unordered_map<std::string, std::string>& properties,
229+ std::shared_ptr<AuthSession> parent) override {
230+ return MaybeCreateChildSession (FilterTableSessionProperties (properties),
231+ /* allow_credential=*/ false , std::move (parent));
232+ }
233+
234+ Status Close () override {
235+ refresh_client_.reset ();
236+ return {};
237+ }
156238
157239 private:
158- // / Cached token from InitSession
159- std::optional<OAuthTokenResponse> init_token_response_;
240+ static OAuthTokenResponse AccessTokenResponse (std::string token) {
241+ return {
242+ .access_token = std::move (token),
243+ .token_type = " bearer" ,
244+ .issued_token_type = AuthProperties::kAccessTokenType ,
245+ };
246+ }
247+
248+ static Result<AuthProperties> ChildConfig (const OAuth2SessionInfo& parent_info,
249+ const std::string& credential) {
250+ auto properties = parent_info.optional_oauth_params ;
251+ properties[AuthProperties::kCredential .key ()] = credential;
252+ properties[AuthProperties::kScope .key ()] = parent_info.scope ;
253+ properties[AuthProperties::kOAuth2ServerUri .key ()] = parent_info.oauth2_server_uri ;
254+ return AuthProperties::FromProperties (properties);
255+ }
256+
257+ Result<std::shared_ptr<AuthSession>> MakeSession (
258+ const OAuthTokenResponse& token_response, const AuthProperties& config,
259+ bool keep_refreshed) const {
260+ ICEBERG_PRECHECK (refresh_client_ != nullptr ,
261+ " OAuth2 catalog session must be initialized before child sessions" );
262+ return AuthSession::MakeOAuth2 (token_response, config.oauth2_server_uri (),
263+ config.client_id (), config.client_secret (),
264+ config.scope (), keep_refreshed,
265+ config.optional_oauth_params (), refresh_client_);
266+ }
267+
268+ Result<std::shared_ptr<AuthSession>> MaybeCreateChildSession (
269+ const std::unordered_map<std::string, std::string>& credentials,
270+ bool allow_credential, std::shared_ptr<AuthSession> parent) {
271+ auto token_it = credentials.find (AuthProperties::kToken .key ());
272+ auto credential_it = credentials.find (AuthProperties::kCredential .key ());
273+ auto typed_token = FindPreferredTypedToken (credentials);
274+ if (token_it == credentials.end () &&
275+ (!allow_credential || credential_it == credentials.end ()) &&
276+ !typed_token.has_value ()) {
277+ return parent;
278+ }
279+
280+ ICEBERG_PRECHECK (refresh_client_ != nullptr ,
281+ " OAuth2 catalog session must be initialized before child sessions" );
282+ auto parent_info = parent->OAuth2Info ();
283+ ICEBERG_PRECHECK (parent_info.has_value (),
284+ " OAuth2 child session requires OAuth2 parent metadata" );
285+
286+ if (token_it != credentials.end ()) {
287+ ICEBERG_ASSIGN_OR_RAISE (auto config,
288+ ChildConfig (*parent_info, parent_info->credential ));
289+ return MakeSession (AccessTokenResponse (token_it->second ), config,
290+ /* keep_refreshed=*/ false );
291+ }
292+
293+ if (allow_credential && credential_it != credentials.end ()) {
294+ 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 );
299+ }
300+
301+ std::optional<std::string> actor_token;
302+ 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 ;
306+ }
307+ ICEBERG_ASSIGN_OR_RAISE (
308+ auto response,
309+ OAuth2Util::ExchangeToken (*refresh_client_, *parent, {}, typed_token->second ,
310+ typed_token->first , actor_token, actor_token_type,
311+ parent_info->scope , parent_info->oauth2_server_uri ,
312+ parent_info->optional_oauth_params ));
313+ ICEBERG_ASSIGN_OR_RAISE (auto config,
314+ ChildConfig (*parent_info, parent_info->credential ));
315+ return MakeSession (response, config, /* keep_refreshed=*/ false );
316+ }
317+
318+ // / Token response and start time captured by InitSession.
319+ std::optional<OAuthTokenResponse> auth_response_;
320+ std::optional<std::chrono::steady_clock::time_point> start_time_;
321+ std::shared_ptr<HttpClient> refresh_client_;
160322};
161323
162324Result<std::unique_ptr<AuthManager>> MakeOAuth2Manager (
0 commit comments