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
33namespace cloud::audiocom
34{
35namespace
36{
37
38StringSetting refreshToken { L"/cloud/audiocom/refreshToken", "" };
39
40const std::string_view uriPrefix = "audacity://link";
41const std::string_view usernamePrefix = "username=";
42const std::string_view passwordPrefix = "password=";
43const std::string_view tokenPrefix = "token=";
44const std::string_view authorizationCodePrefix = "authorization_code=";
45
47 rapidjson::Document& document, std::string_view grantType, std::string_view scope)
48{
49 using namespace rapidjson;
50
51 document.AddMember(
52 "grant_type", StringRef(grantType.data(), grantType.size()),
53 document.GetAllocator());
54
55 const auto clientID = GetServiceConfig().GetOAuthClientID();
56 const auto clientSecret = GetServiceConfig().GetOAuthClientSecret();
57
58 document.AddMember(
59 "client_id",
60 Value(clientID.data(), clientID.size(), document.GetAllocator()),
61 document.GetAllocator());
62
63 document.AddMember(
64 "client_secret",
65 Value(clientSecret.data(), clientSecret.size(), document.GetAllocator()),
66 document.GetAllocator());
67
68 document.AddMember(
69 "scope", StringRef(scope.data(), scope.size()), document.GetAllocator());
70}
71
72bool IsPrefixed(std::string_view hay, std::string_view prefix)
73{
74 if (hay.length() < prefix.length())
75 return false;
76
77 return std::mismatch(
78 prefix.begin(), prefix.end(), hay.begin(),
79 [](auto a, auto b) { return a == std::tolower(b); })
80 .first == prefix.end();
81}
82
83} // namespace
84
86 std::function<void(std::string_view)> completedHandler)
87{
89 {
90 if (completedHandler)
91 completedHandler(GetAccessToken());
92 return;
93 }
94
95 AuthoriseRefreshToken(GetServiceConfig(), std::move(completedHandler));
96}
97
99 std::string_view uri, std::function<void(std::string_view)> completedHandler)
100{
101 if (!IsPrefixed(uri, uriPrefix))
102 {
103 if (completedHandler)
104 completedHandler({});
105 return;
106 }
107
108 // It was observed, that sometimes link is passed as audacity://link/
109 // This is valid from URI point of view, but we need to handle it separately
110 const auto argsStart = uri.find("?");
111
112 if (argsStart == std::string_view::npos)
113 {
114 if (completedHandler)
115 completedHandler({});
116 return;
117 }
118
119 // Length is handled in IsPrefixed
120 auto args = uri.substr(argsStart + 1);
121
122 std::string_view token;
123 std::string_view username;
124 std::string_view password;
125 std::string_view authorizationCode;
126
127 while (!args.empty())
128 {
129 const auto nextArg = args.find('&');
130
131 const auto arg = args.substr(0, nextArg);
132 args = nextArg == std::string_view::npos ? "" : args.substr(nextArg + 1);
133
134 if (IsPrefixed(arg, usernamePrefix))
135 username = arg.substr(usernamePrefix.length());
136 else if (IsPrefixed(arg, passwordPrefix))
137 password = arg.substr(passwordPrefix.length());
138 else if (IsPrefixed(arg, tokenPrefix))
139 token = arg.substr(tokenPrefix.length());
140 else if (IsPrefixed(arg, authorizationCodePrefix))
141 authorizationCode = arg.substr(authorizationCodePrefix.length());
142 }
143
144 // We have a prioritized list of authorization methods
145 if (!authorizationCode.empty())
146 {
148 GetServiceConfig(), authorizationCode, std::move(completedHandler));
149 }
150 else if (!token.empty())
151 {
153 GetServiceConfig(), token, std::move(completedHandler));
154 }
155 else if (!username.empty() && !password.empty())
156 {
159 audacity::UrlDecode(std::string(username)),
160 audacity::UrlDecode(std::string(password)),
161 std::move(completedHandler));
162 }
163 else
164 {
165 if (completedHandler)
166 completedHandler({});
167 }
168}
169
171{
172 std::lock_guard<std::recursive_mutex> lock(mMutex);
173
174 mAccessToken.clear();
176 gPrefs->Flush();
177
178 // Unlink account is expected to be called only
179 // on UI thread
180 Publish({ {}, {}, false });
181}
182
184 const ServiceConfig& config, std::string_view userName,
185 std::string_view password,
186 std::function<void(std::string_view)> completedHandler)
187{
188 using namespace rapidjson;
189
190 Document document;
191 document.SetObject();
192
193 WriteCommonFields(document, "password", "all");
194
195 document.AddMember(
196 "username", StringRef(userName.data(), userName.size()),
197 document.GetAllocator());
198
199 document.AddMember(
200 "password", StringRef(password.data(), password.size()),
201 document.GetAllocator());
202
203 rapidjson::StringBuffer buffer;
204 rapidjson::Writer<rapidjson::StringBuffer> writer(buffer);
205 document.Accept(writer);
206
208 config, { buffer.GetString(), buffer.GetSize() },
209 std::move(completedHandler));
210}
211
213 const ServiceConfig& config, std::string_view token,
214 std::function<void(std::string_view)> completedHandler)
215{
216 using namespace rapidjson;
217
218 Document document;
219 document.SetObject();
220
221 WriteCommonFields(document, "refresh_token", "");
222
223 document.AddMember(
224 "refresh_token", StringRef(token.data(), token.size()),
225 document.GetAllocator());
226
227 rapidjson::StringBuffer buffer;
228 rapidjson::Writer<rapidjson::StringBuffer> writer(buffer);
229 document.Accept(writer);
230
232 config, { buffer.GetString(), buffer.GetSize() },
233 std::move(completedHandler));
234}
235
237 const ServiceConfig& config,
238 std::function<void(std::string_view)> completedHandler)
239{
240 std::lock_guard<std::recursive_mutex> lock(mMutex);
241
244 std::move(completedHandler));
245}
246
248 const ServiceConfig& config, std::string_view authorizationCode,
249 std::function<void(std::string_view)> completedHandler)
250{
251 using namespace rapidjson;
252
253 Document document;
254 document.SetObject();
255
256 WriteCommonFields(document, "authorization_code", "all");
257
258 document.AddMember(
259 "code", StringRef(authorizationCode.data(), authorizationCode.size()),
260 document.GetAllocator());
261
262 const auto redirectURI = config.GetOAuthRedirectURL();
263
264 document.AddMember(
265 "redirect_uri", StringRef(redirectURI.data(), redirectURI.size()),
266 document.GetAllocator());
267
268 rapidjson::StringBuffer buffer;
269 rapidjson::Writer<rapidjson::StringBuffer> writer(buffer);
270 document.Accept(writer);
271
273 config, { buffer.GetString(), buffer.GetSize() },
274 std::move(completedHandler));
275}
276
278{
279 return !GetAccessToken().empty();
280}
281
283{
284 std::lock_guard<std::recursive_mutex> lock(mMutex);
285 return !refreshToken.Read().empty();
286}
287
289{
290 std::lock_guard<std::recursive_mutex> lock(mMutex);
291
292 if (Clock::now() < mTokenExpirationTime)
293 return mAccessToken;
294
295 return {};
296}
297
299 const ServiceConfig& config, std::string_view payload,
300 std::function<void(std::string_view)> completedHandler)
301{
302 using namespace audacity::network_manager;
303
304 Request request(config.GetAPIUrl("/auth/token"));
305
306 request.setHeader(
308
309 request.setHeader(
311
312 auto response = NetworkManager::GetInstance().doPost(
313 request, payload.data(), payload.size());
314
315 response->setRequestFinishedCallback(
316 [response, this, handler = std::move(completedHandler)](auto)
317 {
318 const auto httpCode = response->getHTTPCode();
319 const auto body = response->readAll<std::string>();
320
321 if (httpCode != 200)
322 {
323 if (handler)
324 handler({});
325
326 // Token has expired?
327 if (httpCode == 422)
328 BasicUI::CallAfter([this] { UnlinkAccount(); });
329 else
330 SafePublish({ {}, body, false });
331
332 return;
333 }
334
335 rapidjson::Document document;
336 document.Parse(body.data(), body.size());
337
338 if (!document.IsObject())
339 {
340 if (handler)
341 handler({});
342
343 SafePublish({ {}, body, false });
344 return;
345 }
346
347 const auto tokenType = document["token_type"].GetString();
348 const auto accessToken = document["access_token"].GetString();
349 const auto expiresIn = document["expires_in"].GetInt64();
350 const auto newRefreshToken = document["refresh_token"].GetString();
351
352 {
353 std::lock_guard<std::recursive_mutex> lock(mMutex);
354
355 mAccessToken = std::string(tokenType) + " " + accessToken;
357 Clock::now() + std::chrono::seconds(expiresIn);
358 }
359
361 [token = std::string(newRefreshToken)]()
362 {
363 // At this point access token is already written,
364 // only refresh token is updated.
365 refreshToken.Write(token);
366 gPrefs->Flush();
367 });
368
369 if (handler)
371
372 // The callback only needs the access token, so invoke it immediately.
373 // Networking is thread safe
374 SafePublish({ mAccessToken, {}, true });
375 });
376}
377
379{
380 BasicUI::CallAfter([this, message]() { Publish(message); });
381}
382
384{
385 static OAuthService service;
386 return service;
387}
388} // namespace 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.
Declare a function to decode an URL encode string.
CallbackReturn Publish(const AuthStateChangedMessage &message)
Send a message to connected callbacks.
Definition: Observer.h:207
bool Write(const T &value)
Write value to config and return true if successful.
Definition: Prefs.h:257
bool Read(T *pVar) const
overload of Read returning a boolean that is true if the value was previously defined *‍/
Definition: Prefs.h:205
Specialization of Setting for strings.
Definition: Prefs.h:368
virtual bool Flush() noexcept=0
Request & setHeader(const std::string &name, std::string value)
Definition: Request.cpp:46
Service responsible for OAuth authentication against the audio.com service.
Definition: OAuthService.h:38
void AuthoriseCode(const ServiceConfig &config, std::string_view authorizationCode, std::function< void(std::string_view)> completedHandler)
std::string GetAccessToken() const
Return the current access token, if any.
void AuthoriseRefreshToken(const ServiceConfig &config, std::string_view refreshToken, std::function< void(std::string_view)> completedHandler)
void DoAuthorise(const ServiceConfig &config, std::string_view payload, std::function< void(std::string_view)> completedHandler)
void UnlinkAccount()
Removes access and refresh token, notifies about the logout.
void HandleLinkURI(std::string_view uri, std::function< void(std::string_view)> completedHandler)
Handle the OAuth callback.
std::recursive_mutex mMutex
Definition: OAuthService.h:104
Clock::time_point mTokenExpirationTime
Definition: OAuthService.h:106
void SafePublish(const AuthStateChangedMessage &message)
void AuthorisePassword(const ServiceConfig &config, std::string_view userName, std::string_view password, 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.
void ValidateAuth(std::function< void(std::string_view)> completedHandler)
Attempt to authorize the user.
Configuration for the audio.com.
Definition: ServiceConfig.h:23
std::string GetOAuthClientID() const
OAuth2 client ID.
std::string GetOAuthRedirectURL() const
OAuth2 redirect URL. Only used to satisfy the protocol.
std::string GetAPIUrl(std::string_view apiURI) const
Helper to construct the full URLs for the API.
std::string GetOAuthClientSecret() const
OAuth2 client secret.
void CallAfter(Action action)
Schedule an action to be done later, and in the main thread.
Definition: BasicUI.cpp:208
constexpr size_t npos(-1)
FrameStatistics & GetInstance() noexcept
static CommandContext::TargetFactory::SubstituteInUnique< InteractiveOutputTargets > scope
std::string ToUTF8(const std::wstring &wstr)
std::string UrlDecode(const std::string &url)
Definition: UrlDecode.cpp:18
void WriteCommonFields(rapidjson::Document &document, std::string_view grantType, std::string_view scope)
bool IsPrefixed(std::string_view hay, std::string_view prefix)
const ServiceConfig & GetServiceConfig()
Returns the instance of the ServiceConfig.
OAuthService & GetOAuthService()
Returns the instance of the OAuthService.
Message that is sent when authorization state changes.
Definition: OAuthService.h:26