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
37{
38namespace
39{
40
41StringSetting refreshToken { L"/cloud/audiocom/refreshToken", "" };
42
43const std::string_view uriPrefix = "audacity://link";
44const std::string_view usernamePrefix = "username=";
45const std::string_view passwordPrefix = "password=";
46const std::string_view tokenPrefix = "token=";
47const std::string_view authorizationCodePrefix = "authorization_code=";
48
50 rapidjson::Document& document, std::string_view grantType, std::string_view scope)
51{
52 using namespace rapidjson;
53
54 document.AddMember(
55 "grant_type", StringRef(grantType.data(), grantType.size()),
56 document.GetAllocator());
57
58 const auto clientID = GetServiceConfig().GetOAuthClientID();
59 const auto clientSecret = GetServiceConfig().GetOAuthClientSecret();
60
61 document.AddMember(
62 "client_id",
63 Value(clientID.data(), clientID.size(), document.GetAllocator()),
64 document.GetAllocator());
65
66 document.AddMember(
67 "client_secret",
68 Value(clientSecret.data(), clientSecret.size(), document.GetAllocator()),
69 document.GetAllocator());
70
71 document.AddMember(
72 "scope", StringRef(scope.data(), scope.size()), document.GetAllocator());
73}
74
75} // namespace
76
78 std::function<void(std::string_view)> completedHandler, AudiocomTrace trace,
79 bool silent)
80{
82 {
83 if (completedHandler)
84 completedHandler(GetAccessToken());
85 return;
86 }
87
89 GetServiceConfig(), trace, std::move(completedHandler), silent);
90}
91
93 std::string_view uri, AudiocomTrace trace,
94 std::function<void(std::string_view)> completedHandler)
95{
97 {
98 if (completedHandler)
99 completedHandler({});
100 return false;
101 }
102
103 // It was observed, that sometimes link is passed as audacity://link/
104 // This is valid trace URI point of view, but we need to handle it separately
105 const auto argsStart = uri.find("?");
106
107 if (argsStart == std::string_view::npos)
108 {
109 if (completedHandler)
110 completedHandler({});
111 return false;
112 }
113
114 // Length is handled in IsPrefixed
115 auto args = uri.substr(argsStart + 1);
116
117 std::string_view token;
118 std::string_view username;
119 std::string_view password;
120 std::string_view authorizationCode;
121
122 while (!args.empty())
123 {
124 const auto nextArg = args.find('&');
125
126 const auto arg = args.substr(0, nextArg);
127 args = nextArg == std::string_view::npos ? "" : args.substr(nextArg + 1);
128
129 if (IsPrefixed(arg, usernamePrefix))
130 username = arg.substr(usernamePrefix.length());
131 else if (IsPrefixed(arg, passwordPrefix))
132 password = arg.substr(passwordPrefix.length());
133 else if (IsPrefixed(arg, tokenPrefix))
134 token = arg.substr(tokenPrefix.length());
135 else if (IsPrefixed(arg, authorizationCodePrefix))
136 authorizationCode = arg.substr(authorizationCodePrefix.length());
137 }
138
139 // We have a prioritized list of authorization methods
140 if (!authorizationCode.empty())
141 {
143 GetServiceConfig(), authorizationCode, trace,
144 std::move(completedHandler));
145 }
146 else if (!token.empty())
147 {
149 GetServiceConfig(), token, trace, std::move(completedHandler), false);
150 }
151 else if (!username.empty() && !password.empty())
152 {
154 GetServiceConfig(), audacity::UrlDecode(std::string(username)),
155 audacity::UrlDecode(std::string(password)), trace,
156 std::move(completedHandler));
157 }
158 else
159 {
160 if (completedHandler)
161 completedHandler({});
162
163 return false;
164 }
165
166 return true;
167}
168
170{
171 std::lock_guard<std::recursive_mutex> lock(mMutex);
172
173 mAccessToken.clear();
175 gPrefs->Flush();
176
177 // Unlink account is expected to be called only
178 // on UI thread
179 Publish({ {}, {}, trace, false });
180}
181
183 const ServiceConfig& config, std::string_view userName,
184 std::string_view password, AudiocomTrace trace,
185 std::function<void(std::string_view)> completedHandler)
186{
187 using namespace rapidjson;
188
189 Document document;
190 document.SetObject();
191
192 WriteCommonFields(document, "password", "all");
193
194 document.AddMember(
195 "username", StringRef(userName.data(), userName.size()),
196 document.GetAllocator());
197
198 document.AddMember(
199 "password", StringRef(password.data(), password.size()),
200 document.GetAllocator());
201
202 rapidjson::StringBuffer buffer;
203 rapidjson::Writer<rapidjson::StringBuffer> writer(buffer);
204 document.Accept(writer);
205
207 config, { buffer.GetString(), buffer.GetSize() }, trace,
208 std::move(completedHandler), false);
209}
210
212 const ServiceConfig& config, std::string_view token, AudiocomTrace trace,
213 std::function<void(std::string_view)> completedHandler, bool silent)
214{
215 using namespace rapidjson;
216
217 Document document;
218 document.SetObject();
219
220 WriteCommonFields(document, "refresh_token", "");
221
222 document.AddMember(
223 "refresh_token", StringRef(token.data(), token.size()),
224 document.GetAllocator());
225
226 rapidjson::StringBuffer buffer;
227 rapidjson::Writer<rapidjson::StringBuffer> writer(buffer);
228 document.Accept(writer);
229
231 config, { buffer.GetString(), buffer.GetSize() }, trace,
232 std::move(completedHandler), silent);
233}
234
236 const ServiceConfig& config, AudiocomTrace trace,
237 std::function<void(std::string_view)> completedHandler, bool silent)
238{
239 std::lock_guard<std::recursive_mutex> lock(mMutex);
240
242 config, audacity::ToUTF8(refreshToken.Read()), trace,
243 std::move(completedHandler), silent);
244}
245
247 const ServiceConfig& config, std::string_view authorizationCode,
248 AudiocomTrace trace, std::function<void(std::string_view)> completedHandler)
249{
250 using namespace rapidjson;
251
252 Document document;
253 document.SetObject();
254
255 WriteCommonFields(document, "authorization_code", "all");
256
257 document.AddMember(
258 "code", StringRef(authorizationCode.data(), authorizationCode.size()),
259 document.GetAllocator());
260
261 const auto redirectURI = config.GetOAuthRedirectURL();
262
263 document.AddMember(
264 "redirect_uri", StringRef(redirectURI.data(), redirectURI.size()),
265 document.GetAllocator());
266
267 rapidjson::StringBuffer buffer;
268 rapidjson::Writer<rapidjson::StringBuffer> writer(buffer);
269 document.Accept(writer);
270
272 config, { buffer.GetString(), buffer.GetSize() }, trace,
273 std::move(completedHandler), false);
274}
275
277{
278 return !GetAccessToken().empty();
279}
280
282{
283 std::lock_guard<std::recursive_mutex> lock(mMutex);
284 return !refreshToken.Read().empty();
285}
286
288{
289 std::lock_guard<std::recursive_mutex> lock(mMutex);
290
291 if (Clock::now() < mTokenExpirationTime)
292 return mAccessToken;
293
294 return {};
295}
296
298 const ServiceConfig& config, std::string_view payload, AudiocomTrace trace,
299 std::function<void(std::string_view)> completedHandler, bool silent)
300{
301 using namespace audacity::network_manager;
302
303 Request request(config.GetAPIUrl("/auth/token"));
304
305 request.setHeader(
307
308 request.setHeader(
310
311 auto response = NetworkManager::GetInstance().doPost(
312 request, payload.data(), payload.size());
313
314 response->setRequestFinishedCallback(
315 [response, this, handler = std::move(completedHandler), silent,
316 trace](auto) {
317 const auto httpCode = response->getHTTPCode();
318 const auto body = response->readAll<std::string>();
319
320 if (httpCode != 200)
321 {
322 if (handler)
323 handler({});
324
325 // Token has expired?
326 if (httpCode == 422)
327 BasicUI::CallAfter([this, trace] { UnlinkAccount(trace); });
328 else
329 SafePublish({ {}, body, trace, false, silent });
330
331 return;
332 }
333
334 rapidjson::Document document;
335 document.Parse(body.data(), body.size());
336
337 if (!document.IsObject())
338 {
339 if (handler)
340 handler({});
341
342 SafePublish({ {}, body, trace, false, silent });
343 return;
344 }
345
346 const auto tokenType = document["token_type"].GetString();
347 const auto accessToken = document["access_token"].GetString();
348 const auto expiresIn = document["expires_in"].GetInt64();
349 const auto newRefreshToken = document["refresh_token"].GetString();
350
351 {
352 std::lock_guard<std::recursive_mutex> lock(mMutex);
353
354 mAccessToken = std::string(tokenType) + " " + accessToken;
356 Clock::now() + std::chrono::seconds(expiresIn);
357 }
358
360 [token = std::string(newRefreshToken)]()
361 {
362 // At this point access token is already written,
363 // only refresh token is updated.
364 refreshToken.Write(token);
365 gPrefs->Flush();
366 });
367
368 if (handler)
370
371 // The callback only needs the access token, so invoke it immediately.
372 // Networking is thread safe
373 SafePublish({ mAccessToken, {}, trace, true, silent });
374 });
375}
376
378{
379 BasicUI::CallAfter([this, message]() { Publish(message); });
380}
381
383{
384 static OAuthService service;
385 return service;
386}
387
388namespace
389{
390
392{
393public:
394 void OnSettingResetBegin() override
395 {
396 }
397
398 void OnSettingResetEnd() override
399 {
402 }
403};
404
407}
408
409} // 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.
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
bool HandleLinkURI(std::string_view uri, AudiocomTrace, std::function< void(std::string_view)> completedHandler)
Handle the OAuth callback.
void AuthoriseRefreshToken(const ServiceConfig &config, std::string_view refreshToken, AudiocomTrace, std::function< void(std::string_view)> completedHandler, bool silent)
void SafePublish(const AuthStateChangedMessage &message)
void DoAuthorise(const ServiceConfig &config, std::string_view payload, AudiocomTrace, std::function< void(std::string_view)> completedHandler, bool silent)
void ValidateAuth(std::function< void(std::string_view)> 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.
void AuthoriseCode(const ServiceConfig &config, std::string_view authorizationCode, AudiocomTrace, std::function< void(std::string_view)> completedHandler)
bool HasAccessToken() const
Indicates, that service has a valid access token, i. e. that the user is authorized.
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)
static CommandContext::TargetFactory::SubstituteInUnique< InteractiveOutputTargets > scope
static PreferencesResetHandler::Registration< OAuthServiceSettingsResetHandler > resetHandler
void WriteCommonFields(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