Skip to content

Commit f6a010e

Browse files
DynamicAuthenticationProvider supports refreshing authentication credentials (#821)
* `DynamicAuthenticationProvider` supports refreshing authentication credentials * Fix a compiling issue in the kotlin wrapper * Update API doc Co-authored-by: Oguz Kocer <[email protected]> --------- Co-authored-by: Oguz Kocer <[email protected]>
1 parent 558fa83 commit f6a010e

File tree

4 files changed

+131
-29
lines changed

4 files changed

+131
-29
lines changed

native/kotlin/api/kotlin/src/integrationTest/kotlin/AuthProviderTest.kt

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
package rs.wordpress.api.kotlin
22

3+
import kotlinx.coroutines.CoroutineDispatcher
4+
import kotlinx.coroutines.Dispatchers
5+
import kotlinx.coroutines.withContext
36
import kotlinx.coroutines.test.runTest
47
import org.junit.jupiter.api.Test
58
import uniffi.wp_api.ModifiableAuthenticationProvider
@@ -31,7 +34,10 @@ class AuthProviderTest {
3134

3235
@Test
3336
fun testDynamicAuthProvider() = runTest {
34-
class DynamicAuthProvider(var isAuthorized: Boolean = false): WpDynamicAuthenticationProvider {
37+
class DynamicAuthProvider(
38+
var isAuthorized: Boolean = false,
39+
private val dispatcher: CoroutineDispatcher = Dispatchers.IO
40+
): WpDynamicAuthenticationProvider {
3541
override fun auth(): WpAuthentication =
3642
if (isAuthorized) {
3743
wpAuthenticationFromUsernameAndPassword(
@@ -41,6 +47,7 @@ class AuthProviderTest {
4147
} else {
4248
WpAuthentication.None
4349
}
50+
override suspend fun refresh(): Boolean = withContext(dispatcher) { false }
4451
}
4552

4653
val dynamicAuthProvider = DynamicAuthProvider()

wp_api/src/auth.rs

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use http::HeaderValue;
2+
use std::fmt::Debug;
23
use std::sync::{Arc, RwLock};
34

45
#[derive(Debug, Clone, uniffi::Enum)]
@@ -70,8 +71,17 @@ impl ModifiableAuthenticationProvider {
7071
}
7172

7273
#[uniffi::export(with_foreign)]
73-
pub trait WpDynamicAuthenticationProvider: Send + Sync {
74+
#[async_trait::async_trait]
75+
pub trait WpDynamicAuthenticationProvider: Send + Sync + Debug {
7476
fn auth(&self) -> WpAuthentication;
77+
78+
/// Refresh the authentication token. The implementation should only return true
79+
/// if the authentication was successfully refreshed.
80+
///
81+
/// **Concurrency:** This method may be called concurrently by multiple request
82+
/// executors. Implementations must handle concurrent calls safely and avoid
83+
/// unnecessary duplicate refresh operations.
84+
async fn refresh(&self) -> bool;
7585
}
7686

7787
#[derive(uniffi::Object)]
@@ -135,4 +145,14 @@ impl WpAuthenticationProvider {
135145
WpAuthenticationProvider::Modifiable { inner } => inner.header_value(),
136146
}
137147
}
148+
149+
pub(crate) async fn refresh(&self) -> bool {
150+
match self {
151+
WpAuthenticationProvider::StaticAuthenticationProvider { .. } => false,
152+
WpAuthenticationProvider::DynamicAuthenticationProvider { inner } => {
153+
inner.refresh().await
154+
}
155+
WpAuthenticationProvider::Modifiable { .. } => false,
156+
}
157+
}
138158
}

wp_api_integration_tests/tests/test_auth_provider_immut.rs

Lines changed: 77 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use std::sync::atomic::{AtomicBool, Ordering};
1+
use std::sync::RwLock;
22
use wp_api::auth::{ModifiableAuthenticationProvider, WpDynamicAuthenticationProvider};
33
use wp_api_integration_tests::prelude::*;
44

@@ -20,27 +20,79 @@ async fn test_static_auth_provider() {
2020
assert_eq!(FIRST_USER_ID, user.id);
2121
}
2222

23-
#[tokio::test]
24-
#[parallel]
25-
async fn test_dynamic_auth_provider() {
26-
#[derive(Default)]
27-
struct DynamicAuthProvider {
28-
is_authorized: AtomicBool,
23+
#[derive(Debug)]
24+
struct DynamicAuthProvider {
25+
auth: RwLock<WpAuthentication>,
26+
refreshed_auth: RwLock<Option<WpAuthentication>>,
27+
}
28+
29+
impl DynamicAuthProvider {
30+
fn new() -> Self {
31+
Self {
32+
auth: RwLock::new(WpAuthentication::None),
33+
refreshed_auth: RwLock::new(None),
34+
}
35+
}
36+
37+
fn authenticate_with(&self, auth: WpAuthentication) {
38+
*(self.auth.write().unwrap()) = auth;
39+
}
40+
41+
fn allow_refresh_auth(&self, auth: WpAuthentication) {
42+
*(self.refreshed_auth.write().unwrap()) = Some(auth);
43+
}
44+
}
45+
46+
#[async_trait::async_trait]
47+
impl WpDynamicAuthenticationProvider for DynamicAuthProvider {
48+
fn auth(&self) -> WpAuthentication {
49+
self.auth.read().unwrap().clone()
2950
}
3051

31-
impl WpDynamicAuthenticationProvider for DynamicAuthProvider {
32-
fn auth(&self) -> WpAuthentication {
33-
if self.is_authorized.load(Ordering::Relaxed) {
34-
WpAuthentication::from_username_and_password(
35-
TestCredentials::instance().admin_username.to_string(),
36-
TestCredentials::instance().admin_password.to_string(),
37-
)
38-
} else {
39-
WpAuthentication::None
40-
}
52+
async fn refresh(&self) -> bool {
53+
if let Some(ref auth) = *(self.refreshed_auth.read().unwrap()) {
54+
*(self.auth.write().unwrap()) = auth.clone();
55+
return true;
56+
} else {
57+
return false;
4158
}
4259
}
43-
let dynamic_auth_provider = Arc::new(DynamicAuthProvider::default());
60+
}
61+
62+
#[tokio::test]
63+
#[parallel]
64+
async fn test_dynamic_auth_provider() {
65+
let dynamic_auth_provider = Arc::new(DynamicAuthProvider::new());
66+
let client: WpApiClient = api_client_with_auth_provider(Arc::new(
67+
WpAuthenticationProvider::dynamic(dynamic_auth_provider.clone()),
68+
));
69+
70+
// Assert that initial unauthorized request fails
71+
client
72+
.users()
73+
.retrieve_me_with_edit_context()
74+
.await
75+
.assert_wp_error(WpErrorCode::Unauthorized);
76+
77+
// Assert that request succeeds after providing a valid authentication
78+
dynamic_auth_provider.authenticate_with(WpAuthentication::from_username_and_password(
79+
TestCredentials::instance().admin_username.to_string(),
80+
TestCredentials::instance().admin_password.to_string(),
81+
));
82+
let user = client
83+
.users()
84+
.retrieve_me_with_edit_context()
85+
.await
86+
.assert_response()
87+
.data;
88+
// FIRST_USER_ID is the current user's id
89+
assert_eq!(FIRST_USER_ID, user.id);
90+
}
91+
92+
#[tokio::test]
93+
#[parallel]
94+
async fn test_refresh_dynamic_auth_provider() {
95+
let dynamic_auth_provider = Arc::new(DynamicAuthProvider::new());
4496
let client: WpApiClient = api_client_with_auth_provider(Arc::new(
4597
WpAuthenticationProvider::dynamic(dynamic_auth_provider.clone()),
4698
));
@@ -52,10 +104,13 @@ async fn test_dynamic_auth_provider() {
52104
.await
53105
.assert_wp_error(WpErrorCode::Unauthorized);
54106

55-
// Assert that request succeeds after setting `is_authorized = true`
56-
dynamic_auth_provider
57-
.is_authorized
58-
.store(true, Ordering::Relaxed);
107+
// Set the refreshed authentication
108+
dynamic_auth_provider.allow_refresh_auth(WpAuthentication::from_username_and_password(
109+
TestCredentials::instance().admin_username.to_string(),
110+
TestCredentials::instance().admin_password.to_string(),
111+
));
112+
113+
// Assert that request succeeds after refresh
59114
let user = client
60115
.users()
61116
.retrieve_me_with_edit_context()

wp_derive_request_builder/src/generate.rs

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -74,13 +74,33 @@ fn generate_async_request_executor(
7474
pub async #fn_signature -> Result<#response_type_ident, #error_type> {
7575
use #crate_ident::api_error::MaybeWpError;
7676
use #crate_ident::middleware::PerformsRequests;
77-
#request_from_request_builder
78-
let response = self.perform(std::sync::Arc::new(request)).await?;
79-
let response_status_code = response.status_code;
80-
let parsed_response = response.parse();
81-
if parsed_response.is_unauthorized_error().unwrap_or_default() || (response_status_code == 401 && self.fetch_authentication_state().await.map(|auth_state| auth_state.is_unauthorized()).unwrap_or_default()) {
77+
let perform_request = async || {
78+
#request_from_request_builder
79+
let response = self.perform(std::sync::Arc::new(request)).await?;
80+
let response_status_code = response.status_code;
81+
let parsed_response = response.parse();
82+
let unauthorized = parsed_response.is_unauthorized_error().unwrap_or_default() || (response_status_code == 401 && self.fetch_authentication_state().await.map(|auth_state| auth_state.is_unauthorized()).unwrap_or_default());
83+
84+
Ok((parsed_response, unauthorized))
85+
};
86+
87+
let mut parsed_response;
88+
let mut unauthorized;
89+
90+
// The Rust compiler has trouble figuring out the return type. The statement below helps it.
91+
let result: Result<_, #error_type> = perform_request().await;
92+
(parsed_response, unauthorized) = result?;
93+
94+
// If the auth provider successfully refreshed the authentication, we can retry the request.
95+
if unauthorized && self.delegate.auth_provider.refresh().await {
96+
// The response of the retried request will be returned instead of the original one.
97+
(parsed_response, unauthorized) = perform_request().await?;
98+
}
99+
100+
if unauthorized {
82101
self.delegate.app_notifier.requested_with_invalid_authentication().await;
83102
}
103+
84104
parsed_response
85105
}
86106
}

0 commit comments

Comments
 (0)