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