dbx_tools_auth/
provider.rs1use crate::{
2 AccessToken, AuthClient, AuthError, AuthOptions, AuthSession, BindingResult, CredentialStore,
3 FileLayout, OAuthConfig, OAuthFlow, OAuthTemplate, Result, Storage, StorageHandle, Token,
4 TokenProvider,
5};
6use sha2::{Digest, Sha256};
7use std::{path::PathBuf, sync::Arc, time::Duration};
8
9#[derive(Clone, Copy, Debug, Default, uniffi::Enum)]
10pub enum OAuthGrant {
12 #[default]
13 AuthorizationCode,
14 ClientCredentials,
15}
16
17#[derive(Clone, uniffi::Record)]
19pub struct ProviderOptions {
20 pub provider: String,
22 pub client_id: String,
24 pub token_endpoint: String,
26 #[uniffi(default = None)]
28 pub authorization_endpoint: Option<String>,
29 #[uniffi(default = None)]
31 pub client_secret: Option<String>,
32 #[uniffi(default = None)]
34 pub profile: Option<String>,
35 #[uniffi(default = [])]
37 pub scopes: Vec<String>,
38 #[uniffi(default = None)]
40 pub grant: Option<OAuthGrant>,
41 #[uniffi(default = None)]
43 pub cache_dir: Option<String>,
44 #[uniffi(default = None)]
46 pub storage: Option<Storage>,
47 #[uniffi(default = None)]
49 pub file_layout: Option<FileLayout>,
50 #[uniffi(default = None)]
52 pub auth: Option<AuthOptions>,
53}
54
55#[uniffi::export]
56pub fn canonical_scopes(scopes: Vec<String>) -> Vec<String> {
58 let mut scopes: Vec<_> = scopes
59 .into_iter()
60 .map(|scope| scope.trim().to_owned())
61 .filter(|scope| !scope.is_empty())
62 .collect();
63 scopes.sort();
64 scopes.dedup();
65 scopes
66}
67
68#[uniffi::export]
69pub fn credential_key(provider: String, profile: Option<String>, scopes: Vec<String>) -> String {
71 let scope_hash = format!(
72 "{:x}",
73 Sha256::digest(serde_json::to_vec(&canonical_scopes(scopes)).expect("strings serialize"))
74 );
75 serde_json::to_string(&(provider, profile, scope_hash)).expect("strings serialize")
76}
77
78struct ProviderFlow {
79 flow: OAuthFlow,
80 grant: OAuthGrant,
81}
82
83#[async_trait::async_trait]
84impl TokenProvider for ProviderFlow {
85 async fn authenticate(&self, timeout: Duration) -> Result<Token> {
86 match self.grant {
87 OAuthGrant::AuthorizationCode => self.flow.login(timeout).await,
88 OAuthGrant::ClientCredentials => self.flow.client_credentials().await,
89 }
90 }
91 async fn refresh(&self, token: &Token) -> Result<Token> {
92 match self.grant {
93 OAuthGrant::AuthorizationCode => self.flow.refresh(token).await,
94 OAuthGrant::ClientCredentials => self.flow.client_credentials().await,
95 }
96 }
97 fn can_authenticate_silently(&self) -> bool {
98 matches!(self.grant, OAuthGrant::ClientCredentials)
99 }
100}
101
102#[derive(uniffi::Object)]
103pub struct ProviderAuth {
105 inner: AuthClient,
106}
107
108fn failure(error: impl std::fmt::Display) -> AuthError {
109 AuthError::Failure {
110 message: error.to_string(),
111 }
112}
113
114fn create(
115 options: ProviderOptions,
116 store: Arc<dyn CredentialStore>,
117) -> BindingResult<Arc<ProviderAuth>> {
118 let auth = options.auth.unwrap_or_default();
119 if options.provider.trim().is_empty() {
120 return Err(failure("provider must not be empty"));
121 }
122 let grant = options.grant.unwrap_or_default();
123 if matches!(grant, OAuthGrant::AuthorizationCode) && options.authorization_endpoint.is_none() {
124 return Err(failure(
125 "authorization-code grants require an authorization endpoint",
126 ));
127 }
128 let scopes = canonical_scopes(options.scopes);
129 let key = credential_key(options.provider.clone(), options.profile, scopes.clone());
130 let flow = OAuthFlow::new(OAuthConfig {
131 provider: options.provider,
132 authorization_endpoint: options
133 .authorization_endpoint
134 .unwrap_or_else(|| options.token_endpoint.clone()),
135 token_endpoint: options.token_endpoint,
136 client_id: options.client_id,
137 client_secret: options.client_secret,
138 scopes,
139 extra_token_params: vec![],
140 host: None,
141 })
142 .map_err(failure)?
143 .with_template(OAuthTemplate::new(auth.callback_image_src.clone()));
144 let inner = AuthClient::new(key, Arc::new(ProviderFlow { flow, grant }), store, auth);
145 Ok(Arc::new(ProviderAuth { inner }))
146}
147
148#[uniffi::export(async_runtime = "tokio")]
149pub async fn create_provider_auth(options: ProviderOptions) -> BindingResult<Arc<ProviderAuth>> {
151 let directory = match &options.cache_dir {
152 Some(directory) => PathBuf::from(directory),
153 None => directories::UserDirs::new()
154 .ok_or_else(|| failure("could not resolve the user home directory"))?
155 .home_dir()
156 .join(".dbx-tools/auth"),
157 };
158 let store = crate::open_store(
159 options.storage.unwrap_or_default(),
160 directory,
161 options.file_layout.unwrap_or_default(),
162 )
163 .await
164 .map_err(failure)?;
165 create(options, store)
166}
167
168#[uniffi::export(async_runtime = "tokio")]
169pub async fn create_provider_auth_with_storage(
171 options: ProviderOptions,
172 storage: Arc<StorageHandle>,
173) -> BindingResult<Arc<ProviderAuth>> {
174 create(options, storage.store.clone())
175}
176
177#[uniffi::export(async_runtime = "tokio")]
178impl ProviderAuth {
179 #[uniffi::method(default(login = None))]
181 pub async fn token(&self, login: Option<bool>) -> BindingResult<AccessToken> {
182 self.inner
183 .token_with_login(login)
184 .await
185 .map(Into::into)
186 .map_err(failure)
187 }
188 pub async fn force_refresh_token(&self) -> BindingResult<AccessToken> {
190 self.inner
191 .force_refresh()
192 .await
193 .map(Into::into)
194 .map_err(failure)
195 }
196 pub async fn refresh_rejected_token(
198 &self,
199 stale_access_token: String,
200 ) -> BindingResult<AccessToken> {
201 self.inner
202 .refresh_rejected_token(&stale_access_token)
203 .await
204 .map(Into::into)
205 .map_err(failure)
206 }
207 pub async fn logout(&self) -> BindingResult<()> {
209 self.inner.logout().await.map_err(failure)
210 }
211}