dropbox_sdk/oauth2.rs
1// Copyright (c) 2019-2025 Dropbox, Inc.
2
3//! Helpers for requesting OAuth2 tokens.
4//!
5//! OAuth2 has a few possible ways to authenticate, and the right choice depends on how your app
6//! operates and is deployed.
7//!
8//! For an overview, see the [Dropbox OAuth Guide].
9//!
10//! For quick recommendations based on the type of app you have, see the [OAuth types summary].
11//!
12//! [Dropbox OAuth Guide]: https://developers.dropbox.com/oauth-guide
13//! [OAuth types summary]: https://developers.dropbox.com/oauth-guide#summary
14
15use crate::async_client_trait::NoauthClient;
16use crate::client_helpers::{parse_response, prepare_request};
17use crate::client_trait_common::{Endpoint, ParamsType, Style};
18use crate::Error;
19use async_lock::RwLock;
20use base64::engine::general_purpose::{URL_SAFE, URL_SAFE_NO_PAD};
21use base64::Engine;
22use ring::rand::{SecureRandom, SystemRandom};
23use std::env;
24use std::io::{self, IsTerminal, Write};
25use std::sync::Arc;
26use url::form_urlencoded::Serializer as UrlEncoder;
27use url::Url;
28
29/// Which type of OAuth2 flow to use.
30#[derive(Debug, Clone)]
31pub enum Oauth2Type {
32 /// The Authorization Code flow yields a temporary authorization code which must be turned into
33 /// an OAuth2 token by making another call. The authorization page can do a web redirect back to
34 /// your app with the code (if it is a server-side app), or can be used without a redirect URI,
35 /// in which case the authorization page displays the authorization code to the user and they
36 /// must then input the code manually into the program.
37 AuthorizationCode {
38 /// Client secret
39 client_secret: String,
40 },
41
42 /// The PKCE flow is an extension of the Authorization Code flow which uses dynamically
43 /// generated codes instead of an app secret to perform the OAuth exchange. This both avoids
44 /// having a hardcoded secret in the app (useful for client-side / mobile apps) and also ensures
45 /// that the authorization code can only be used by the client.
46 PKCE(PkceCode),
47
48 /// In Implicit Grant flow, the authorization page directly includes an OAuth2 token when it
49 /// redirects the user's web browser back to your program, and no separate call to generate a
50 /// token is needed. This can ONLY be used with a redirect URI.
51 ///
52 /// This flow is considered "legacy" and is not as secure as the other flows.
53 ImplicitGrant,
54}
55
56impl Oauth2Type {
57 /// The value to put in the "response_type" parameter to request the given token type.
58 pub(crate) fn response_type_str(&self) -> &'static str {
59 match self {
60 Oauth2Type::AuthorizationCode { .. } | Oauth2Type::PKCE { .. } => "code",
61 Oauth2Type::ImplicitGrant => "token",
62 }
63 }
64}
65
66/// What type of access token is requested? If unsure, ShortLivedAndRefresh is probably what you
67/// want.
68#[derive(Debug, Copy, Clone)]
69pub enum TokenType {
70 /// Return a short-lived bearer token and a long-lived refresh token that can be used to
71 /// generate new bearer tokens in the future (as long as a user's approval remains valid).
72 /// This is the default type for this SDK.
73 ShortLivedAndRefresh,
74
75 /// Return just the short-lived bearer token, without refresh token. The app will have to start
76 /// the authorization flow again to obtain a new token.
77 ShortLived,
78
79 /// Return a long-lived bearer token. The app must be allowed to do this in the Dropbox app
80 /// console. This capability will be removed in the future.
81 #[deprecated]
82 LongLived,
83}
84
85impl TokenType {
86 /// The value to put in the `token_access_type` parameter. If `None`, the parameter is omitted
87 /// entirely.
88 pub(crate) fn token_access_type_str(self) -> Option<&'static str> {
89 match self {
90 TokenType::ShortLivedAndRefresh => Some("offline"),
91 TokenType::ShortLived => Some("online"),
92 #[allow(deprecated)]
93 TokenType::LongLived => None,
94 }
95 }
96}
97
98/// A proof key for OAuth2 PKCE ("Proof Key for Code Exchange") flow.
99#[derive(Debug, Clone)]
100pub struct PkceCode {
101 /// The value of the code key.
102 pub code: String,
103}
104
105impl PkceCode {
106 /// Generate a new random code string.
107 #[allow(clippy::new_without_default)]
108 pub fn new() -> Self {
109 // Spec lets us use [a-zA-Z0-9._~-] as the alphabet, and a length between 43 and 128.
110 // A 93-byte input ends up as 125 base64 characters, so let's do that.
111 let mut bytes = [0u8; 93];
112 // not expecting this to ever actually fail:
113 SystemRandom::new()
114 .fill(&mut bytes)
115 .expect("failed to get random bytes for PKCE");
116 let code = URL_SAFE.encode(bytes);
117 Self { code }
118 }
119
120 /// Get the SHA-256 hash as a base64-encoded string.
121 pub fn s256(&self) -> String {
122 let digest = ring::digest::digest(&ring::digest::SHA256, self.code.as_bytes());
123 URL_SAFE_NO_PAD.encode(digest.as_ref())
124 }
125}
126
127/// Builds a URL that can be given to the user to visit to have Dropbox authorize your app.
128///
129/// If this app is a server-side app, you should redirect the user's browser to this URL to begin
130/// the authorization, and set the redirect_uri to bring the user back to your site when done.
131///
132/// If this app is a client-side app, you should open a web browser with this URL to begin the
133/// authorization, and set the redirect_uri to bring the user back to your app.
134///
135/// As a special case, if your app is a command-line application, you can skip setting the
136/// redirect_uri and print this URL and instruct the user to open it in a browser. When they
137/// complete the authorization, they will be given an auth code to input back into your app.
138///
139/// If you are using the deprecated Implicit Grant flow, the redirect after authentication will
140/// provide you an OAuth2 token. In all other cases, you will have an authorization code, and you
141/// must call make another call to obtain a token. See [`Authorization`], which is used to do this.
142#[derive(Debug)]
143pub struct AuthorizeUrlBuilder<'a> {
144 client_id: &'a str,
145 flow_type: &'a Oauth2Type,
146 token_type: TokenType,
147 force_reapprove: bool,
148 force_reauthentication: bool,
149 disable_signup: bool,
150 redirect_uri: Option<&'a str>,
151 state: Option<&'a str>,
152 require_role: Option<&'a str>,
153 locale: Option<&'a str>,
154 scope: Option<&'a str>,
155}
156
157impl<'a> AuthorizeUrlBuilder<'a> {
158 /// Return a new builder for the given client ID and auth flow type, with all fields set to
159 /// defaults.
160 pub fn new(client_id: &'a str, flow_type: &'a Oauth2Type) -> Self {
161 Self {
162 client_id,
163 flow_type,
164 token_type: TokenType::ShortLivedAndRefresh,
165 force_reapprove: false,
166 force_reauthentication: false,
167 disable_signup: false,
168 redirect_uri: None,
169 state: None,
170 require_role: None,
171 locale: None,
172 scope: None,
173 }
174 }
175
176 /// Set whether the user should be prompted to approve the request regardless of whether they
177 /// have approved it before.
178 pub fn force_reapprove(mut self, value: bool) -> Self {
179 self.force_reapprove = value;
180 self
181 }
182
183 /// Set whether the user should have to re-login when approving the request.
184 pub fn force_reauthentication(mut self, value: bool) -> Self {
185 self.force_reauthentication = value;
186 self
187 }
188
189 /// Set whether new user signups should be allowed or not while approving the request.
190 pub fn disable_signup(mut self, value: bool) -> Self {
191 self.disable_signup = value;
192 self
193 }
194
195 /// Set the URI the approve request should redirect the user to when completed.
196 /// If no redirect URI is specified, the user will be shown the code directly and will have to
197 /// manually input it into your app.
198 pub fn redirect_uri(mut self, value: &'a str) -> Self {
199 self.redirect_uri = Some(value);
200 self
201 }
202
203 /// Up to 500 bytes of arbitrary data that will be passed back to your redirect URI. This
204 /// parameter should be used to protect against cross-site request forgery (CSRF).
205 pub fn state(mut self, value: &'a str) -> Self {
206 self.state = Some(value);
207 self
208 }
209
210 /// If this parameter is specified, the user will be asked to authorize with a particular type
211 /// of Dropbox account, either `work` for a team account or `personal` for a personal account.
212 /// Your app should still verify the type of Dropbox account after authorization since the user
213 /// could modify or remove the require_role parameter.
214 pub fn require_role(mut self, value: &'a str) -> Self {
215 self.require_role = Some(value);
216 self
217 }
218
219 /// Force a specific locale when prompting the user, instead of the locale indicated by their
220 /// browser.
221 pub fn locale(mut self, value: &'a str) -> Self {
222 self.locale = Some(value);
223 self
224 }
225
226 /// What type of token should be requested. Defaults to [`TokenType::ShortLivedAndRefresh`].
227 pub fn token_type(mut self, value: TokenType) -> Self {
228 self.token_type = value;
229 self
230 }
231
232 /// This parameter allows your user to authorize a subset of the scopes selected in the
233 /// App Console. Multiple scopes are separated by a space. If this parameter is omitted, the
234 /// authorization page will request all scopes selected on the Permissions tab.
235 pub fn scope(mut self, value: &'a str) -> Self {
236 self.scope = Some(value);
237 self
238 }
239
240 /// Build the OAuth2 authorization URL from the previously given parameters.
241 pub fn build(self) -> Url {
242 let mut url = Url::parse("https://www.dropbox.com/oauth2/authorize").unwrap();
243 {
244 let mut params = url.query_pairs_mut();
245 params.append_pair("response_type", self.flow_type.response_type_str());
246 params.append_pair("client_id", self.client_id);
247 if let Some(val) = self.token_type.token_access_type_str() {
248 params.append_pair("token_access_type", val);
249 }
250 if self.force_reapprove {
251 params.append_pair("force_reapprove", "true");
252 }
253 if self.force_reauthentication {
254 params.append_pair("force_reauthentication", "true");
255 }
256 if self.disable_signup {
257 params.append_pair("disable_signup", "true");
258 }
259 if let Some(value) = self.redirect_uri {
260 params.append_pair("redirect_uri", value);
261 }
262 if let Some(value) = self.state {
263 params.append_pair("state", value);
264 }
265 if let Some(value) = self.require_role {
266 params.append_pair("require_role", value);
267 }
268 if let Some(value) = self.locale {
269 params.append_pair("locale", value);
270 }
271 if let Some(value) = self.scope {
272 params.append_pair("scope", value);
273 }
274 if let Oauth2Type::PKCE(code) = self.flow_type {
275 params.append_pair("code_challenge", &code.s256());
276 params.append_pair("code_challenge_method", "S256");
277 }
278 }
279 url
280 }
281}
282
283/// [`Authorization`] is a state-machine.
284///
285/// Every flow starts with the `InitialAuth` state, which is just after the user authorizes the app
286/// and gets redirected back. It then proceeds to either the `Refresh` or `AccessToken` state
287/// depending on whether a long-lived token was requested.
288///
289/// `Refresh` contains the refresh token necessary to obtain updated short-lived access tokens.
290///
291/// `AccessToken` contains just the access token itself, which is either a long-lived access token
292/// not expected to expire, or a short-lived token which, if it expires, cannot be refreshed except
293/// by starting the authorization flow over again.
294#[derive(Debug, Clone)]
295enum AuthorizationState {
296 InitialAuth {
297 flow_type: Oauth2Type,
298 auth_code: String,
299 redirect_uri: Option<String>,
300 },
301 Refresh {
302 refresh_token: String,
303 client_secret: Option<String>,
304 },
305 AccessToken {
306 client_secret: Option<String>,
307 token: String,
308 },
309}
310
311/// Provides for continuing authorization of the app.
312#[derive(Debug, Clone)]
313pub struct Authorization {
314 /// Dropbox app key
315 pub client_id: String,
316 state: AuthorizationState,
317}
318
319impl Authorization {
320 /// Get the client ID for this authorization.
321 pub fn client_id(&self) -> &str {
322 &self.client_id
323 }
324
325 /// Create a new instance using the authorization code provided upon redirect back to your app
326 /// (or via manual user entry if not using a redirect URI) after the user logs in.
327 ///
328 /// Requires the client ID; the type of OAuth2 flow being used (including the client secret or
329 /// the PKCE challenge); the authorization code; and the redirect URI used for the original
330 /// authorization request, if any.
331 pub fn from_auth_code(
332 client_id: String,
333 flow_type: Oauth2Type,
334 auth_code: String,
335 redirect_uri: Option<String>,
336 ) -> Self {
337 Self {
338 client_id,
339 state: AuthorizationState::InitialAuth {
340 flow_type,
341 auth_code,
342 redirect_uri,
343 },
344 }
345 }
346
347 /// Save the authorization state to a string which can be reloaded later.
348 ///
349 /// Returns `None` if the state cannot be saved (e.g. authorization has not completed getting a
350 /// token yet).
351 pub fn save(&self) -> Option<String> {
352 match &self.state {
353 AuthorizationState::AccessToken {
354 token,
355 client_secret,
356 } if client_secret.is_none() => {
357 // Legacy long-lived access token.
358 Some(format!("1&{}", token))
359 }
360 AuthorizationState::Refresh { refresh_token, .. } => {
361 Some(format!("2&{}", refresh_token))
362 }
363 _ => None,
364 }
365 }
366
367 /// Reload a saved authorization state produced by [`save`](Authorization::save).
368 ///
369 /// Returns `None` if the string could not be recognized. In this case, you should start the
370 /// authorization procedure from scratch.
371 ///
372 /// Note that a loaded authorization state is not necessarily still valid and may produce
373 /// [`Authentication`](crate::Error::Authentication) errors. In such a case you should also
374 /// start the authorization procedure from scratch.
375 pub fn load(client_id: String, saved: &str) -> Option<Self> {
376 Some(match saved.get(0..2) {
377 Some("1&") =>
378 {
379 #[allow(deprecated)]
380 Self::from_long_lived_access_token(saved[2..].to_owned())
381 }
382 Some("2&") => Self::from_refresh_token(client_id, saved[2..].to_owned()),
383 _ => {
384 error!("unrecognized saved Authorization representation: {saved:?}");
385 return None;
386 }
387 })
388 }
389
390 /// Recreate the authorization from a refresh token obtained using the [`Oauth2Type::PKCE`]
391 /// flow.
392 pub fn from_refresh_token(client_id: String, refresh_token: String) -> Self {
393 Self {
394 client_id,
395 state: AuthorizationState::Refresh {
396 refresh_token,
397 client_secret: None,
398 },
399 }
400 }
401
402 /// Recreate the authorization from a refresh token obtained using the
403 /// [`Oauth2Type::AuthorizationCode`] flow. This requires the client secret as well.
404 pub fn from_client_secret_refresh_token(
405 client_id: String,
406 client_secret: String,
407 refresh_token: String,
408 ) -> Self {
409 Self {
410 client_id,
411 state: AuthorizationState::Refresh {
412 refresh_token,
413 client_secret: Some(client_secret),
414 },
415 }
416 }
417
418 /// Recreate the authorization from a long-lived access token. This token cannot be refreshed;
419 /// any call to [`obtain_access_token_async`](Authorization::obtain_access_token_async) will
420 /// simply return the given token. Therefore this requires neither client ID or client secret.
421 ///
422 /// Long-lived tokens are deprecated and the ability to generate them will be removed in the
423 /// future.
424 #[deprecated]
425 pub fn from_long_lived_access_token(access_token: String) -> Self {
426 Self {
427 client_id: String::new(),
428 state: AuthorizationState::AccessToken {
429 token: access_token,
430 client_secret: None,
431 },
432 }
433 }
434
435 if_feature! { "sync_routes",
436 /// Compatibility shim for working with sync HTTP clients.
437 pub fn obtain_access_token(
438 &mut self,
439 sync_client: impl crate::client_trait::NoauthClient
440 ) -> Result<String, Error> {
441 use futures::FutureExt;
442 self.obtain_access_token_async(sync_client)
443 .now_or_never()
444 .expect("sync client future should resolve immediately")
445 }
446 }
447
448 /// Obtain an access token. Use this to complete the authorization process, or to obtain an
449 /// updated token when a short-lived access token has expired.
450 pub async fn obtain_access_token_async(
451 &mut self,
452 client: impl NoauthClient,
453 ) -> Result<String, Error> {
454 let mut redirect_uri = None;
455 let mut client_secret = None;
456 let mut pkce_code = None;
457 let mut refresh_token = None;
458 let mut auth_code = None;
459
460 match self.state.clone() {
461 AuthorizationState::AccessToken {
462 token,
463 client_secret: secret,
464 } => {
465 match secret {
466 None => {
467 // Long-lived token which cannot be refreshed
468 return Ok(token);
469 }
470 Some(secret) => {
471 client_secret = Some(secret);
472 }
473 }
474 }
475 AuthorizationState::InitialAuth {
476 flow_type,
477 auth_code: code,
478 redirect_uri: uri,
479 } => {
480 match flow_type {
481 Oauth2Type::ImplicitGrant => {
482 self.state = AuthorizationState::AccessToken {
483 client_secret: None,
484 token: code.clone(),
485 };
486 return Ok(code);
487 }
488 Oauth2Type::AuthorizationCode {
489 client_secret: secret,
490 } => {
491 client_secret = Some(secret);
492 }
493 Oauth2Type::PKCE(pkce) => {
494 pkce_code = Some(pkce.code.clone());
495 }
496 }
497 auth_code = Some(code);
498 redirect_uri = uri;
499 }
500 AuthorizationState::Refresh {
501 refresh_token: refresh,
502 client_secret: secret,
503 } => {
504 refresh_token = Some(refresh);
505 if let Some(secret) = secret {
506 client_secret = Some(secret);
507 }
508 }
509 }
510
511 let params = {
512 let mut params = UrlEncoder::new(String::new());
513
514 if let Some(refresh) = &refresh_token {
515 params.append_pair("grant_type", "refresh_token");
516 params.append_pair("refresh_token", refresh);
517 } else {
518 params.append_pair("grant_type", "authorization_code");
519 params.append_pair("code", &auth_code.unwrap());
520 }
521
522 params.append_pair("client_id", &self.client_id);
523
524 if let Some(client_secret) = client_secret.as_deref() {
525 params.append_pair("client_secret", client_secret);
526 }
527
528 if let Some(pkce) = &pkce_code {
529 params.append_pair("code_verifier", pkce);
530 }
531
532 if refresh_token.is_none() {
533 if let Some(pkce) = pkce_code {
534 params.append_pair("code_verifier", &pkce);
535 } else {
536 params.append_pair(
537 "client_secret",
538 client_secret
539 .as_ref()
540 .expect("need either PKCE code or client secret"),
541 );
542 }
543 }
544
545 if let Some(value) = redirect_uri {
546 params.append_pair("redirect_uri", &value);
547 }
548
549 params.finish()
550 };
551
552 let (req, body) = prepare_request(
553 &client,
554 Endpoint::OAuth2,
555 Style::Rpc,
556 "oauth2/token",
557 params,
558 ParamsType::Form,
559 None,
560 None,
561 None,
562 None,
563 None,
564 );
565 let body = body.unwrap_or_default();
566
567 debug!("Requesting OAuth2 token");
568 let resp = client.execute(req, body).await?;
569 let (result_json, _, _) = parse_response(resp, Style::Rpc).await?;
570 let result_value = serde_json::from_str(&result_json)?;
571
572 debug!("OAuth2 response: {:?}", result_value);
573
574 let access_token: String;
575 let refresh_token: Option<String>;
576
577 match result_value {
578 serde_json::Value::Object(mut map) => {
579 match map.remove("access_token") {
580 Some(serde_json::Value::String(token)) => access_token = token,
581 _ => {
582 return Err(Error::UnexpectedResponse(
583 "no access token in response!".to_owned(),
584 ))
585 }
586 }
587 match map.remove("refresh_token") {
588 Some(serde_json::Value::String(refresh)) => refresh_token = Some(refresh),
589 Some(_) => {
590 return Err(Error::UnexpectedResponse(
591 "refresh token is not a string!".to_owned(),
592 ));
593 }
594 None => refresh_token = None,
595 }
596 }
597 _ => {
598 return Err(Error::UnexpectedResponse(
599 "response is not a JSON object".to_owned(),
600 ))
601 }
602 }
603
604 match refresh_token {
605 Some(refresh) => {
606 self.state = AuthorizationState::Refresh {
607 refresh_token: refresh,
608 client_secret,
609 };
610 }
611 None if !matches!(self.state, AuthorizationState::Refresh { .. }) => {
612 self.state = AuthorizationState::AccessToken {
613 token: access_token.clone(),
614 client_secret,
615 };
616 }
617 _ => (),
618 }
619
620 Ok(access_token)
621 }
622}
623
624/// `TokenCache` provides the current OAuth2 token and a means to refresh it in a thread-safe way.
625pub struct TokenCache {
626 auth: RwLock<(Authorization, Arc<String>)>,
627}
628
629impl TokenCache {
630 /// Make a new token cache, using the given [`Authorization`] as a source of tokens.
631 pub fn new(auth: Authorization) -> Self {
632 Self {
633 auth: RwLock::new((auth, Arc::new(String::new()))),
634 }
635 }
636
637 /// Get the current token, unless no cached token is set yet.
638 pub fn get_token(&self) -> Option<Arc<String>> {
639 let read = self.auth.read_blocking();
640 if read.1.is_empty() {
641 None
642 } else {
643 Some(Arc::clone(&read.1))
644 }
645 }
646
647 /// Forces an update to the token, for when it is detected that the token is expired.
648 ///
649 /// To avoid double-updating the token in a race, requires the token which is being replaced.
650 /// For the case where no token is currently present, use the empty string as the token.
651 pub async fn update_token(
652 &self,
653 client: impl NoauthClient,
654 old_token: Arc<String>,
655 ) -> Result<Arc<String>, Error> {
656 let mut write = self.auth.write().await;
657 // Check if the token changed while we were unlocked; only update it if it
658 // didn't.
659 if write.1 == old_token {
660 write.1 = Arc::new(write.0.obtain_access_token_async(client).await?);
661 }
662 Ok(Arc::clone(&write.1))
663 }
664
665 /// Set the current short-lived token to a specific provided value. Normally it should not be
666 /// necessary to call this function; the token should be obtained automatically using the
667 /// refresh token.
668 pub fn set_access_token(&self, access_token: String) {
669 let mut write = self.auth.write_blocking();
670 write.1 = Arc::new(access_token);
671 }
672}
673
674/// Get an [`Authorization`] instance from environment variables `DBX_CLIENT_ID` and `DBX_OAUTH`
675/// (containing a refresh token) or `DBX_OAUTH_TOKEN` (containing a legacy long-lived token).
676///
677/// If environment variables are not set, and stdin is a terminal, prompt interactively for
678/// authorization.
679///
680/// If environment variables are not set, and stdin is not a terminal, panics.
681///
682/// This is a helper function intended only for tests and example code. Use in production code is
683/// strongly discouraged; you should write something more customized to your needs instead.
684///
685/// In particular, in real production code, you probably don't want to use environment variables.
686/// The client ID should be a hard-coded constant, or specified in configuration somewhere. It is
687/// not something that will change often, or maybe ever.
688/// The refresh token should only be stored somewhere safe like a file or database with restricted
689/// access permissions.
690pub fn get_auth_from_env_or_prompt() -> Authorization {
691 if let Ok(long_lived) = env::var("DBX_OAUTH_TOKEN") {
692 // Used to provide a legacy long-lived token.
693 #[allow(deprecated)]
694 return Authorization::from_long_lived_access_token(long_lived);
695 }
696
697 if let (Ok(client_id), Ok(saved)) = (env::var("DBX_CLIENT_ID"), env::var("DBX_OAUTH"))
698 // important! see the above warning about using environment variables for this
699 {
700 match Authorization::load(client_id, &saved) {
701 Some(auth) => return auth,
702 None => {
703 eprintln!("saved authorization in DBX_CLIENT_ID and DBX_OAUTH are invalid");
704 // and fall back to prompting
705 }
706 }
707 }
708
709 if !io::stdin().is_terminal() {
710 panic!("DBX_CLIENT_ID and/or DBX_OAUTH not set, and stdin not a TTY; cannot authorize");
711 }
712
713 fn prompt(msg: &str) -> String {
714 eprint!("{}: ", msg);
715 io::stderr().flush().unwrap();
716 let mut input = String::new();
717 io::stdin().read_line(&mut input).unwrap();
718 input.trim().to_owned()
719 }
720
721 let client_id = prompt("Give me a Dropbox API app key");
722
723 let oauth2_flow = Oauth2Type::PKCE(PkceCode::new());
724 let url = AuthorizeUrlBuilder::new(&client_id, &oauth2_flow).build();
725 eprintln!("Open this URL in your browser:");
726 eprintln!("{}", url);
727 eprintln!();
728 let auth_code = prompt("Then paste the code here");
729
730 Authorization::from_auth_code(client_id, oauth2_flow, auth_code.trim().to_owned(), None)
731}