Stroika Library 3.0d23
 
Loading...
Searching...
No Matches
Client.cpp
1/*
2 * Copyright(c) Sophist Solutions, Inc. 1990-2026. All rights reserved
3 */
4#include "Stroika/Frameworks/StroikaPreComp.h"
5
7#include "Stroika/Foundation/Containers/Association.h"
14#include "Stroika/Foundation/IO/Network/Transfer/Connection.h"
17
18#include "Client.h"
19
20using namespace Stroika::Foundation;
24using namespace Stroika::Foundation::Execution;
26
27using namespace Stroika::Frameworks;
28using namespace Stroika::Frameworks::Auth::OAuth;
29
30using Memory::BLOB;
31
32// Comment this in to turn on aggressive noisy DbgTrace in this module
33// #define USE_NOISY_TRACE_IN_THIS_MODULE_ 1
34
35/*
36 ********************************************************************************
37 ************************* Auth::OAuth::TokenRequest ****************************
38 ********************************************************************************
39 */
40String TokenRequest::ToString () const
41{
43 sb << "{"sv;
44 sb << "client_id: "sv << client_id;
45 if (code) {
46 sb << ", code: "sv << code;
47 sb << ", grant_type: authorization_code"sv;
48 }
49 if (refresh_token) {
50 sb << ", refresh_token: "sv << refresh_token;
51 sb << ", grant_type: refresh_token"sv;
52 }
53 if (client_secret) {
54 sb << ", client_secret: "sv << client_secret;
55 }
56 if (redirect_uri) {
57 sb << ", redirect_uri: "sv << redirect_uri;
58 }
59 if (code_verifier) {
60 sb << ", code_verifier: "sv << code_verifier;
61 }
62 sb << "}"sv;
63 return sb;
64}
65
66const ObjectVariantMapper TokenRequest::kMapper = [] () {
68 mapper.AddCommonType<String> ();
69 mapper.AddCommonType<optional<String>> ();
70 mapper.AddCommonType<optional<URI>> ();
71 mapper.AddClass<TokenRequest> ({
72 {"client_id"sv, &TokenRequest::client_id},
73 {"code"sv, &TokenRequest::code},
74 {"refresh_token"sv, &TokenRequest::refresh_token},
75 {"client_secret"sv, &TokenRequest::client_secret},
76 {"redirect_uri"sv, &TokenRequest::redirect_uri},
77 {"code_verifier"sv, &TokenRequest::code_verifier},
78 });
79 return mapper;
80}();
81
82TypedBLOB TokenRequest::ToWireFormat () const
83{
84 if (not code and not refresh_token) {
85 static const auto kExcept_ = RuntimeErrorException{"Missing authorization code/refresh_token"sv};
86 Throw (kExcept_);
87 }
88 if (code and refresh_token) {
89 static const auto kExcept_ = RuntimeErrorException{"Cannot combine authorization code/refresh_token"sv};
90 Throw (kExcept_);
91 }
92 if (client_id.empty ()) {
93 static const auto kExcept_ = RuntimeErrorException{"Missing client_id"sv};
94 Throw (kExcept_);
95 }
96 BLOB reqBody = [&] () {
98 params.Add ({"client_id"sv, client_id});
99 if (code) {
100 params.Add ({"code"sv, *code});
101 params.Add ({"grant_type"sv, "authorization_code"sv});
102 }
103 else {
104 params.Add ({"refresh_token"sv, *refresh_token});
105 params.Add ({"grant_type"sv, "refresh_token"sv});
106 }
107 if (client_secret) {
108 params.Add ({"client_secret"sv, *client_secret});
109 }
110 if (redirect_uri) {
111 params.Add ({"redirect_uri"sv, redirect_uri->As<String> ()});
112 }
113 if (code_verifier) {
114 params.Add ({"code_verifier"sv, *code_verifier});
115 }
116 return Variant::FormURLEncoded::Writer{}.WriteAsBLOB (params);
117 }();
118 return TypedBLOB{reqBody, InternetMediaTypes::kWWWFormURLEncoded};
119}
120
121TokenRequest TokenRequest::FromWireFormat (const TypedBLOB& src)
122{
123 if (not src.fType or not InternetMediaTypeRegistry::sThe->IsA (InternetMediaTypes::kWWWFormURLEncoded, *src.fType)) {
124 static const auto kExcept_ = RuntimeErrorException{"Expected {}"_f(InternetMediaTypes::kWWWFormURLEncoded)};
125 Throw (kExcept_);
126 }
128 static const auto kExcept_clientid_ = RuntimeErrorException{"Missing client_id"sv};
129 static const auto kExcept_authCode_ = RuntimeErrorException{"Missing authentication code"sv};
130 static const auto kExcept_grant_type_ = RuntimeErrorException{"Missing grant_type"sv};
131 auto code = params.LookupOne ("code"sv);
132 auto refresh_token = params.LookupOne ("refresh_token"sv);
133 if (not code and not refresh_token) {
134 static const auto kExcept_ = RuntimeErrorException{"Missing authorization code/refresh_token"sv};
135 Throw (kExcept_);
136 }
137 if (code and refresh_token) {
138 static const auto kExcept_ = RuntimeErrorException{"Cannot combine authorization code/refresh_token"sv};
139 Throw (kExcept_);
140 }
141 return TokenRequest{.client_id = params.LookupOneChecked ("client_id"sv, kExcept_clientid_),
142 .code = code,
143 .refresh_token = refresh_token,
144 .client_secret = params.LookupOne ("client_secret"sv),
145 .redirect_uri = params.LookupOne ("redirect_uri"sv)};
146}
147
148/*
149 ********************************************************************************
150 ************************* Auth::OAuth::TokenResponse ***************************
151 ********************************************************************************
152 */
153String TokenResponse::ToString () const
154{
155 StringBuilder sb;
156 sb << "{"sv;
157 sb << "access_token: "sv << access_token;
158 sb << ", expires_at: "sv << expires_at;
159 sb << ", scope: "sv << scope;
160 if (refresh_token) {
161 sb << ", refresh_token: "sv << refresh_token;
162 }
163 if (id_token) {
164 sb << ", id_token: "sv << id_token;
165 }
166 if (token_type) {
167 sb << ", token_type: "sv << token_type;
168 }
169 sb << "}"sv;
170 return sb;
171}
172
173const ObjectVariantMapper TokenResponse::kMapper = [] () {
174 ObjectVariantMapper mapper;
175 using TypeMappingDetails = ObjectVariantMapper::TypeMappingDetails;
176 mapper.AddCommonType<String> ();
177 mapper.AddCommonType<optional<String>> ();
178 mapper.AddCommonType<DateTime> ();
179 mapper.AddCommonType<Set<String>> ();
180 mapper.AddClass<TokenResponse> ({
181 {"access_token"sv, &TokenResponse::access_token},
182 // expires_at in wire-format is expires_in seconds into future
183 {"expires_in"sv, &TokenResponse::expires_at,
185 [] ([[maybe_unused]] const ObjectVariantMapper& mapper, const DateTime* objOfType) -> VariantValue {
186 return VariantValue{(objOfType->AsUTC () - DateTime::NowUTC ()).As<int> ()};
187 }),
189 [] ([[maybe_unused]] const ObjectVariantMapper& mapper, const VariantValue& d, DateTime* into) -> void {
190 *into = DateTime::NowUTC ().AddSeconds (d.As<int> ());
191 })}},
192 // scope in wire-format is space separated
193 {"scope"sv, &TokenResponse::scope,
195 [] ([[maybe_unused]] const ObjectVariantMapper& mapper, const Set<String>* objOfType) -> VariantValue {
196 return objOfType->Join (" "sv);
197 }),
199 [] ([[maybe_unused]] const ObjectVariantMapper& mapper, const VariantValue& d, Set<String>* into) -> void {
200 *into = Set<String>{d.As<String> ().Tokenize ()};
201 })}},
202 {"refresh_token"sv, &TokenResponse::refresh_token},
203 {"id_token"sv, &TokenResponse::id_token},
204 {"token_type"sv, &TokenResponse::token_type},
205 });
206 return mapper;
207}();
208
209TypedBLOB TokenResponse::ToWireFormat () const
210{
211 return TypedBLOB{Variant::JSON::Writer{}.WriteAsBLOB (kMapper.FromObject (*this)), InternetMediaTypes::kJSON};
212}
213
214TokenResponse TokenResponse::FromWireFormat (const TypedBLOB& src)
215{
216 if (not src.fType or not InternetMediaTypeRegistry::sThe->IsA (InternetMediaTypes::kJSON, *src.fType)) {
217 static const auto kExcept_ = RuntimeErrorException{"Expected JSON"sv};
218 Throw (kExcept_);
219 }
220 return kMapper.ToObject<TokenResponse> (Variant::JSON::Reader{}.Read (src.fData));
221}
222
223/*
224 ********************************************************************************
225 ********************* Auth::OAuth::TokenRevocationRequest **********************
226 ********************************************************************************
227 */
228String TokenRevocationRequest::ToString () const
229{
230 StringBuilder sb;
231 sb << "{"sv;
232 sb << "access_token: "sv << access_token;
233 if (refresh_token) {
234 sb << ", refresh_token: "sv << *refresh_token;
235 }
236 if (client_id) {
237 sb << ", client_id: "sv << *client_id;
238 }
239 if (client_secret) {
240 sb << ", client_secret: "sv << *client_secret;
241 }
242 sb << "}"sv;
243 return sb;
244}
245
246const ObjectVariantMapper TokenRevocationRequest::kMapper = [] () {
247 ObjectVariantMapper mapper;
248 mapper.AddCommonType<String> ();
249 mapper.AddCommonType<optional<String>> ();
250 mapper.AddClass<TokenRevocationRequest> ({
251 {"access_token"sv, &TokenRevocationRequest::access_token},
252 {"refresh_token"sv, &TokenRevocationRequest::refresh_token},
253 {"client_id"sv, &TokenRevocationRequest::client_id},
254 {"client_secret"sv, &TokenRevocationRequest::client_secret},
255 });
256 return mapper;
257}();
258
259TypedBLOB TokenRevocationRequest::ToWireFormat () const
260{
261 if (access_token.empty ()) {
262 static const auto kExcept_ = RuntimeErrorException{"Missing access_token"sv};
263 Throw (kExcept_);
264 }
265 BLOB reqBody = [&] () {
267 if (refresh_token) {
268 params.Add ({"token_type_hint"sv, "refresh_token"sv});
269 params.Add ({"token"sv, *refresh_token});
270 }
271 else {
272 params.Add ({"token_type_hint"sv, "access_token"sv});
273 params.Add ({"token"sv, access_token});
274 }
275 if (client_id) {
276 params.Add ({"client_id"sv, *client_id});
277 }
278 if (client_secret) {
279 params.Add ({"client_secret"sv, *client_secret});
280 }
281 return Variant::FormURLEncoded::Writer{}.WriteAsBLOB (params);
282 }();
283 return TypedBLOB{reqBody, InternetMediaTypes::kWWWFormURLEncoded};
284}
285
286/*
287 ********************************************************************************
288 ******************** Auth::OAuth::TokenIntrospectionResponse *******************
289 ********************************************************************************
290 */
291String TokenIntrospectionResponse::ToString () const
292{
293 StringBuilder sb;
294 sb << "{"sv;
295 sb << ", expires_at: "sv << expires_at;
296 sb << "}"sv;
297 return sb;
298}
299
300const ObjectVariantMapper TokenIntrospectionResponse::kMapper = [] () {
301 ObjectVariantMapper mapper;
302 using TypeMappingDetails = ObjectVariantMapper::TypeMappingDetails;
303 mapper.AddCommonType<String> ();
304 mapper.AddCommonType<optional<String>> ();
305 mapper.AddCommonType<DateTime> ();
306 mapper.AddCommonType<Set<String>> ();
307#if 0
308 Google TokenInfo Endpoint
309 +You can use this endpoint to "introspect" an access token by sending a GET request:
310 +Endpoint: https://oauth2.googleapis.com/tokeninfo
311 +Parameter: access_token
312 +Example Request
313 +http GET https://oauth2.googleapis.com
314 +
315 +Expected JSON Response
316 +If the token is valid, Google returns metadata including the expiration time:
317 +json
318 +{
319 + "azp": "123456789-example.apps.googleusercontent.com",
320 + "aud": "123456789-example.apps.googleusercontent.com",
321 + "sub": "111222333444555",
322 + "scope": "https://www.googleapis.com/auth/userinfo.email openid",
323 + "exp": "1710275200", // Expiration time in Unix epoch format
324 + "expires_in": "3599", // Seconds remaining until expiration
325 + "email": "user@example.com",
326 + "email_verified": "true"
327#endif
329 // expires_at in wire-format is expires_in seconds into future
332 [] ([[maybe_unused]] const ObjectVariantMapper& mapper, const DateTime* objOfType) -> VariantValue {
333 return VariantValue{(objOfType->AsUTC () - DateTime::NowUTC ()).As<int> ()};
334 }),
336 [] ([[maybe_unused]] const ObjectVariantMapper& mapper, const VariantValue& d, DateTime* into) -> void {
337 *into = DateTime::NowUTC ().AddSeconds (d.As<int> ());
338 })}},
339 });
340 return mapper;
341}();
342
343TypedBLOB TokenIntrospectionResponse::ToWireFormat () const
344{
345 return TypedBLOB{Variant::JSON::Writer{}.WriteAsBLOB (kMapper.FromObject (*this)), InternetMediaTypes::kJSON};
346}
347
348TokenIntrospectionResponse TokenIntrospectionResponse::FromWireFormat (const TypedBLOB& src)
349{
350 if (not src.fType or not InternetMediaTypeRegistry::sThe->IsA (InternetMediaTypes::kJSON, *src.fType)) {
351 static const auto kExcept_ = RuntimeErrorException{"Expected JSON"sv};
352 Throw (kExcept_);
353 }
354 return kMapper.ToObject<TokenIntrospectionResponse> (Variant::JSON::Reader{}.Read (src.fData));
355}
356
357/*
358 ********************************************************************************
359 ****************************** Auth::OAuth::UserInfo ***************************
360 ********************************************************************************
361 */
362String UserInfo::ToString () const
363{
364 StringBuilder sb;
365 sb << "{"sv;
366 if (name) {
367 sb << "name: "sv << name;
368 }
369 if (given_name) {
370 sb << ", given_name: "sv << given_name;
371 }
372 if (family_name) {
373 sb << ", family_name: "sv << family_name;
374 }
375 if (email) {
376 sb << ", email: "sv << email;
377 }
378 if (picture) {
379 sb << ", picture: "sv << picture;
380 }
381 sb << "}"sv;
382 return sb;
383}
384
385const ObjectVariantMapper UserInfo::kMapper = [] () {
386 ObjectVariantMapper mapper;
387 mapper.AddCommonType<String> ();
388 mapper.AddCommonType<optional<String>> ();
389 mapper.AddCommonType<URI> ();
390 mapper.AddCommonType<optional<URI>> ();
391 mapper.AddClass<UserInfo> ({
392 {"name"sv, &UserInfo::name},
393 {"given_name"sv, &UserInfo::given_name},
394 {"family_name"sv, &UserInfo::family_name},
395 {"email"sv, &UserInfo::email},
396 {"picture"sv, &UserInfo::picture},
397 });
398 return mapper;
399}();
400
401UserInfo UserInfo::FromWireFormat (const TypedBLOB& src)
402{
403 if (not src.fType or not InternetMediaTypeRegistry::sThe->IsA (InternetMediaTypes::kJSON, *src.fType)) {
404 static const auto kExcept_ = RuntimeErrorException{"Expected JSON"sv};
405 Throw (kExcept_);
406 }
407 return kMapper.ToObject<UserInfo> (Variant::JSON::Reader{}.Read (src.fData));
408}
409
410/*
411 ********************************************************************************
412 ***************************** Auth::OAuth::Fetcher *****************************
413 ********************************************************************************
414 */
415Fetcher::Fetcher (const ProviderConfiguration& providerConfiguration, const Options& options)
416 : fProviderConfiguration_{providerConfiguration}
417 , fOptions_{options}
418 , fMaybeLock_{options.fInternallySyncrhonized == eInternallySynchronized ? VirtualLockable::Make<recursive_mutex> ()
419 : VirtualLockable::Make<Debug::AssertExternallySynchronizedMutex> ()}
420 , fCache_{options.fCaching ? make_unique<Cache_> () : nullptr}
421{
422}
423
424Fetcher::Fetcher (const Fetcher& src)
425 : fProviderConfiguration_{src.fProviderConfiguration_}
426 , fOptions_{src.fOptions_}
427 , fMaybeLock_{src.fOptions_.fInternallySyncrhonized == eInternallySynchronized
428 ? VirtualLockable::Make<recursive_mutex> ()
429 : VirtualLockable::Make<Debug::AssertExternallySynchronizedMutex> ()}
430 , fCache_{src.fCache_ ? make_unique<Cache_> () : nullptr}
431{
432}
433
435{
436#if USE_NOISY_TRACE_IN_THIS_MODULE_
437 Debug::TraceContextBumper ctx{"OAuth::Fetcher::GetToken", "tr={}"_f, tr};
438#endif
439 auto nonCachingFetcher = [&] () -> TokenResponse {
440 URI tokenRequestURI = Memory::ValueOfOrThrow (fProviderConfiguration_.token_uri, RuntimeErrorException{"no token_uri"sv});
441 auto connection = IO::Network::Transfer::Connection::New ();
442 try {
443 //DbgTrace ("Sending={}"_f, Streams::BinaryToText::Convert (tr.ToWireFormat ().fData));
444 IO::Network::Transfer::Response r = connection.POST (tokenRequestURI, tr.ToWireFormat ());
445 //DbgTrace ("rawResponse={}"_f, Streams::BinaryToText::Convert (r.GetData ()));
446 return TokenResponse::FromWireFormat (r.GetTypedData ());
447 }
448 catch (...) {
449 DbgTrace ("Fetcher::Token: exception={}"_f, current_exception ());
451 }
452 };
453 if (fCache_) {
454 scoped_lock critSec{fMaybeLock_};
455 if (optional<TokenResponse> o = fCache_->fTokens.Lookup (tr)) {
456 auto now = DateTime::Now ();
457 if (o->expires_at <= now) {
458 return *o;
459 }
460 }
461 }
462 auto r = nonCachingFetcher ();
463 if (fCache_) {
464 scoped_lock critSec{fMaybeLock_};
465 fCache_->fTokens.Add (tr, r);
466 fCache_->fAccessToken2Expiration.Add (r.access_token, r.expires_at);
467 if (r.id_token) {
468 // @todo
469 // NOTE - ID_token doesnt contain EXACTLY same info as user_info endpoint - may need to update API to reflect this difference
470 // No, a decoded ID token may not contain the exact same information as the userinfo endpoint response
471 // . The information can overlap significantly, but there are key differences:
472
473 // cache ID_Token return from TOKEN API (since that has the expiry and userinfo information)
474 // This maybe best! Avoids whole API call, and I'm not sure we have the right URL todo this with facebook
475 // as identity manager...
476 // @todo if we got access token AND id token - parse out of ID token the user info and cache in
477 // ...
478 }
479 }
480 ClearOldStuffFromCache_ ();
481#if USE_NOISY_TRACE_IN_THIS_MODULE_
482 DbgTrace ("returning: {}"_f, r);
483#endif
484 return r;
485}
486
487void Fetcher::RevokeTokens (const TokenRevocationRequest& tr) const
488{
489#if USE_NOISY_TRACE_IN_THIS_MODULE_
490 Debug::TraceContextBumper ctx{"OAuth::Fetcher::RevokeTokens", "tr={}"_f, tr};
491#endif
492 if (fCache_) {
493 scoped_lock critSec{fMaybeLock_};
494 // remove references to the argument access_token (we dont cache refresh tokens currently)
495 fCache_->fTokens.RemoveAll (
496 [&] (const KeyValuePair<TokenRequest, TokenResponse>& kvp) { return tr.access_token == kvp.fValue.access_token; });
497 fCache_->fAccessToken2Expiration.RemoveIf (tr.access_token);
498 fCache_->fAccessToken2UserInfo.RemoveIf (tr.access_token);
499 }
500 if (optional<URI> revokeURI = fProviderConfiguration_.revocation_endpoint) {
501 auto connection = IO::Network::Transfer::Connection::New ();
502 try {
503 //DbgTrace ("Sending={}"_f, Streams::BinaryToText::Convert (tr.ToWireFormat ().fData));
504 [[maybe_unused]] IO::Network::Transfer::Response r = connection.POST (*revokeURI, tr.ToWireFormat ());
505 }
506 catch (...) {
507 DbgTrace ("Fetcher::RevokeTokens: exception={}"_f, current_exception ());
509 }
510 }
511 else {
512 DbgTrace ("Fetcher::RevokeTokens: skipping due to missing revocation_endpoint"_f);
513 }
514}
515
516#if 0
517Google TokenInfo Endpoint
518You can use this endpoint to "introspect" an access token by sending a GET request:
519Endpoint: https://oauth2.googleapis.com/tokeninfo
520Parameter: access_token
521Example Request
522http
523GET https://oauth2.googleapis.com
524Use code with caution.
525
526Expected JSON Response
527If the token is valid, Google returns metadata including the expiration time:
528Google Cloud Documentation
529Google Cloud Documentation
530json
531{
532 "azp": "123456789-example.apps.googleusercontent.com",
533 "aud": "123456789-example.apps.googleusercontent.com",
534 "sub": "111222333444555",
535 "scope": "https://www.googleapis.com/auth/userinfo.email openid",
536 "exp": "1710275200", // Expiration time in Unix epoch format
537 "expires_in": "3599", // Seconds remaining until expiration
538 "email": "user@example.com",
539 "email_verified": "true"
540}
541#endif
542
543UserInfo Fetcher::GetUserInfo (const String& accessToken) const
544{
545#if USE_NOISY_TRACE_IN_THIS_MODULE_
546 Debug::TraceContextBumper ctx{"OAuth::Fetcher::GetUserInfo", "accessToken={}"_f, tr};
547#endif
548 auto nonCachingFetcher = [&] () -> UserInfo {
549 URI userInfoRequestURI = Memory::ValueOfOrThrow (fProviderConfiguration_.userinfo_endpoint, RuntimeErrorException{"no userinfo_endpoint"sv});
550 auto authInfo = IO::Network::Transfer::Connection::Options::Authentication{"Bearer "sv + accessToken};
551 auto connection = IO::Network::Transfer::Connection::New (IO::Network::Transfer::Connection::Options{.fAuthentication = authInfo});
552 try {
553 IO::Network::Transfer::Response r = connection.GET (userInfoRequestURI);
554 //DbgTrace ("rawResponse={}"_f, Streams::BinaryToText::Convert (r.GetData ()));
555 return UserInfo::FromWireFormat (r.GetTypedData ());
556 }
557 catch (...) {
558 DbgTrace ("Fetcher::UserInfo: exception={}"_f, current_exception ());
560 }
561 };
562 if (fCache_) {
563 scoped_lock critSec{fMaybeLock_};
564 if (optional<DateTime> od = fCache_->fAccessToken2Expiration.Lookup (accessToken)) {
565 Time::DateTime now = Time::DateTime::Now ();
566 if (now > *od) {
567 fCache_->fAccessToken2UserInfo.RemoveIf (accessToken); // may as well remove if its expired
568 }
569 else {
570 if (optional<UserInfo> ou = fCache_->fAccessToken2UserInfo.Lookup (accessToken)) {
571 return *ou;
572 }
573 }
574 }
575 }
576 UserInfo userInfo = nonCachingFetcher ();
577 if (fCache_) {
578 /// if this is first time we've seen the access_code (load balancing situation where another server generates access_code and we dont see it)
579 // we still need to know how long the user_info is valid for - so ask, and if we cannot tell, make a conservative guess
580 {
581 unique_lock tmpLock{fMaybeLock_};
582 if (not fCache_->fAccessToken2Expiration.ContainsKey (accessToken)) {
583 tmpLock.unlock (); // don't hold lock while fetching
584 if (optional<TokenIntrospectionResponse> o = FetchTokenIntrospection_ (accessToken)) {
585 tmpLock.lock (); // but re-lock to update data structures
586 fCache_->fAccessToken2Expiration.Add (accessToken, o->expires_at);
587 }
588 else {
589 tmpLock.lock (); // but re-lock to update data structures
590 static constexpr auto kWAG_ = 30s;
591 fCache_->fAccessToken2Expiration.Add (accessToken, Time::DateTime::NowUTC () + kWAG_);
592 }
593 }
594 }
595 scoped_lock critSec{fMaybeLock_};
596 fCache_->fAccessToken2UserInfo.Add (accessToken, userInfo);
597 }
598 ClearOldStuffFromCache_ ();
599#if USE_NOISY_TRACE_IN_THIS_MODULE_
600 DbgTrace ("returning: {}"_f, userInfo);
601#endif
602 return userInfo;
603}
604
605optional<TokenIntrospectionResponse> Fetcher::FetchTokenIntrospection_ (const String& accessToken) const
606{
607 if (fProviderConfiguration_.introspection_endpoint) {
608 // NYI, but no biggie cuz google doesn't either
609 // https://datatracker.ietf.org/doc/html/rfc7662
611 }
612 if (fProviderConfiguration_.tokeninfo_endpoint) {
613 auto authInfo = IO::Network::Transfer::Connection::Options::Authentication{"Bearer "sv + accessToken};
614 auto connection = IO::Network::Transfer::Connection::New (IO::Network::Transfer::Connection::Options{.fAuthentication = authInfo});
615 try {
616 // A successful request returns a JSON object containing information about the token, such as:
617 // issued_to: The client ID to whom the token was issued.
618 // audience: The intended audience for the token.
619 // user_id: The obfuscated unique identifier for the user.
620 // scope: The space-separated list of scopes granted to the token.
621 // expires_in: The number of seconds left until the token expires.
622 // email: The user's email address.
623 // verified_email: A boolean indicating if the email address is verified.
624 // hd: The hosted domain of the user if they belong to a Google Workspace account.
625
626 // CLOSE to same as UserInfo - but all I use this for is the expiration info, so good enuf for that...
627 IO::Network::Transfer::Response r = connection.GET (*fProviderConfiguration_.tokeninfo_endpoint);
628 // DbgTrace ("rawResponse={}"_f, Streams::BinaryToText::Convert (r.GetData ()));
629 return TokenIntrospectionResponse::FromWireFormat (r.GetTypedData ());
630 }
631 catch (...) {
632 DbgTrace ("Fetcher::FetchTokenInfo_: exception={}"_f, current_exception ());
634 }
635 }
636 return nullopt;
637}
638
639void Fetcher::ClearOldStuffFromCache_ () const
640{
641#if USE_NOISY_TRACE_IN_THIS_MODULE_
642 Debug::TimingTrace ctx{"OAuth::ClearOldStuffFromCache_", 1ms};
643#endif
644 // quicky algorithm - hopefully good enuf for starters --LGP 2026-03-12
645 scoped_lock critSec{fMaybeLock_};
646 if (fCache_) {
647 Time::DateTime now = Time::DateTime::Now ();
648 if (Time::GetTickCount () > fCache_->fNextClearAt_) {
649 fCache_->fTokens.RemoveAll ([&] (const KeyValuePair<TokenRequest, TokenResponse>& kvp) { return now > kvp.fValue.expires_at; });
650 auto keys2Keep = fCache_->fAccessToken2UserInfo.Keys ();
651 fCache_->fAccessToken2Expiration.RetainAll (keys2Keep);
652 fCache_->fAccessToken2UserInfo.RetainAll (keys2Keep);
653 fCache_->fNextClearAt_ = Time::GetTickCount () + Cache_::kClearMaxFrequency_;
654 }
655 }
656}
#define AssertNotImplemented()
Definition Assertions.h:402
#define DbgTrace
Definition Trace.h:317
Similar to String, but intended to more efficiently construct a String. Mutable type (String is large...
String is like std::u32string, except it is much easier to use, often much more space efficient,...
Definition String.h:201
An Association pairs key values with (possibly multiple or none) mapped_type values....
nonvirtual optional< mapped_type > LookupOne(ArgByValueType< key_type > key) const
Lookup and return the first (maybe arbitrarily chosen which is first) value with this key,...
nonvirtual void Add(ArgByValueType< key_type > key, ArgByValueType< mapped_type > newElt)
nonvirtual mapped_type LookupOneChecked(ArgByValueType< key_type > key, const THROW_IF_MISSING &throwIfMissing) const
Lookup and return the first (maybe arbitrarily chosen which is first) value with this key,...
Set<T> is a container of T, where once an item is added, additionally adds () do nothing.
static Execution::Synchronized< InternetMediaTypeRegistry > sThe
ObjectVariantMapper can be used to map C++ types to and from variant-union types, which can be transp...
nonvirtual void AddClass(const Traversal::Iterable< StructFieldInfo > &fieldDescriptions, const ClassMapperOptions< CLASS > &mapperOptions={})
function< VariantValue(const ObjectVariantMapper &mapper, const T *objOfType)> FromObjectMapperType
nonvirtual void AddCommonType(ARGS &&... args)
nonvirtual VariantValue FromObject(const T &from) const
nonvirtual T ToObject(const VariantValue &v) const
function< void(const ObjectVariantMapper &mapper, const VariantValue &d, T *into)> ToObjectMapperType
TypedBLOB is a named tuple<Memory::BLOB, optional<InternetMediaType>> - with friendlier names,...
Definition TypedBLOB.h:48
Read a stream into an Association (or VariantValue) - following https://url.spec.whatwg....
nonvirtual Association< String, String > ReadAssociation(const Streams::InputStream::Ptr< byte > &in) const
Association (or VariantValue) to the output stream - following https://url.spec.whatwg....
nonvirtual Memory::BLOB WriteAsBLOB(const VariantValue &v) const
Simple variant-value (case variant union) object, with (variant) basic types analogous to a value in ...
nonvirtual TypedBLOB GetTypedData() const
combine the 'body' data with a content-type indicator into a combined structure.
nonvirtual CONTAINER_OF_T As(CONTAINER_OF_T_CONSTRUCTOR_ARGS... args) const
simple wrapper on IO::Network::Transfer to do fetching (more configurability to do)
Definition Client.h:231
nonvirtual UserInfo GetUserInfo(const String &accessToken) const
Definition Client.cpp:543
nonvirtual TokenResponse GetToken(const TokenRequest &tr) const
Definition Client.cpp:434
nonvirtual void RevokeTokens(const TokenRevocationRequest &tr) const
Definition Client.cpp:487
Track configuration data about stuff that differentiates different OAuth providers - what URLs to use...
optional< URI > tokeninfo_endpoint
logically similar to introspection_endpoint, but googles incompatible way
optional< URI > introspection_endpoint
RFC 7662 compatible API for finding info about a token - https://datatracker.ietf....
this represents a HTTP request object for the WebServer module
void Throw(T &&e2Throw)
identical to builtin C++ 'throw' except that it does helpful, type dependent DbgTrace() messages firs...
Definition Throw.inl:43
RFC 7662 compatible API for finding info about a token - https://datatracker.ietf....
Definition Client.h:179
this is the argument to the Fetcher::GetToken () API. It typically consists of a client_id,...
Definition Client.h:49
this is the response to the Fetcher::GetToken () API. It typically provides an 'access token' with a ...
Definition Client.h:125