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
57 document.AddMember(
58 "client_id", StringRef(clientID.data(), clientID.size()),
59 document.GetAllocator());
60
61 document.AddMember(
62 "client_secret", StringRef("shKqnY2sLTfRK7hztwzNEVxnmhJfOy1i"),
63 document.GetAllocator());
64
65 document.AddMember(
66 "scope", StringRef(scope.data(), scope.size()), document.GetAllocator());
67}
68
69bool IsPrefixed(std::string_view hay, std::string_view prefix)
70{
71 if (hay.length() < prefix.length())
72 return false;
73
74 return std::mismatch(
75 prefix.begin(), prefix.end(), hay.begin(),
76 [](auto a, auto b) { return a == std::tolower(b); })
77 .first == prefix.end();
78}
79
80} // namespace
81
83 std::function<void(std::string_view)> completedHandler)
84{
86 {
87 if (completedHandler)
88 completedHandler(GetAccessToken());
89 return;
90 }
91
92 AuthoriseRefreshToken(GetServiceConfig(), std::move(completedHandler));
93}
94
96 std::string_view uri, std::function<void(std::string_view)> completedHandler)
97{
98 if (!IsPrefixed(uri, uriPrefix))
99 {
100 if (completedHandler)
101 completedHandler({});
102 return;
103 }
104
105 // It was observed, that sometimes link is passed as audacity://link/
106 // This is valid from URI point of view, but we need to handle it separately
107 const auto argsStart = uri.find("?");
108
109 if (argsStart == std::string_view::npos)
110 {
111 if (completedHandler)
112 completedHandler({});
113 return;
114 }
115
116 // Length is handled in IsPrefixed
117 auto args = uri.substr(argsStart + 1);
118
119 std::string_view token;
120 std::string_view username;
121 std::string_view password;
122 std::string_view authorizationCode;
123
124 while (!args.empty())
125 {
126 const auto nextArg = args.find('&');
127
128 const auto arg = args.substr(0, nextArg);
129 args = nextArg == std::string_view::npos ? "" : args.substr(nextArg + 1);
130
131 if (IsPrefixed(arg, usernamePrefix))
132 username = arg.substr(usernamePrefix.length());
133 else if (IsPrefixed(arg, passwordPrefix))
134 password = arg.substr(passwordPrefix.length());
135 else if (IsPrefixed(arg, tokenPrefix))
136 token = arg.substr(tokenPrefix.length());
137 else if (IsPrefixed(arg, authorizationCodePrefix))
138 authorizationCode = arg.substr(authorizationCodePrefix.length());
139 }
140
141 // We have a prioritized list of authorization methods
142 if (!authorizationCode.empty())
143 {
145 GetServiceConfig(), authorizationCode, std::move(completedHandler));
146 }
147 else if (!token.empty())
148 {
150 GetServiceConfig(), token, std::move(completedHandler));
151 }
152 else if (!username.empty() && !password.empty())
153 {
156 audacity::UrlDecode(std::string(username)),
157 audacity::UrlDecode(std::string(password)),
158 std::move(completedHandler));
159 }
160 else
161 {
162 if (completedHandler)
163 completedHandler({});
164 }
165}
166
168{
169 std::lock_guard<std::recursive_mutex> lock(mMutex);
170
171 mAccessToken.clear();
173 gPrefs->Flush();
174
175 // Unlink account is expected to be called only
176 // on UI thread
177 Publish({ {}, {}, false });
178}
179
181 const ServiceConfig& config, std::string_view userName,
182 std::string_view password,
183 std::function<void(std::string_view)> completedHandler)
184{
185 using namespace rapidjson;
186
187 Document document;
188 document.SetObject();
189
190 WriteCommonFields(document, "password", "all");
191
192 document.AddMember(
193 "username", StringRef(userName.data(), userName.size()),
194 document.GetAllocator());
195
196 document.AddMember(
197 "password", StringRef(password.data(), password.size()),
198 document.GetAllocator());
199
200 rapidjson::StringBuffer buffer;
201 rapidjson::Writer<rapidjson::StringBuffer> writer(buffer);
202 document.Accept(writer);
203
205 config, { buffer.GetString(), buffer.GetSize() },
206 std::move(completedHandler));
207}
208
210 const ServiceConfig& config, std::string_view token,
211 std::function<void(std::string_view)> completedHandler)
212{
213 using namespace rapidjson;
214
215 Document document;
216 document.SetObject();
217
218 WriteCommonFields(document, "refresh_token", "");
219
220 document.AddMember(
221 "refresh_token", StringRef(token.data(), token.size()),
222 document.GetAllocator());
223
224 rapidjson::StringBuffer buffer;
225 rapidjson::Writer<rapidjson::StringBuffer> writer(buffer);
226 document.Accept(writer);
227
229 config, { buffer.GetString(), buffer.GetSize() },
230 std::move(completedHandler));
231}
232
234 const ServiceConfig& config,
235 std::function<void(std::string_view)> completedHandler)
236{
237 std::lock_guard<std::recursive_mutex> lock(mMutex);
238
241 std::move(completedHandler));
242}
243
245 const ServiceConfig& config, std::string_view authorizationCode,
246 std::function<void(std::string_view)> completedHandler)
247{
248 using namespace rapidjson;
249
250 Document document;
251 document.SetObject();
252
253 WriteCommonFields(document, "authorization_code", "all");
254
255 document.AddMember(
256 "code", StringRef(authorizationCode.data(), authorizationCode.size()),
257 document.GetAllocator());
258
259 const auto redirectURI = config.GetOAuthRedirectURL();
260
261 document.AddMember(
262 "redirect_uri", StringRef(redirectURI.data(), redirectURI.size()),
263 document.GetAllocator());
264
265 rapidjson::StringBuffer buffer;
266 rapidjson::Writer<rapidjson::StringBuffer> writer(buffer);
267 document.Accept(writer);
268
270 config, { buffer.GetString(), buffer.GetSize() },
271 std::move(completedHandler));
272}
273
275{
276 return !GetAccessToken().empty();
277}
278
280{
281 std::lock_guard<std::recursive_mutex> lock(mMutex);
282 return !refreshToken.Read().empty();
283}
284
286{
287 std::lock_guard<std::recursive_mutex> lock(mMutex);
288
289 if (Clock::now() < mTokenExpirationTime)
290 return mAccessToken;
291
292 return {};
293}
294
296 const ServiceConfig& config, std::string_view payload,
297 std::function<void(std::string_view)> completedHandler)
298{
299 using namespace audacity::network_manager;
300
301 Request request(config.GetAPIUrl("/auth/token"));
302
303 request.setHeader(
305
306 request.setHeader(
308
309 auto response = NetworkManager::GetInstance().doPost(
310 request, payload.data(), payload.size());
311
312 response->setRequestFinishedCallback(
313 [response, this, handler = std::move(completedHandler)](auto)
314 {
315 const auto httpCode = response->getHTTPCode();
316 const auto body = response->readAll<std::string>();
317
318 if (httpCode != 200)
319 {
320 if (handler)
321 handler({});
322
323 // Token has expired?
324 if (httpCode == 422)
325 BasicUI::CallAfter([this] { UnlinkAccount(); });
326 else
327 SafePublish({ {}, body, false });
328
329 return;
330 }
331
332 rapidjson::Document document;
333 document.Parse(body.data(), body.size());
334
335 if (!document.IsObject())
336 {
337 if (handler)
338 handler({});
339
340 SafePublish({ {}, body, false });
341 return;
342 }
343
344 const auto tokenType = document["token_type"].GetString();
345 const auto accessToken = document["access_token"].GetString();
346 const auto expiresIn = document["expires_in"].GetInt64();
347 const auto newRefreshToken = document["refresh_token"].GetString();
348
349 {
350 std::lock_guard<std::recursive_mutex> lock(mMutex);
351
352 mAccessToken = std::string(tokenType) + " " + accessToken;
354 Clock::now() + std::chrono::seconds(expiresIn);
355 }
356
358 [token = std::string(newRefreshToken)]()
359 {
360 // At this point access token is already written,
361 // only refresh token is updated.
362 refreshToken.Write(token);
363 gPrefs->Flush();
364 });
365
366 if (handler)
368
369 // The callback only needs the access token, so invoke it immediately.
370 // Networking is thread safe
371 SafePublish({ mAccessToken, {}, true });
372 });
373}
374
376{
377 BasicUI::CallAfter([this, message]() { Publish(message); });
378}
379
381{
382 static OAuthService service;
383 return service;
384}
385} // namespace cloud::audiocom
static AudioUnitEffectsModule::Factory::SubstituteInUnique< AudioUnitEffect > scope
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.
FileConfig * gPrefs
Definition: Prefs.cpp:70
Declare a class for constructing HTTP requests.
Declare a function to decode an URL encode string.
virtual bool Flush(bool bCurrentOnly=false) wxOVERRIDE
Definition: FileConfig.cpp:143
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:252
bool Read(T *pVar) const
overload of Read returning a boolean that is true if the value was previously defined *‍/
Definition: Prefs.h:200
Specialization of Setting for strings.
Definition: Prefs.h:363
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:24
std::string_view 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_view GetOAuthClientID() const
OAuth2 client ID.
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
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