Skip to content

Commit d02efa6

Browse files
authored
Improve cookies nonce api (#1020)
* Cookies+nonces auth performs URL discovery internally if needed * Add Swift helpers for Cookies+nonce authentication
1 parent 8cfcc87 commit d02efa6

File tree

3 files changed

+166
-6
lines changed

3 files changed

+166
-6
lines changed

native/swift/Sources/wordpress-api/WordPressAPI.swift

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,55 @@ public actor WordPressAPI {
6868
)
6969
}
7070

71+
public init(
72+
urlSession: URLSession,
73+
siteUrl: String,
74+
apiRootUrl: ParsedUrl,
75+
username: String,
76+
password: String,
77+
middlewarePipeline: MiddlewarePipeline = .default,
78+
appNotifier: WpAppNotifier? = nil
79+
) {
80+
let executor = WpRequestExecutor(urlSession: urlSession)
81+
let provider = CookiesNonceAuthenticationProvider.withSiteUrl(
82+
url: siteUrl,
83+
username: username,
84+
password: password,
85+
requestExecutor: executor
86+
)
87+
self.init(
88+
apiUrlResolver: WpOrgSiteApiUrlResolver(apiRootUrl: apiRootUrl),
89+
authenticationProvider: .dynamic(dynamicAuthenticationProvider: provider),
90+
executor: executor,
91+
middlewarePipeline: middlewarePipeline,
92+
appNotifier: appNotifier
93+
)
94+
}
95+
96+
public init(
97+
urlSession: URLSession,
98+
details: AutoDiscoveryAttemptSuccess,
99+
username: String,
100+
password: String,
101+
middlewarePipeline: MiddlewarePipeline = .default,
102+
appNotifier: WpAppNotifier? = nil
103+
) {
104+
let executor = WpRequestExecutor(urlSession: urlSession)
105+
let provider = CookiesNonceAuthenticationProvider(
106+
username: username,
107+
password: password,
108+
details: details,
109+
requestExecutor: executor
110+
)
111+
self.init(
112+
apiUrlResolver: WpOrgSiteApiUrlResolver(apiRootUrl: details.apiRootUrl),
113+
authenticationProvider: .dynamic(dynamicAuthenticationProvider: provider),
114+
executor: executor,
115+
middlewarePipeline: middlewarePipeline,
116+
appNotifier: appNotifier
117+
)
118+
}
119+
71120
init(
72121
apiUrlResolver: ApiUrlResolver,
73122
authenticationProvider: WpAuthenticationProvider,

native/swift/Tests/integration-tests/NonceAuthenticationTests.swift

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,4 +45,35 @@ struct NonceAuthenticationTests {
4545
}
4646
}
4747

48+
@Test
49+
func cookiesNonceAuth() async throws {
50+
let credentials = TestCredentials.instance()
51+
let client = WordPressLoginClient(urlSession: .init(configuration: .ephemeral))
52+
let details = try await client.details(ofSite: credentials.siteUrl)
53+
let api = WordPressAPI(
54+
urlSession: .init(configuration: .ephemeral),
55+
details: details,
56+
username: credentials.adminUsername,
57+
password: credentials.adminAccountPassword
58+
)
59+
60+
let loggedIn = try await api.users.retrieveMeWithEditContext().data.username
61+
#expect(loggedIn == credentials.adminUsername)
62+
}
63+
64+
@Test
65+
func cookiesNonceAuthWithoutURLDiscovery() async throws {
66+
let credentials = TestCredentials.instance()
67+
let api = WordPressAPI(
68+
urlSession: .init(configuration: .ephemeral),
69+
siteUrl: credentials.siteUrl,
70+
apiRootUrl: credentials.apiRootURL,
71+
username: credentials.adminUsername,
72+
password: credentials.adminAccountPassword
73+
)
74+
75+
let loggedIn = try await api.users.retrieveMeWithEditContext().data.username
76+
#expect(loggedIn == credentials.adminUsername)
77+
}
78+
4879
}

wp_api/src/auth.rs

Lines changed: 86 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ use http::{HeaderMap, HeaderValue};
22
use std::fmt::Debug;
33
use std::sync::{Arc, RwLock};
44

5+
use crate::login::url_discovery::AutoDiscoveryAttemptFailure;
6+
use crate::prelude::WpLoginClient;
57
use crate::{
68
login::{nonce::WpRestNonceRetrieval, url_discovery::AutoDiscoveryAttemptSuccess},
79
request::RequestExecutor,
@@ -86,32 +88,97 @@ impl ModifiableAuthenticationProvider {
8688
}
8789
}
8890

