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