alibi_api/plugins/oauth/providers/
dropbox.rs1use super::{
3 OAuthAuthorizationPolicy, OAuthProvider, OAuthTokenEndpointAuth, OAuthUserInfo,
4 OAuthUserInfoHandler, OAuthUserInfoRequest, OAuthUserInfoResponse,
5};
6use async_trait::async_trait;
7use serde_json::Value;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum DropboxAccessType {
12 Offline,
13 Online,
14 Legacy,
15}
16impl DropboxAccessType {
17 const fn as_str(self) -> &'static str {
18 match self {
19 Self::Offline => "offline",
20 Self::Online => "online",
21 Self::Legacy => "legacy",
22 }
23 }
24}
25
26#[derive(Clone)]
29pub struct DropboxOptions {
30 pub client_id: String,
31 pub client_secret: Option<String>,
32 pub client_key: Option<String>,
34 pub scope: Vec<String>,
35 pub disable_default_scope: bool,
36 pub access_type: Option<DropboxAccessType>,
37 pub authorization_endpoint: Option<String>,
38 pub redirect_uri: Option<String>,
39 pub user_info_endpoint: Option<String>,
41 pub map_profile_to_user: Option<fn(Value) -> Result<OAuthUserInfo, String>>,
43}
44impl DropboxOptions {
45 #[must_use]
46 pub fn new(client_id: impl Into<String>, client_secret: Option<String>) -> Self {
47 Self {
48 client_id: client_id.into(),
49 client_secret,
50 client_key: None,
51 scope: Vec::new(),
52 disable_default_scope: false,
53 access_type: None,
54 authorization_endpoint: None,
55 redirect_uri: None,
56 user_info_endpoint: None,
57 map_profile_to_user: None,
58 }
59 }
60}
61impl std::fmt::Debug for DropboxOptions {
62 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
63 formatter
64 .debug_struct("DropboxOptions")
65 .field("client_id", &self.client_id)
66 .field("scope", &self.scope)
67 .field("disable_default_scope", &self.disable_default_scope)
68 .field("access_type", &self.access_type)
69 .field("authorization_endpoint", &self.authorization_endpoint)
70 .field("redirect_uri", &self.redirect_uri)
71 .finish_non_exhaustive()
72 }
73}
74impl OAuthProvider {
75 #[must_use]
76 pub fn dropbox(client_id: &str, client_secret: Option<&str>) -> Self {
77 Self::dropbox_with_options(DropboxOptions::new(
78 client_id,
79 client_secret.map(str::to_owned),
80 ))
81 }
82 #[must_use]
83 pub fn dropbox_with_options(options: DropboxOptions) -> Self {
84 let secret = options.client_secret.unwrap_or_default();
85 let authentication = if secret.is_empty() {
86 OAuthTokenEndpointAuth::None
87 } else {
88 OAuthTokenEndpointAuth::ClientSecretPost
89 };
90 let endpoint = options
91 .user_info_endpoint
92 .unwrap_or_else(|| "https://api.dropboxapi.com/2/users/get_current_account".into());
93 Self {
94 client_id: options.client_id,
95 client_secret: secret,
96 additional_client_ids: Vec::new(),
97 hosted_domain: None,
98 require_email_verification: false,
99 auth_url: options
100 .authorization_endpoint
101 .filter(|value| !value.is_empty())
102 .unwrap_or_else(|| "https://www.dropbox.com/oauth2/authorize".into()),
103 token_url: "https://api.dropboxapi.com/oauth2/token".into(),
104 user_info_url: Some(endpoint.clone()),
105 scopes: vec!["account_info.read".into()],
106 authorization: Some(OAuthAuthorizationPolicy {
107 configured_scopes: options.scope,
108 disable_default_scopes: options.disable_default_scope,
109 require_client_id: true,
110 token_endpoint_auth: Some(authentication),
111 authorization_code_client_key: options.client_key,
112 redirect_uri: options.redirect_uri,
113 login_hint: false,
114 ..OAuthAuthorizationPolicy::default()
115 }),
116 authorization_params: options
117 .access_type
118 .map(|value| vec![("token_access_type".into(), value.as_str().into())])
119 .unwrap_or_default(),
120 account_subject: Some(subject),
121 map_user_info: None,
122 get_user_info: Some(std::sync::Arc::new(DropboxUserInfo {
123 endpoint,
124 mapper: options.map_profile_to_user,
125 })),
126 refresh_access_token: None,
127 verify_id_token: None,
128 id_token: None,
129 disable_id_token_sign_in: false,
130 disable_implicit_sign_up: false,
131 disable_sign_up: false,
132 override_user_info_on_sign_in: false,
133 }
134 }
135}
136struct DropboxUserInfo {
137 endpoint: String,
138 mapper: Option<fn(Value) -> Result<OAuthUserInfo, String>>,
139}
140#[async_trait]
141impl OAuthUserInfoHandler for DropboxUserInfo {
142 async fn get_user_info(
143 &self,
144 request: OAuthUserInfoRequest,
145 ) -> Result<OAuthUserInfoResponse, String> {
146 let access_token = request.access_token.ok_or("Missing Dropbox access token")?;
147 let profile: Value = reqwest::Client::new()
148 .post(&self.endpoint)
149 .bearer_auth(access_token)
150 .send()
151 .await
152 .map_err(|error| error.to_string())?
153 .error_for_status()
154 .map_err(|error| error.to_string())?
155 .json()
156 .await
157 .map_err(|error| error.to_string())?;
158 let mapped = self
159 .mapper
160 .map(|mapper| mapper(profile.clone()))
161 .transpose()?;
162 let user_output = mapped.as_ref().map(|user| user.public_profile(true));
163 let user = match mapped {
164 Some(user) => user,
165 None => OAuthUserInfo {
166 additional_fields: Default::default(),
167 id: scalar(profile.get("account_id"))?.unwrap_or_default(),
168 name: scalar(
169 profile
170 .get("name")
171 .and_then(|name| name.get("display_name")),
172 )?,
173 email: profile
174 .get("email")
175 .and_then(Value::as_str)
176 .unwrap_or_default()
177 .into(),
178 image: scalar(profile.get("profile_photo_url"))?,
179 email_verified: profile
180 .get("email_verified")
181 .and_then(Value::as_bool)
182 .unwrap_or(false),
183 },
184 };
185 Ok(OAuthUserInfoResponse {
186 user_output,
187 user,
188 data: profile,
189 })
190 }
191}
192fn scalar(value: Option<&Value>) -> Result<Option<String>, String> {
193 match value {
194 None | Some(Value::Null) => Ok(None),
195 Some(Value::String(value)) => Ok(Some(value.clone())),
196 Some(Value::Number(value)) => alibi_core::utils::json::number_to_string(value)
197 .map(Some)
198 .map_err(|error| error.to_string()),
199 Some(Value::Bool(value)) => Ok(Some(value.to_string())),
200 Some(Value::Array(_) | Value::Object(_)) => Err("Invalid Dropbox profile field".into()),
201 }
202}
203fn subject(profile: &Value) -> Result<String, String> {
204 let id = scalar(profile.get("account_id"))?.ok_or("Missing Dropbox account_id")?;
205 if id
206 .trim_matches(|character: char| {
207 (character.is_whitespace() && character != '\u{85}') || character == '\u{feff}'
208 })
209 .is_empty()
210 || id == "null"
211 || id == "undefined"
212 {
213 return Err("Invalid Dropbox account_id".into());
214 }
215 Ok(id)
216}