89-
#[derive(Debug, uniffi::Object)]
91+
#[derive(uniffi::Object)]
9092
pub struct CookiesNonceAuthenticationProvider {
93+
site_url: String,
9194
username: String,
9295
password: String,
93-
nonce_retrieval: Arc<WpRestNonceRetrieval>,
96+
request_executor: Arc<dyn RequestExecutor>,
97+
nonce_retrieval: RwLock<Option<Arc<WpRestNonceRetrieval>>>,
9498
auth: RwLock<WpAuthentication>,
9599
}
96100

97101
#[uniffi::export]
98102
impl CookiesNonceAuthenticationProvider {
99-
#[uniffi::constructor]
103+
#[uniffi::constructor(name = "new")]
100104
pub fn new(
101105
username: String,
102106
password: String,
103107
details: AutoDiscoveryAttemptSuccess,
104108
request_executor: Arc<dyn RequestExecutor>,
105109
) -> Self {
106110
Self {
111+
site_url: details.api_details.site_url_string(),
112+
username,
113+
password,
114+
request_executor: request_executor.clone(),
115+
nonce_retrieval: RwLock::new(Some(Arc::new(WpRestNonceRetrieval::new(
116+
details,
117+
request_executor.clone(),
118+
)))),
119+
auth: RwLock::new(WpAuthentication::None),
120+
}
121+
}
122+
123+
#[uniffi::constructor(name = "with_site_url")]
124+
pub fn with_site_url(
125+
url: String,
126+
username: String,
127+
password: String,
128+
request_executor: Arc<dyn RequestExecutor>,
129+
) -> Self {
130+
Self {
131+
site_url: url,
107132
username,
108133
password,
109-
nonce_retrieval: Arc::new(WpRestNonceRetrieval::new(details, request_executor)),
134+
request_executor,
135+
nonce_retrieval: RwLock::new(None),
110136
auth: RwLock::new(WpAuthentication::None),
111137
}
112138
}
113139
}
114140

141+
impl CookiesNonceAuthenticationProvider {
142+
fn get_nonce_retrieval(&self) -> Option<Arc<WpRestNonceRetrieval>> {
143+
self.nonce_retrieval
144+
.read()
145+
.expect("If the lock is poisoned, there isn't much we can do")
146+
.as_ref()
147+
.cloned()
148+
}
149+
150+
async fn create_nonce_retrieval_if_needed(
151+
&self,
152+
) -> Result<Arc<WpRestNonceRetrieval>, AutoDiscoveryAttemptFailure> {
153+
if let Some(nonce_retrieval) = self.get_nonce_retrieval() {
154+
return Ok(nonce_retrieval);
155+
}
156+
157+
let client =
158+
WpLoginClient::new_with_default_middleware_pipeline(self.request_executor.clone());
159+
let result = client.api_discovery(self.site_url.clone()).await;
160+
let details = match result.combined_result() {
161+
Ok(details) => details.clone(),
162+
Err(err) => {
163+
return Err(err.clone());
164+
}
165+
};
166+
167+
let nonce_retrieval = Arc::new(WpRestNonceRetrieval::new(
168+
details,
169+
self.request_executor.clone(),
170+
));
171+
172+
*self
173+
.nonce_retrieval
174+
.write()
175+
.expect("If the lock is poisoned, there isn't much we can do") =
176+
Some(nonce_retrieval.clone());
177+
Ok(nonce_retrieval)
178+
}
179+
}
180+
181+
#[uniffi::export]
115182
#[async_trait::async_trait]
116183
impl WpDynamicAuthenticationProvider for CookiesNonceAuthenticationProvider {
117184
fn auth(&self) -> WpAuthentication {
@@ -122,8 +189,12 @@ impl WpDynamicAuthenticationProvider for CookiesNonceAuthenticationProvider {
122189
}
123190

124191
async fn refresh(&self) -> bool {
125-
match self
126-
.nonce_retrieval
192+
let nonce_retrieval = match self.create_nonce_retrieval_if_needed().await {
193+
Ok(value) => value,
194+
Err(_) => return false,
195+
};
196+
197+
match nonce_retrieval
127198
.get_nonce(self.username.clone(), self.password.clone())
128199
.await
129200
{
@@ -140,6 +211,15 @@ impl WpDynamicAuthenticationProvider for CookiesNonceAuthenticationProvider {
140211
}
141212
}
142213

214+
impl Debug for CookiesNonceAuthenticationProvider {
215+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
216+
f.debug_struct("CookiesNonceAuthenticationProvider")
217+
.field("site_url", &self.site_url)
218+
.field("username", &self.username)
219+
.finish()
220+
}
221+
}
222+
143223
#[uniffi::export(with_foreign)]
144224
#[async_trait::async_trait]
145225
pub trait WpDynamicAuthenticationProvider: Send + Sync + Debug {

0 commit comments

Comments
 (0)