Stroika Library 3.0d16
 
Loading...
Searching...
No Matches
Client.cpp
1/*
2 * Copyright(c) Sophist Solutions, Inc. 1990-2025. All rights reserved
3 */
4#include "Stroika/Frameworks/StroikaPreComp.h"
5
7#include "Stroika/Foundation/Containers/Association.h"
13#include "Stroika/Foundation/IO/Network/Transfer/Connection.h"
16
17#include "Client.h"
18
19using namespace Stroika::Foundation;
23using namespace Stroika::Foundation::Execution;
25
26using namespace Stroika::Frameworks;
27using namespace Stroika::Frameworks::Auth::OAuth;
28
29using Memory::BLOB;
30
31/*
32 ********************************************************************************
33 ************************* Auth::OAuth::TokenRequest ****************************
34 ********************************************************************************
35 */
36String TokenRequest::ToString () const
37{
39 sb << "{"sv;
40 sb << "client_id: "sv << client_id;
41 if (code) {
42 sb << ", code: "sv << code;
43 sb << ", grant_type: authorization_code"sv;
44 }
45 if (refresh_token) {
46 sb << ", refresh_token: "sv << refresh_token;
47 sb << ", grant_type: refresh_token"sv;
48 }
49 if (client_secret) {
50 sb << ", client_secret: "sv << client_secret;
51 }
52 if (redirect_uri) {
53 sb << ", redirect_uri: "sv << redirect_uri;
54 }
55 if (code_verifier) {
56 sb << ", code_verifier: "sv << code_verifier;
57 }
58 sb << "}"sv;
59 return sb;
60}
61
62const ObjectVariantMapper TokenRequest::kMapper = [] () {
64 mapper.AddCommonType<String> ();
65 mapper.AddCommonType<optional<String>> ();
66 mapper.AddCommonType<optional<URI>> ();
67 mapper.AddClass<TokenRequest> ({
68 {"client_id"sv, &TokenRequest::client_id},
69 {"code"sv, &TokenRequest::code},
70 {"refresh_token"sv, &TokenRequest::refresh_token},
71 {"client_secret"sv, &TokenRequest::client_secret},
72 {"redirect_uri"sv, &TokenRequest::redirect_uri},
73 {"code_verifier"sv, &TokenRequest::code_verifier},
74 });
75 return mapper;
76}();
77
78TypedBLOB TokenRequest::ToWireFormat () const
79{
80 if (not code and not refresh_token) {
81 static const auto kExcept_ = RuntimeErrorException{"Missing authorization code/refresh_token"sv};
82 Throw (kExcept_);
83 }
84 if (code and refresh_token) {
85 static const auto kExcept_ = RuntimeErrorException{"Cannot combine authorization code/refresh_token"sv};
86 Throw (kExcept_);
87 }
88 if (client_id.empty ()) {
89 static const auto kExcept_ = RuntimeErrorException{"Missing client_id"sv};
90 Throw (kExcept_);
91 }
92 BLOB reqBody = [&] () {
94 params.Add ({"client_id"sv, client_id});
95 if (code) {
96 params.Add ({"code"sv, *code});
97 params.Add ({"grant_type"sv, "authorization_code"sv});
98 }
99 else {
100 params.Add ({"refresh_token"sv, *refresh_token});
101 params.Add ({"grant_type"sv, "refresh_token"sv});
102 }
103 if (client_secret) {
104 params.Add ({"client_secret"sv, *client_secret});
105 }
106 if (redirect_uri) {
107 params.Add ({"redirect_uri"sv, redirect_uri->As<String> ()});
108 }
109 if (code_verifier) {
110 params.Add ({"code_verifier"sv, *code_verifier});
111 }
112 return Variant::FormURLEncoded::Writer{}.WriteAsBLOB (params);
113 }();
114 return TypedBLOB{reqBody, InternetMediaTypes::kWWWFormURLEncoded};
115}
116
117TokenRequest TokenRequest::FromWireFormat (const TypedBLOB& src)
118{
119 if (not src.fType or not InternetMediaTypeRegistry::sThe->IsA (InternetMediaTypes::kWWWFormURLEncoded, *src.fType)) {
120 static const auto kExcept_ = RuntimeErrorException{"Expected {}"_f(InternetMediaTypes::kWWWFormURLEncoded)};
121 Throw (kExcept_);
122 }
124 static const auto kExcept_clientid_ = RuntimeErrorException{"Missing client_id"sv};
125 static const auto kExcept_authCode_ = RuntimeErrorException{"Missing authentication code"sv};
126 static const auto kExcept_grant_type_ = RuntimeErrorException{"Missing grant_type"sv};
127 auto code = params.LookupOne ("code"sv);
128 auto refresh_token = params.LookupOne ("refresh_token"sv);
129 if (not code and not refresh_token) {
130 static const auto kExcept_ = RuntimeErrorException{"Missing authorization code/refresh_token"sv};
131 Throw (kExcept_);
132 }
133 if (code and refresh_token) {
134 static const auto kExcept_ = RuntimeErrorException{"Cannot combine authorization code/refresh_token"sv};
135 Throw (kExcept_);
136 }
137 return TokenRequest{.client_id = params.LookupOneChecked ("client_id"sv, kExcept_clientid_),
138 .code = code,
139 .refresh_token = refresh_token,
140 .client_secret = params.LookupOne ("client_secret"sv),
141 .redirect_uri = params.LookupOne ("redirect_uri"sv)};
142}
143
144/*
145 ********************************************************************************
146 ************************* Auth::OAuth::TokenResponse ***************************
147 ********************************************************************************
148 */
149String TokenResponse::ToString () const
150{
151 StringBuilder sb;
152 sb << "{"sv;
153 sb << "access_token: "sv << access_token;
154 sb << ", expires_at: "sv << expires_at;
155 sb << ", scope: "sv << scope;
156 if (refresh_token) {
157 sb << ", refresh_token: "sv << refresh_token;
158 }
159 if (id_token) {
160 sb << ", id_token: "sv << id_token;
161 }
162 if (token_type) {
163 sb << ", token_type: "sv << token_type;
164 }
165 sb << "}"sv;
166 return sb;
167}
168
169const ObjectVariantMapper TokenResponse::kMapper = [] () {
170 ObjectVariantMapper mapper;
171 using TypeMappingDetails = ObjectVariantMapper::TypeMappingDetails;
172 mapper.AddCommonType<String> ();
173 mapper.AddCommonType<optional<String>> ();
174 mapper.AddCommonType<DateTime> ();
175 mapper.AddCommonType<Set<String>> ();
176 mapper.AddClass<TokenResponse> ({
177 {"access_token"sv, &TokenResponse::access_token},
178 // expires_at in wire-format is expires_in seconds into future
179 {"expires_in"sv, &TokenResponse::expires_at,
181 [] ([[maybe_unused]] const ObjectVariantMapper& mapper, const DateTime* objOfType) -> VariantValue {
182 return VariantValue{(objOfType->AsUTC () - DateTime::NowUTC ()).As<int> ()};
183 }),
185 [] ([[maybe_unused]] const ObjectVariantMapper& mapper, const VariantValue& d, DateTime* into) -> void {
186 *into = DateTime::NowUTC ().AddSeconds (d.As<int> ());
187 })}},
188 // scope in wire-format is space separated
189 {"scope"sv, &TokenResponse::scope,
191 [] ([[maybe_unused]] const ObjectVariantMapper& mapper, const Set<String>* objOfType) -> VariantValue {
192 return objOfType->Join (" "sv);
193 }),
195 [] ([[maybe_unused]] const ObjectVariantMapper& mapper, const VariantValue& d, Set<String>* into) -> void {
196 *into = Set<String>{d.As<String> ().Tokenize ()};
197 })}},
198 {"refresh_token"sv, &TokenResponse::refresh_token},
199 {"id_token"sv, &TokenResponse::id_token},
200 {"token_type"sv, &TokenResponse::token_type},
201 });
202 return mapper;
203}();
204
205TypedBLOB TokenResponse::ToWireFormat () const
206{
207 return TypedBLOB{Variant::JSON::Writer{}.WriteAsBLOB (kMapper.FromObject (*this)), InternetMediaTypes::kJSON};
208}
209
210TokenResponse TokenResponse::FromWireFormat (const TypedBLOB& src)
211{
212 if (not src.fType or not InternetMediaTypeRegistry::sThe->IsA (InternetMediaTypes::kJSON, *src.fType)) {
213 static const auto kExcept_ = RuntimeErrorException{"Expected JSON"sv};
214 Throw (kExcept_);
215 }
216 return kMapper.ToObject<TokenResponse> (Variant::JSON::Reader{}.Read (src.fData));
217}
218
219/*
220 ********************************************************************************
221 ********************* Auth::OAuth::TokenRevocationRequest **********************
222 ********************************************************************************
223 */
224String TokenRevocationRequest::ToString () const
225{
226 StringBuilder sb;
227 sb << "{"sv;
228 sb << "access_token: "sv << access_token;
229 if (refresh_token) {
230 sb << ", refresh_token: "sv << *refresh_token;
231 }
232 if (client_id) {
233 sb << ", client_id: "sv << *client_id;
234 }
235 if (client_secret) {
236 sb << ", client_secret: "sv << *client_secret;
237 }
238 sb << "}"sv;
239 return sb;
240}
241
242const ObjectVariantMapper TokenRevocationRequest::kMapper = [] () {
243 ObjectVariantMapper mapper;
244 mapper.AddCommonType<String> ();
245 mapper.AddCommonType<optional<String>> ();
246 mapper.AddClass<TokenRevocationRequest> ({
247 {"access_token"sv, &TokenRevocationRequest::access_token},
248 {"refresh_token"sv, &TokenRevocationRequest::refresh_token},
249 {"client_id"sv, &TokenRevocationRequest::client_id},
250 {"client_secret"sv, &TokenRevocationRequest::client_secret},
251 });
252 return mapper;
253}();
254
255TypedBLOB TokenRevocationRequest::ToWireFormat () const
256{
257 if (access_token.empty ()) {
258 static const auto kExcept_ = RuntimeErrorException{"Missing access_token"sv};
259 Throw (kExcept_);
260 }
261 BLOB reqBody = [&] () {
263 if (refresh_token) {
264 params.Add ({"token_type_hint"sv, "refresh_token"sv});
265 params.Add ({"token"sv, *refresh_token});
266 }
267 else {
268 params.Add ({"token_type_hint"sv, "access_token"sv});
269 params.Add ({"token"sv, access_token});
270 }
271 if (client_id) {
272 params.Add ({"client_id"sv, *client_id});
273 }
274 if (client_secret) {
275 params.Add ({"client_secret"sv, *client_secret});
276 }
277 return Variant::FormURLEncoded::Writer{}.WriteAsBLOB (params);
278 }();
279 return TypedBLOB{reqBody, InternetMediaTypes::kWWWFormURLEncoded};
280}
281
282/*
283 ********************************************************************************
284 ****************************** Auth::OAuth::UserInfo ***************************
285 ********************************************************************************
286 */
287String UserInfo::ToString () const
288{
289 StringBuilder sb;
290 sb << "{"sv;
291 if (name) {
292 sb << ", name: "sv << name;
293 }
294 if (given_name) {
295 sb << ", given_name: "sv << given_name;
296 }
297 if (family_name) {
298 sb << ", family_name: "sv << family_name;
299 }
300 if (email) {
301 sb << ", email: "sv << email;
302 }
303 if (picture) {
304 sb << ", picture: "sv << picture;
305 }
306 sb << "}"sv;
307 return sb;
308}
309
310const ObjectVariantMapper UserInfo::kMapper = [] () {
311 ObjectVariantMapper mapper;
312 mapper.AddCommonType<String> ();
313 mapper.AddCommonType<optional<String>> ();
314 mapper.AddCommonType<URI> ();
315 mapper.AddCommonType<optional<URI>> ();
316 mapper.AddClass<UserInfo> ({
317 {"name"sv, &UserInfo::name},
318 {"given_name"sv, &UserInfo::given_name},
319 {"family_name"sv, &UserInfo::family_name},
320 {"email"sv, &UserInfo::email},
321 {"picture"sv, &UserInfo::picture},
322 });
323 return mapper;
324}();
325
326UserInfo UserInfo::FromWireFormat (const TypedBLOB& src)
327{
328 if (not src.fType or not InternetMediaTypeRegistry::sThe->IsA (InternetMediaTypes::kJSON, *src.fType)) {
329 static const auto kExcept_ = RuntimeErrorException{"Expected JSON"sv};
330 Throw (kExcept_);
331 }
332 return kMapper.ToObject<UserInfo> (Variant::JSON::Reader{}.Read (src.fData));
333}
334
335/*
336 ********************************************************************************
337 ***************************** Auth::OAuth::Fetcher *****************************
338 ********************************************************************************
339 */
340Fetcher::Fetcher (const ProviderConfiguration& providerConfiguration)
341 : fProviderConfiguration_{providerConfiguration}
342{
343}
344
346{
347 URI tokenRequestURI = Memory::ValueOfOrThrow (fProviderConfiguration_.token_uri, RuntimeErrorException{"no token_uri"sv});
348 auto connection = IO::Network::Transfer::Connection::New ();
349 try {
350 //DbgTrace ("Sending={}"_f, Streams::BinaryToText::Convert (tr.ToWireFormat ().fData));
351 IO::Network::Transfer::Response r = connection.POST (tokenRequestURI, tr.ToWireFormat ());
352 //DbgTrace ("rawResponse={}"_f, Streams::BinaryToText::Convert (r.GetData ()));
353 return TokenResponse::FromWireFormat (r.GetTypedData ());
354 }
355 catch (...) {
356 DbgTrace ("Fetcher::Token: exception={}"_f, current_exception ());
358 }
359}
360
361void Fetcher::RevokeTokens (const TokenRevocationRequest& tr) const
362{
363 if (optional<URI> revokeURI = fProviderConfiguration_.revocation_endpoint) {
364 auto connection = IO::Network::Transfer::Connection::New ();
365 try {
366 //DbgTrace ("Sending={}"_f, Streams::BinaryToText::Convert (tr.ToWireFormat ().fData));
367 [[maybe_unused]] IO::Network::Transfer::Response r = connection.POST (*revokeURI, tr.ToWireFormat ());
368 }
369 catch (...) {
370 DbgTrace ("Fetcher::RevokeTokens: exception={}"_f, current_exception ());
372 }
373 }
374 else {
375 DbgTrace ("Fetcher::RevokeTokens: skipping due to missing revocation_endpoint"_f);
376 }
377}
378
379Auth::OAuth::UserInfo Fetcher::GetUserInfo (const String& accessToken) const
380{
381 URI userInfoRequestURI = Memory::ValueOfOrThrow (fProviderConfiguration_.userinfo_endpoint, RuntimeErrorException{"no userinfo_endpoint"sv});
382 auto authInfo = IO::Network::Transfer::Connection::Options::Authentication{"Bearer "sv + accessToken};
383 auto connection = IO::Network::Transfer::Connection::New (IO::Network::Transfer::Connection::Options{.fAuthentication = authInfo});
384 try {
385 IO::Network::Transfer::Response r = connection.GET (userInfoRequestURI);
386 //DbgTrace ("rawResponse={}"_f, Streams::BinaryToText::Convert (r.GetData ()));
387 return Auth::OAuth::UserInfo::FromWireFormat (r.GetTypedData ());
388 }
389 catch (...) {
390 DbgTrace ("Fetcher::UserInfo: exception={}"_f, current_exception ());
392 }
393}
#define DbgTrace
Definition Trace.h:309
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.
Definition Set.h:105
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
Definition Writer.cpp:48
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
nonvirtual UserInfo GetUserInfo(const String &accessToken) const
Definition Client.cpp:379
nonvirtual TokenResponse GetToken(const TokenRequest &tr) const
Definition Client.cpp:345
nonvirtual void RevokeTokens(const TokenRevocationRequest &tr) const
Definition Client.cpp:361
Track configuration data about stuff that differentiates different OAuth providers - what URLs to use...
void Throw(T &&e2Throw)
identical to builtin C++ 'throw' except that it does helpful, type dependent DbgTrace() messages firs...
Definition Throw.inl:43