Audacity 3.2.0
OAuthService.cpp
Go to the documentation of this file.
1/* SPDX-License-Identifier: GPL-2.0-or-later */
2/*!********************************************************************
3
4 Audacity: A Digital Audio Editor
5
6 OAuthService.cpp
7
8 Dmitry Vedenko
9
10**********************************************************************/
11
12#include "OAuthService.h"
13
14#include <cassert>
15#include <cctype>
16
17#include <rapidjson/document.h>
18#include <rapidjson/writer.h>
19
20#include "CodeConversions.h"
21#include "Prefs.h"
22
23#include "IResponse.h"
24#include "NetworkManager.h"
25#include "Request.h"
26
27#include "ServiceConfig.h"
28
29#include "UrlDecode.h"
30
31#include "BasicUI.h"
32#include "ExportUtils.h"
33
34#include "StringUtils.h"
35#include "UrlEncode.h"
36
38{
39namespace
40{
41
42StringSetting refreshToken { L"/cloud/audiocom/refreshToken", "" };
43
44const std::string_view uriPrefix = "audacity://link";
45const std::string_view usernamePrefix = "username=";
46const std::string_view passwordPrefix = "password=";
47const std::string_view tokenPrefix = "token=";
48const std::string_view authClientPrefix = "authclient=";
49const std::string_view responseTypePrefix = "response_type=";
50const std::string_view clientIdPrefix = "client_id=";
51const std::string_view authorizationCodePrefix = "authorization_code=";
52const std::string_view codePrefix = "code=";
53const std::string_view urlPrefix = "url=";
54const std::string_view userPrefix = "user=";
55
56void WriteClientFields(rapidjson::Document& document)
57{
58 using namespace rapidjson;
59
60 const auto clientID = GetServiceConfig().GetOAuthClientID();
61 const auto clientSecret = GetServiceConfig().GetOAuthClientSecret();
62
63 document.AddMember(
64 "client_id",
65 Value(clientID.data(), clientID.size(), document.GetAllocator()),
66 document.GetAllocator());
67
68 document.AddMember(
69 "client_secret",
70 Value(clientSecret.data(), clientSecret.size(), document.GetAllocator()),
71 document.GetAllocator());
72}
73
74void WriteAccessFields(rapidjson::Document& document, std::string_view grantType, std::string_view scope)
75{
76 using namespace rapidjson;
77
78 document.AddMember(
79 "grant_type", StringRef(grantType.data(), grantType.size()),
80 document.GetAllocator());
81
82 document.AddMember(
83 "scope", StringRef(scope.data(), scope.size()), document.GetAllocator());
84}
85
87 rapidjson::Document& document, std::string_view grantType, std::string_view scope)
88{
89 WriteClientFields(document);
90 WriteAccessFields(document, grantType, scope);
91}
92
93template<typename Elem, typename First, typename ...Others>
94void append(std::basic_string<Elem>& dest, First&& first, Others&& ...others)
95{
96 dest.append(first);
97 if constexpr (sizeof...(others) != 0)
98 append(dest, std::forward<Others>(others)...);
99}
100
101template<typename First, typename ...Others>
102auto concat(First&& first, Others&& ...others)
103{
104 std::basic_string<typename First::value_type> dest(first);
105 append(dest, std::forward<Others>(others)...);
106 return dest;
107}
108
109} // namespace
110
112 std::function<void(std::string_view)> completedHandler, AudiocomTrace trace,
113 bool silent)
114{
116 {
117 if (completedHandler)
118 completedHandler(GetAccessToken());
119 return;
120 }
121
123 GetServiceConfig(), trace, std::move(completedHandler), silent);
124}
125
127 std::string_view uri, AudiocomTrace trace,
128 std::function<void(std::string_view)> completedHandler)
129{
131 {
132 if (completedHandler)
133 completedHandler({});
134 return false;
135 }
136
137 // It was observed, that sometimes link is passed as audacity://link/
138 // This is valid trace URI point of view, but we need to handle it separately
139 const auto argsStart = uri.find("?");
140
141 if (argsStart == std::string_view::npos)
142 {
143 if (completedHandler)
144 completedHandler({});
145 return false;
146 }
147
148 // Length is handled in IsPrefixed
149 auto args = uri.substr(argsStart + 1);
150
151 std::string_view token;
152 std::string_view username;
153 std::string_view password;
154 std::string_view authorizationCode;
155 auto useAudioComRedirectURI = false;
156
157 while (!args.empty())
158 {
159 const auto nextArg = args.find('&');
160
161 const auto arg = args.substr(0, nextArg);
162 args = nextArg == std::string_view::npos ? "" : args.substr(nextArg + 1);
163
164 if (IsPrefixed(arg, usernamePrefix))
165 username = arg.substr(usernamePrefix.length());
166 else if (IsPrefixed(arg, passwordPrefix))
167 password = arg.substr(passwordPrefix.length());
168 else if (IsPrefixed(arg, tokenPrefix))
169 token = arg.substr(tokenPrefix.length());
170 else if (IsPrefixed(arg, authorizationCodePrefix))
171 {
172 //authorization code was generated for audio.com, not audacity...
173 useAudioComRedirectURI = true;
174 authorizationCode = arg.substr(authorizationCodePrefix.length());
175 }
176 else if (IsPrefixed(arg, codePrefix))
177 authorizationCode = arg.substr(codePrefix.length());
178 }
179
180 // Some browsers (safari) add an extra trailing chars we don't need
181 size_t hashPos = authorizationCode.find('#');
182
183 if (hashPos != std::string::npos) {
184 authorizationCode = authorizationCode.substr(0, hashPos);
185 }
186
187 // We have a prioritized list of authorization methods
188 if (!authorizationCode.empty())
189 {
191 GetServiceConfig(), authorizationCode, useAudioComRedirectURI, trace,
192 std::move(completedHandler));
193 }
194 else if (!token.empty())
195 {
197 GetServiceConfig(), token, trace, std::move(completedHandler), false);
198 }
199 else if (!username.empty() && !password.empty())
200 {
202 GetServiceConfig(), audacity::UrlDecode(std::string(username)),
203 audacity::UrlDecode(std::string(password)), trace,
204 std::move(completedHandler));
205 }
206 else
207 {
208 if (completedHandler)
209 completedHandler({});
210
211 return false;
212 }
213
214 return true;
215}
216
218{
219 std::lock_guard<std::recursive_mutex> lock(mMutex);
220
221 mAccessToken.clear();
223 gPrefs->Flush();
224
225 // Unlink account is expected to be called only
226 // on UI thread
227 Publish({ {}, {}, trace, false });
228}
229
231 const ServiceConfig& config, std::string_view userName,
232 std::string_view password, AudiocomTrace trace,
233 std::function<void(std::string_view)> completedHandler)
234{
235 using namespace rapidjson;
236
237 Document document;
238 document.SetObject();
239
240 WriteCommonFields(document, "password", "all");
241
242 document.AddMember(
243 "username", StringRef(userName.data(), userName.size()),
244 document.GetAllocator());
245
246 document.AddMember(
247 "password", StringRef(password.data(), password.size()),
248 document.GetAllocator());
249
250 rapidjson::StringBuffer buffer;
251 rapidjson::Writer<rapidjson::StringBuffer> writer(buffer);
252 document.Accept(writer);
253
255 config, { buffer.GetString(), buffer.GetSize() }, trace,
256 std::move(completedHandler), false);
257}
258
260 const ServiceConfig& config, std::string_view token, AudiocomTrace trace,
261 std::function<void(std::string_view)> completedHandler, bool silent)
262{
263 using namespace rapidjson;
264
265 Document document;
266 document.SetObject();
267
268 WriteCommonFields(document, "refresh_token", "");
269
270 document.AddMember(
271 "refresh_token", StringRef(token.data(), token.size()),
272 document.GetAllocator());
273
274 rapidjson::StringBuffer buffer;
275 rapidjson::Writer<rapidjson::StringBuffer> writer(buffer);
276 document.Accept(writer);
277
279 config, { buffer.GetString(), buffer.GetSize() }, trace,
280 std::move(completedHandler), silent);
281}
282
284 const ServiceConfig& config, AudiocomTrace trace,
285 std::function<void(std::string_view)> completedHandler, bool silent)
286{
287 std::lock_guard<std::recursive_mutex> lock(mMutex);
288
290 config, audacity::ToUTF8(refreshToken.Read()), trace,
291 std::move(completedHandler), silent);
292}
293
295 const ServiceConfig& config, std::string_view authorizationCode, bool useAudioComRedirectURI,
296 AudiocomTrace trace, std::function<void(std::string_view)> completedHandler)
297{
298 using namespace rapidjson;
299
300 Document document;
301 document.SetObject();
302
303 WriteCommonFields(document, "authorization_code", "all");
304
305 document.AddMember(
306 "code", StringRef(authorizationCode.data(), authorizationCode.size()),
307 document.GetAllocator());
308
309 const auto redirectURI = useAudioComRedirectURI ? config.GetOAuthRedirectURL() : std::string("audacity://link");
310
311 document.AddMember(
312 "redirect_uri", StringRef(redirectURI.data(), redirectURI.size()),
313 document.GetAllocator());
314
315 rapidjson::StringBuffer buffer;
316 rapidjson::Writer<rapidjson::StringBuffer> writer(buffer);
317 document.Accept(writer);
318
320 config, { buffer.GetString(), buffer.GetSize() }, trace,
321 std::move(completedHandler), false);
322}
323
325{
326 return !GetAccessToken().empty();
327}
328
330{
331 std::lock_guard<std::recursive_mutex> lock(mMutex);
332 return !refreshToken.Read().empty();
333}
334
336{
337 std::lock_guard<std::recursive_mutex> lock(mMutex);
338
339 if (Clock::now() < mTokenExpirationTime)
340 return mAccessToken;
341
342 return {};
343}
344
345std::string OAuthService::MakeOAuthRequestURL(std::string_view authClientId)
346{
347 using namespace audacity::network_manager;
348
349 return concat(
350 GetServiceConfig().GetAPIUrl("/auth/authorize?"),
351 authClientPrefix, authClientId, "&",
352 responseTypePrefix, "code", "&",
353 clientIdPrefix, GetServiceConfig().GetOAuthClientID(),
354 "&redirect_uri=audacity://link"
355 );
356}
357
358std::string OAuthService::MakeAudioComAuthorizeURL(std::string_view userId, std::string_view redirectUrl)
359{
360 auto token = GetAccessToken();
361
362 // Remove token type from the token string
363 size_t pos = token.find(' ');
364 if (pos != std::string::npos) {
365 token = token.substr(pos + 1);
366 }
367
368 return concat(
369 GetServiceConfig().GetAuthWithRedirectURL(), "?",
370 tokenPrefix, token, "&",
371 userPrefix, userId, "&",
372 urlPrefix, redirectUrl
373 );
374}
375
376void OAuthService::Authorize(std::string_view email,
377 std::string_view password,
378 AuthSuccessCallback onSuccess,
379 AuthFailureCallback onFailure,
380 AudiocomTrace trace)
381{
382 using namespace audacity::network_manager;
383 using rapidjson::StringRef;
384
385 rapidjson::Document document;
386 document.SetObject();
387
388 WriteCommonFields(document, "password", "all");
389
390 document.AddMember(
391 "username", StringRef(email.data(), email.size()),
392 document.GetAllocator());
393
394 document.AddMember(
395 "password", StringRef(password.data(), password.size()),
396 document.GetAllocator());
397
398 rapidjson::StringBuffer buffer;
399 rapidjson::Writer<rapidjson::StringBuffer> writer(buffer);
400 document.Accept(writer);
401
402 Request request(GetServiceConfig().GetAPIUrl("/auth/token"));
405
406 auto response = NetworkManager::GetInstance().doPost(request, buffer.GetString(), buffer.GetSize());
407 response->setRequestFinishedCallback(
408 [response, this, trace,
409 onSuccess = std::move(onSuccess),
410 onFailure = std::move(onFailure)](auto) mutable
411 {
412 const auto httpCode = response->getHTTPCode();
413 const auto body = response->readAll<std::string>();
414 if(httpCode == 200)
415 ParseTokenResponse(body, std::move(onSuccess), std::move(onFailure), trace, false);
416 else
417 {
418 if(onFailure)
419 onFailure(httpCode, body);
420
421 SafePublish({ {}, body, trace, false, false });
422 }
423 });
424}
425
426void OAuthService::Register(std::string_view email,
427 std::string_view password,
428 AuthSuccessCallback successCallback,
429 AuthFailureCallback failureCallback,
430 AudiocomTrace trace)
431{
432 using namespace audacity::network_manager;
433 using rapidjson::StringRef;
434
435 rapidjson::Document document;
436 document.SetObject();
437
438 WriteClientFields(document);
439
440 document.AddMember(
441 "email", StringRef(email.data(), email.size()),
442 document.GetAllocator());
443
444 document.AddMember(
445 "password", StringRef(password.data(), password.size()),
446 document.GetAllocator());
447
448 rapidjson::StringBuffer buffer;
449 rapidjson::Writer<rapidjson::StringBuffer> writer(buffer);
450 document.Accept(writer);
451
452 Request request(GetServiceConfig().GetAPIUrl("/auth/register"));
455
456 auto response = NetworkManager::GetInstance().doPost(request, buffer.GetString(), buffer.GetSize());
457 response->setRequestFinishedCallback(
458 [response, this, trace,
459 successCallback = std::move(successCallback),
460 failureCallback = std::move(failureCallback)] (auto) mutable
461 {
462 const auto httpCode = response->getHTTPCode();
463 const auto body = response->readAll<std::string>();
464 if(httpCode == 200)
465 ParseTokenResponse(body, std::move(successCallback), std::move(failureCallback), trace, false);
466 else
467 {
468 if(failureCallback)
469 failureCallback(httpCode, body);
470
471 SafePublish({ {}, body, trace, false, false });
472 }
473 });
474}
475
477 const ServiceConfig& config, std::string_view payload, AudiocomTrace trace,
478 AuthSuccessCallback completedHandler, bool silent)
479{
480 using namespace audacity::network_manager;
481
482 Request request(config.GetAPIUrl("/auth/token"));
483
484 request.setHeader(
486
487 request.setHeader(
489
490 auto response = NetworkManager::GetInstance().doPost(
491 request, payload.data(), payload.size());
492
493 response->setRequestFinishedCallback(
494 [response, this, handler = std::move(completedHandler), silent,
495 trace](auto) mutable {
496 const auto httpCode = response->getHTTPCode();
497 const auto body = response->readAll<std::string>();
498
499 if (httpCode != 200)
500 {
501 if (handler)
502 handler({});
503
504 // Token has expired?
505 if (httpCode == 422)
506 BasicUI::CallAfter([this, trace] { UnlinkAccount(trace); });
507 else
508 SafePublish({ {}, body, trace, false, silent });
509
510 return;
511 }
512 ParseTokenResponse(body, std::move(handler), {}, trace, silent);
513 });
514}
515
516void OAuthService::ParseTokenResponse(std::string_view body,
517 AuthSuccessCallback successCallback,
518 AuthFailureCallback failureCallback,
519 AudiocomTrace trace,
520 bool silent)
521{
522 rapidjson::Document document;
523 document.Parse(body.data(), body.size());
524
525 if (!document.IsObject())
526 {
527 if (failureCallback)
528 failureCallback(200, body);
529
530 SafePublish({ {}, body, trace, false, silent });
531 return;
532 }
533
534 const auto tokenType = document["token_type"].GetString();
535 const auto accessToken = document["access_token"].GetString();
536 const auto expiresIn = document["expires_in"].GetInt64();
537 const auto newRefreshToken = document["refresh_token"].GetString();
538
539 {
540 std::lock_guard<std::recursive_mutex> lock(mMutex);
541
542 mAccessToken = std::string(tokenType) + " " + accessToken;
544 Clock::now() + std::chrono::seconds(expiresIn);
545 }
546
548 [token = std::string(newRefreshToken)]()
549 {
550 // At this point access token is already written,
551 // only refresh token is updated.
552 refreshToken.Write(token);
553 gPrefs->Flush();
554 });
555
556 if (successCallback)
557 successCallback(mAccessToken);
558
559 // The callback only needs the access token, so invoke it immediately.
560 // Networking is thread safe
561 SafePublish({ mAccessToken, {}, trace, true, silent });
562}
563
565{
566 BasicUI::CallAfter([this, message]() { Publish(message); });
567}
568
570{
571 static OAuthService service;
572 return service;
573}
574
575namespace
576{
577
579{
580public:
581 void OnSettingResetBegin() override
582 {
583 }
584
585 void OnSettingResetEnd() override
586 {
589 }
590};
591
594}
595
596} // namespace audacity::cloud::audiocom
Toolkit-neutral facade for basic user interface services.
Declare functions to perform UTF-8 to std::wstring conversions.
AudiocomTrace
Definition: ExportUtils.h:27
Declare an interface for HTTP response.
Declare a class for performing HTTP requests.
audacity::BasicSettings * gPrefs
Definition: Prefs.cpp:68
Declare a class for constructing HTTP requests.
bool IsPrefixedInsensitive(const HayType &hay, const PrefixType &prefix)
Definition: StringUtils.h:146
bool IsPrefixed(const HayType &hay, const PrefixType &prefix)
Definition: StringUtils.h:129
Declare a function to decode an URL encode string.
Declare a function to perform URL encoding of a string.
CallbackReturn Publish(const AuthStateChangedMessage &message)
Send a message to connected callbacks.
Definition: Observer.h:207
Allows custom logic for preferences reset event.
Definition: Prefs.h:563
bool Write(const T &value)
Write value to config and return true if successful.
Definition: Prefs.h:259
void Invalidate() override
Definition: Prefs.h:289
bool Read(T *pVar) const
overload of Read returning a boolean that is true if the value was previously defined *‍/
Definition: Prefs.h:207
Specialization of Setting for strings.
Definition: Prefs.h:370
virtual bool Flush() noexcept=0
Service responsible for OAuth authentication against the audio.com service.
Definition: OAuthService.h:43
void AuthoriseRefreshToken(const ServiceConfig &config, std::string_view refreshToken, AudiocomTrace, std::function< void(std::string_view)> completedHandler, bool silent)
std::function< void(std::string_view)> AuthSuccessCallback
Definition: OAuthService.h:45
void Register(std::string_view email, std::string_view password, AuthSuccessCallback successCallback, AuthFailureCallback failureCallback, AudiocomTrace trace)
Register a new user with email and password.
void Authorize(std::string_view email, std::string_view password, AuthSuccessCallback successCallback, AuthFailureCallback failureCallback, AudiocomTrace trace)
void SafePublish(const AuthStateChangedMessage &message)
void AuthoriseCode(const ServiceConfig &config, std::string_view authorizationCode, bool useAudioComRedirectURI, AudiocomTrace, std::function< void(std::string_view)> completedHandler)
void DoAuthorise(const ServiceConfig &config, std::string_view payload, AudiocomTrace, std::function< void(std::string_view)> completedHandler, bool silent)
void ParseTokenResponse(std::string_view response, AuthSuccessCallback successCallback, AuthFailureCallback failureCallback, AudiocomTrace trace, bool silent)
std::string MakeAudioComAuthorizeURL(std::string_view userId, std::string_view redirectUrl)
Creates a link to authorize audio.com using current auth token.
void ValidateAuth(AuthSuccessCallback completedHandler, AudiocomTrace, bool silent)
Attempt to authorize the user.
void AuthorisePassword(const ServiceConfig &config, std::string_view userName, std::string_view password, AudiocomTrace, std::function< void(std::string_view)> completedHandler)
void UnlinkAccount(AudiocomTrace)
Removes access and refresh token, notifies about the logout.
static std::string MakeOAuthRequestURL(std::string_view authClientId)
Creates a link to authorization request dialog.
std::function< void(unsigned, std::string_view)> AuthFailureCallback
Definition: OAuthService.h:46
bool HasAccessToken() const
Indicates, that service has a valid access token, i. e. that the user is authorized.
bool HandleLinkURI(std::string_view uri, AudiocomTrace, AuthSuccessCallback completedHandler)
Handle the OAuth callback.
std::string GetAccessToken() const
Return the current access token, if any.
Configuration for the audio.com.
Definition: ServiceConfig.h:25
std::string GetOAuthRedirectURL() const
OAuth2 redirect URL. Only used to satisfy the protocol.
std::string GetOAuthClientSecret() const
OAuth2 client secret.
std::string GetAPIUrl(std::string_view apiURI) const
Helper to construct the full URLs for the API.
std::string GetOAuthClientID() const
OAuth2 client ID.
ResponsePtr doPost(const Request &request, const void *data, size_t size)
Request & setHeader(const std::string &name, std::string value)
Definition: Request.cpp:46
void CallAfter(Action action)
Schedule an action to be done later, and in the main thread.
Definition: BasicUI.cpp:214
constexpr size_t npos(-1)
Nth< 0, TypeList > First
Definition: TypeList.h:133
static CommandContext::TargetFactory::SubstituteInUnique< InteractiveOutputTargets > scope
void append(std::basic_string< Elem > &dest, First &&first, Others &&...others)
static PreferencesResetHandler::Registration< OAuthServiceSettingsResetHandler > resetHandler
void WriteCommonFields(rapidjson::Document &document, std::string_view grantType, std::string_view scope)
void WriteAccessFields(rapidjson::Document &document, std::string_view grantType, std::string_view scope)
OAuthService & GetOAuthService()
Returns the instance of the OAuthService.
const ServiceConfig & GetServiceConfig()
Returns the instance of the ServiceConfig.
std::string ToUTF8(const std::wstring &wstr)
std::string UrlDecode(const std::string &url)
Definition: UrlDecode.cpp:18
Performs single-time global handler registration.
Definition: Prefs.h:570
Message that is sent when authorization state changes.
Definition: OAuthService.h:29