Skip to content

Commit d5c0dc5

Browse files
authored
[PM-23189] Add ClientManagedTokens trait (#337)
Adds support for client managed access tokens alongside SDK managed access tokens. It does so by splitting `Tokens` into `SdkManaged` and `ClientManaged`. `ClientManaged` relies on the consumer implementing a trait with `get_access_token` function. In WASM this is exposed as a `TokenProvider` interface which can be implemented in TS clients using: ```ts class JsTokenProvider implements TokenProvider { constructor(private apiService: ApiService) {} async get_access_token(): Promise<string> { return await this.apiService.getActiveBearerToken(); } } ```
1 parent be567ce commit d5c0dc5

File tree

9 files changed

+135
-45
lines changed

9 files changed

+135
-45
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/bitwarden-core/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ wasm = [
2929
] # WASM support
3030

3131
[dependencies]
32+
async-trait = { workspace = true }
3233
base64 = ">=0.22.1, <0.23"
3334
bitwarden-api-api = { workspace = true }
3435
bitwarden-api-identity = { workspace = true }

crates/bitwarden-core/src/auth/renew.rs

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
use std::sync::Arc;
2+
13
use chrono::Utc;
24

35
use super::login::LoginError;
@@ -10,18 +12,44 @@ use crate::{
1012
};
1113
use crate::{
1214
auth::api::{request::ApiTokenRequest, response::IdentityTokenResponse},
13-
client::{internal::InternalClient, LoginMethod, UserLoginMethod},
15+
client::{
16+
internal::{ClientManagedTokens, InternalClient, SdkManagedTokens, Tokens},
17+
LoginMethod, UserLoginMethod,
18+
},
1419
NotAuthenticatedError,
1520
};
1621

1722
pub(crate) async fn renew_token(client: &InternalClient) -> Result<(), LoginError> {
18-
const TOKEN_RENEW_MARGIN_SECONDS: i64 = 5 * 60;
19-
20-
let tokens = client
23+
let tokens_guard = client
2124
.tokens
2225
.read()
2326
.expect("RwLock is not poisoned")
2427
.clone();
28+
29+
match tokens_guard {
30+
Tokens::SdkManaged(tokens) => renew_token_sdk_managed(client, tokens).await,
31+
Tokens::ClientManaged(tokens) => renew_token_client_managed(client, tokens).await,
32+
}
33+
}
34+
35+
async fn renew_token_client_managed(
36+
client: &InternalClient,
37+
tokens: Arc<dyn ClientManagedTokens>,
38+
) -> Result<(), LoginError> {
39+
let token = tokens
40+
.get_access_token()
41+
.await
42+
.ok_or(NotAuthenticatedError)?;
43+
client.set_api_tokens_internal(token);
44+
Ok(())
45+
}
46+
47+
async fn renew_token_sdk_managed(
48+
client: &InternalClient,
49+
tokens: SdkManagedTokens,
50+
) -> Result<(), LoginError> {
51+
const TOKEN_RENEW_MARGIN_SECONDS: i64 = 5 * 60;
52+
2553
let login_method = client
2654
.login_method
2755
.read()

crates/bitwarden-core/src/client/client.rs

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ use super::internal::InternalClient;
1010
use crate::client::flags::Flags;
1111
use crate::client::{
1212
client_settings::ClientSettings,
13-
internal::{ApiConfigurations, Tokens},
13+
internal::{ApiConfigurations, ClientManagedTokens, SdkManagedTokens, Tokens},
1414
};
1515

1616
/// The main struct to interact with the Bitwarden SDK.
@@ -25,8 +25,20 @@ pub struct Client {
2525
}
2626

2727
impl Client {
28-
#[allow(missing_docs)]
29-
pub fn new(settings_input: Option<ClientSettings>) -> Self {
28+
/// Create a new Bitwarden client with SDK-managed tokens.
29+
pub fn new(settings: Option<ClientSettings>) -> Self {
30+
Self::new_internal(settings, Tokens::SdkManaged(SdkManagedTokens::default()))
31+
}
32+
33+
/// Create a new Bitwarden client with client-managed tokens.
34+
pub fn new_with_client_tokens(
35+
settings: Option<ClientSettings>,
36+
tokens: Arc<dyn ClientManagedTokens>,
37+
) -> Self {
38+
Self::new_internal(settings, Tokens::ClientManaged(tokens))
39+
}
40+
41+
fn new_internal(settings_input: Option<ClientSettings>, tokens: Tokens) -> Self {
3042
let settings = settings_input.unwrap_or_default();
3143

3244
fn new_client_builder() -> reqwest::ClientBuilder {
@@ -81,7 +93,7 @@ impl Client {
8193
Self {
8294
internal: Arc::new(InternalClient {
8395
user_id: OnceLock::new(),
84-
tokens: RwLock::new(Tokens::default()),
96+
tokens: RwLock::new(tokens),
8597
login_method: RwLock::new(None),
8698
#[cfg(feature = "internal")]
8799
flags: RwLock::new(Flags::default()),

crates/bitwarden-core/src/client/internal.rs

Lines changed: 28 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,26 @@ pub struct ApiConfigurations {
3333
pub device_type: DeviceType,
3434
}
3535

36+
/// Access and refresh tokens used for authentication and authorization.
37+
#[derive(Debug, Clone)]
38+
pub(crate) enum Tokens {
39+
SdkManaged(SdkManagedTokens),
40+
ClientManaged(Arc<dyn ClientManagedTokens>),
41+
}
42+
43+
/// Access tokens managed by client applications, such as the web or mobile apps.
44+
#[async_trait::async_trait]
45+
pub trait ClientManagedTokens: std::fmt::Debug + Send + Sync {
46+
/// Returns the access token, if available.
47+
async fn get_access_token(&self) -> Option<String>;
48+
}
49+
50+
/// Tokens managed by the SDK, the SDK will automatically handle token renewal.
3651
#[derive(Debug, Default, Clone)]
37-
pub(crate) struct Tokens {
52+
pub(crate) struct SdkManagedTokens {
3853
// These two fields are always written to, but they are not read
3954
// from the secrets manager SDK.
40-
#[cfg_attr(not(feature = "internal"), allow(dead_code))]
55+
#[allow(dead_code)]
4156
access_token: Option<String>,
4257
pub(crate) expires_on: Option<i64>,
4358

@@ -117,11 +132,17 @@ impl InternalClient {
117132
}
118133

119134
pub(crate) fn set_tokens(&self, token: String, refresh_token: Option<String>, expires_in: u64) {
120-
*self.tokens.write().expect("RwLock is not poisoned") = Tokens {
121-
access_token: Some(token.clone()),
122-
expires_on: Some(Utc::now().timestamp() + expires_in as i64),
123-
refresh_token,
124-
};
135+
*self.tokens.write().expect("RwLock is not poisoned") =
136+
Tokens::SdkManaged(SdkManagedTokens {
137+
access_token: Some(token.clone()),
138+
expires_on: Some(Utc::now().timestamp() + expires_in as i64),
139+
refresh_token,
140+
});
141+
self.set_api_tokens_internal(token);
142+
}
143+
144+
/// Sets api tokens for only internal API clients, use `set_tokens` for SdkManagedTokens.
145+
pub(crate) fn set_api_tokens_internal(&self, token: String) {
125146
let mut guard = self
126147
.__api_configurations
127148
.write()
@@ -132,24 +153,6 @@ impl InternalClient {
132153
inner.api.oauth_access_token = Some(token);
133154
}
134155

135-
#[allow(missing_docs)]
136-
#[cfg(feature = "internal")]
137-
pub fn is_authed(&self) -> bool {
138-
let is_token_set = self
139-
.tokens
140-
.read()
141-
.expect("RwLock is not poisoned")
142-
.access_token
143-
.is_some();
144-
let is_login_method_set = self
145-
.login_method
146-
.read()
147-
.expect("RwLock is not poisoned")
148-
.is_some();
149-
150-
is_token_set || is_login_method_set
151-
}
152-
153156
#[allow(missing_docs)]
154157
#[cfg(feature = "internal")]
155158
pub fn get_kdf(&self) -> Result<Kdf, NotAuthenticatedError> {

crates/bitwarden-core/src/platform/get_user_api_key.rs

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -63,14 +63,10 @@ pub(crate) async fn get_user_api_key(
6363
}
6464

6565
fn get_login_method(client: &Client) -> Result<Arc<LoginMethod>, NotAuthenticatedError> {
66-
if client.internal.is_authed() {
67-
client
68-
.internal
69-
.get_login_method()
70-
.ok_or(NotAuthenticatedError)
71-
} else {
72-
Err(NotAuthenticatedError)
73-
}
66+
client
67+
.internal
68+
.get_login_method()
69+
.ok_or(NotAuthenticatedError)
7470
}
7571

7672
/// Build the secret verification request.

crates/bitwarden-wasm-internal/src/client.rs

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
extern crate console_error_panic_hook;
2-
use std::fmt::Display;
2+
use std::{fmt::Display, sync::Arc};
33

44
use bitwarden_core::{key_management::CryptoClient, Client, ClientSettings};
55
use bitwarden_error::bitwarden_error;
@@ -8,7 +8,10 @@ use bitwarden_generators::GeneratorClientsExt;
88
use bitwarden_vault::{VaultClient, VaultClientExt};
99
use wasm_bindgen::prelude::*;
1010

11-
use crate::platform::PlatformClient;
11+
use crate::platform::{
12+
token_provider::{JsTokenProvider, WasmClientManagedTokens},
13+
PlatformClient,
14+
};
1215

1316
#[allow(missing_docs)]
1417
#[wasm_bindgen]
@@ -18,8 +21,9 @@ pub struct BitwardenClient(pub(crate) Client);
1821
impl BitwardenClient {
1922
#[allow(missing_docs)]
2023
#[wasm_bindgen(constructor)]
21-
pub fn new(settings: Option<ClientSettings>) -> Self {
22-
Self(Client::new(settings))
24+
pub fn new(settings: Option<ClientSettings>, token_provider: JsTokenProvider) -> Self {
25+
let tokens = Arc::new(WasmClientManagedTokens::new(token_provider));
26+
Self(Client::new_with_client_tokens(settings, tokens))
2327
}
2428

2529
/// Test method, echoes back the input

crates/bitwarden-wasm-internal/src/platform/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ use bitwarden_vault::Cipher;
33
use wasm_bindgen::prelude::wasm_bindgen;
44

55
mod repository;
6+
pub mod token_provider;
67

78
#[wasm_bindgen]
89
pub struct PlatformClient(Client);
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
use bitwarden_core::client::internal::ClientManagedTokens;
2+
use bitwarden_threading::ThreadBoundRunner;
3+
use wasm_bindgen::{prelude::wasm_bindgen, JsValue};
4+
5+
#[wasm_bindgen(typescript_custom_section)]
6+
const TOKEN_CUSTOM_TS_TYPE: &'static str = r#"
7+
export interface TokenProvider {
8+
get_access_token(): Promise<string>;
9+
}
10+
"#;
11+
12+
#[wasm_bindgen]
13+
extern "C" {
14+
#[wasm_bindgen(js_name = TokenProvider)]
15+
pub type JsTokenProvider;
16+
17+
#[wasm_bindgen(method)]
18+
pub async fn get_access_token(this: &JsTokenProvider) -> JsValue;
19+
}
20+
21+
/// Thread-bound runner for JavaScript token provider
22+
pub(crate) struct WasmClientManagedTokens(ThreadBoundRunner<JsTokenProvider>);
23+
24+
impl WasmClientManagedTokens {
25+
pub fn new(js_provider: JsTokenProvider) -> Self {
26+
Self(ThreadBoundRunner::new(js_provider))
27+
}
28+
}
29+
30+
impl std::fmt::Debug for WasmClientManagedTokens {
31+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
32+
f.debug_struct("WasmClientManagedTokens").finish()
33+
}
34+
}
35+
36+
#[async_trait::async_trait]
37+
impl ClientManagedTokens for WasmClientManagedTokens {
38+
async fn get_access_token(&self) -> Option<String> {
39+
self.0
40+
.run_in_thread(|c| async move { c.get_access_token().await.as_string() })
41+
.await
42+
.unwrap_or_default()
43+
}
44+
}

0 commit comments

Comments
 (0)