Skip to content

Commit c49a3d9

Browse files
committed
Add initial OAuth 2 support
1 parent 883e7cf commit c49a3d9

File tree

1 file changed

+191
-10
lines changed

1 file changed

+191
-10
lines changed

wp_api/src/login.rs

Lines changed: 191 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ use crate::ParsedUrl;
1111
use crate::WpUuid;
1212

1313
const KEY_APPLICATION_PASSWORDS: &str = "application-passwords";
14+
const KEY_OAUTH_2: &str = "oauth2";
1415

1516
mod login_client;
1617
mod url_discovery;
@@ -57,27 +58,147 @@ pub struct WpApiDetails {
5758
pub gmt_offset: i64,
5859
pub timezone_string: String,
5960
pub namespaces: Vec<String>,
60-
pub authentication: HashMap<String, WpRestApiAuthenticationScheme>,
61+
pub authentication: HashMap<String, AuthenticationProtocol>,
6162
pub site_icon_url: Option<String>,
6263
}
6364

6465
#[uniffi::export]
6566
impl WpApiDetails {
6667
pub fn find_application_passwords_authentication_url(&self) -> Option<String> {
67-
self.authentication
68-
.get(KEY_APPLICATION_PASSWORDS)
69-
.map(|auth_scheme| auth_scheme.endpoints.authorization.clone())
68+
match self.authentication.get(KEY_APPLICATION_PASSWORDS) {
69+
Some(AuthenticationProtocol::ApplicationPassword(scheme)) => {
70+
Some(scheme.endpoints.authorization.clone())
71+
}
72+
_ => None,
73+
}
74+
}
75+
76+
pub fn find_oauth_server_details(&self) -> Option<WpApiOAuth2ServerDetails> {
77+
match self.authentication.get(KEY_OAUTH_2) {
78+
Some(AuthenticationProtocol::OAuth2(scheme)) => Some(scheme.clone().into()),
79+
_ => None,
80+
}
81+
}
82+
83+
pub fn registered_authentication_methods(&self) -> Vec<WpAuthenticationProtocol> {
84+
let mut methods: Vec<WpAuthenticationProtocol> = vec![];
85+
86+
for (name, protocol) in &self.authentication {
87+
methods.push(protocol.clone().into())
88+
}
89+
90+
methods
91+
}
92+
}
93+
94+
#[derive(Debug, Serialize, Deserialize, Clone)]
95+
#[serde(untagged)]
96+
pub enum AuthenticationProtocol {
97+
OAuth2(OAuth2Scheme),
98+
ApplicationPassword(ApplicationPasswordScheme),
99+
Other(UnknownAuthenticationData),
100+
}
101+
102+
#[derive(Debug, Clone, uniffi::Enum)]
103+
pub enum WpAuthenticationProtocol {
104+
OAuth2(WpApiOAuth2ServerDetails),
105+
ApplicationPassword(String),
106+
Other(UnknownAuthenticationData),
107+
}
108+
109+
impl From<AuthenticationProtocol> for WpAuthenticationProtocol {
110+
fn from(protocol: AuthenticationProtocol) -> Self {
111+
match protocol {
112+
AuthenticationProtocol::OAuth2(scheme) => {
113+
WpAuthenticationProtocol::OAuth2(scheme.clone().into())
114+
}
115+
AuthenticationProtocol::ApplicationPassword(scheme) => {
116+
WpAuthenticationProtocol::ApplicationPassword(
117+
scheme.endpoints.authorization.clone(),
118+
)
119+
}
120+
AuthenticationProtocol::Other(scheme) => {
121+
WpAuthenticationProtocol::Other(scheme.clone())
122+
}
123+
}
70124
}
71125
}
72126

73-
#[derive(Debug, Serialize, Deserialize, uniffi::Record)]
74-
pub struct WpRestApiAuthenticationScheme {
75-
pub endpoints: WpRestApiAuthenticationEndpoint,
127+
#[derive(Debug, Serialize, Deserialize, Clone, uniffi::Enum)]
128+
#[serde(untagged)]
129+
pub enum UnknownAuthenticationData {
130+
Bool(bool),
131+
Int(i64),
132+
String(String),
133+
Float(f64),
134+
Object(HashMap<String, UnknownAuthenticationData>),
135+
Dictionary(HashMap<String, String>),
136+
List(Vec<UnknownAuthenticationData>),
137+
}
138+
139+
/// An internal JSON representation of the WP Core `application-passwords` authentication method.
140+
///
141+
#[derive(Debug, Serialize, Deserialize, Clone, uniffi::Record)]
142+
pub struct ApplicationPasswordScheme {
143+
endpoints: ApplicationPasswordEndpoints,
76144
}
77145

78-
#[derive(Debug, Serialize, Deserialize, uniffi::Record)]
79-
pub struct WpRestApiAuthenticationEndpoint {
80-
pub authorization: String,
146+
/// An internal JSON representation of the WP Core `application-passwords` authentication method's endpoints.
147+
#[derive(Debug, Serialize, Deserialize, Clone, uniffi::Record)]
148+
pub struct ApplicationPasswordEndpoints {
149+
authorization: String,
150+
}
151+
152+
/// An internal JSON representation of an `oauth2` authentication method as provided by https://wordpress.org/plugins/oauth2-provider/
153+
/// Provides a fallback for servers that use an `endpoints` key to match the `application-passwords` method.
154+
///
155+
#[derive(Debug, Serialize, Deserialize, Clone)]
156+
#[serde(untagged)]
157+
pub enum OAuth2Scheme {
158+
WithoutEndpointKey(OAuth2SchemeWithoutEndpoint),
159+
WithEndpointKey(OAuth2SchemeWithEndpoint),
160+
}
161+
162+
#[derive(Debug, Serialize, Deserialize, Clone)]
163+
pub struct OAuth2SchemeWithEndpoint {
164+
endpoints: OAuth2Endpoints,
165+
}
166+
167+
/// An internal JSON representation of the `oauth2` authentication method's endpoints.
168+
///
169+
#[derive(Debug, Serialize, Deserialize, Clone)]
170+
pub struct OAuth2Endpoints {
171+
authorization: String,
172+
token: String,
173+
}
174+
175+
#[derive(Debug, Serialize, Deserialize, Clone)]
176+
pub struct OAuth2SchemeWithoutEndpoint {
177+
authorize: String,
178+
token: String,
179+
}
180+
181+
/// A derived representation of `OAuth2Scheme` for clients that normalizes the fields
182+
///
183+
#[derive(Debug, Serialize, Deserialize, Clone, uniffi::Record)]
184+
pub struct WpApiOAuth2ServerDetails {
185+
pub authorization_url: String,
186+
pub token_url: String,
187+
}
188+
189+
impl From<OAuth2Scheme> for WpApiOAuth2ServerDetails {
190+
fn from(scheme: OAuth2Scheme) -> Self {
191+
match scheme {
192+
OAuth2Scheme::WithoutEndpointKey(subscheme) => WpApiOAuth2ServerDetails {
193+
authorization_url: subscheme.authorize.clone(),
194+
token_url: subscheme.token.clone(),
195+
},
196+
OAuth2Scheme::WithEndpointKey(subscheme) => WpApiOAuth2ServerDetails {
197+
authorization_url: subscheme.endpoints.authorization.clone(),
198+
token_url: subscheme.endpoints.token.clone(),
199+
},
200+
}
201+
}
81202
}
82203

83204
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, uniffi::Record)]
@@ -200,4 +321,64 @@ mod tests {
200321
);
201322
assert_eq!(auth_url, ParsedUrl::parse(expected_url.as_str()).unwrap());
202323
}
324+
325+
#[derive(Debug, Serialize, Deserialize)]
326+
struct AuthenticationTest {
327+
authentication: HashMap<String, AuthenticationProtocol>,
328+
}
329+
330+
#[rstest]
331+
#[case(r#"{ "authentication": { } }"#)]
332+
// #[case(r#"{ "authentication": [ ] }"#)] // TODO
333+
fn test_empty_authentication_can_be_parsed(#[case] input_json: &str) {
334+
let test_object: AuthenticationTest = serde_json::from_str(input_json).unwrap();
335+
assert!(test_object.authentication.is_empty())
336+
}
337+
338+
#[rstest]
339+
fn test_authentication_with_valid_application_passwords() {
340+
let input_json = r#"
341+
{ "authentication": { "application-passwords": { "endpoints": { "authorization": "http:\/\/localhost\/wp-admin\/authorize-application.php" } } } }"#;
342+
let test_object: AuthenticationTest = serde_json::from_str(input_json).unwrap();
343+
assert!(matches!(
344+
test_object
345+
.authentication
346+
.get(KEY_APPLICATION_PASSWORDS)
347+
.unwrap(),
348+
AuthenticationProtocol::ApplicationPassword(_)
349+
));
350+
}
351+
352+
#[rstest]
353+
#[case(r#"{ "authentication": { "application-passwords": { } } }"#)]
354+
#[case(r#"{ "authentication": { "application-passwords": [ ] } }"#)]
355+
#[case(r#"{ "authentication": { "application-passwords": { "disabled": true } } }"#)]
356+
#[case(r#"{ "authentication": { "application-passwords": { "florps": 42 } } }"#)]
357+
#[case(r#"{ "authentication": { "application-passwords": { "florps": -42 } } }"#)]
358+
#[case(r#"{ "authentication": { "application-passwords": { "florps": 0.5234 } } }"#)]
359+
fn test_authentication_with_invalid_application_passwords_is_other(#[case] input_json: &str) {
360+
let test_object: AuthenticationTest = serde_json::from_str(input_json).unwrap();
361+
assert!(matches!(
362+
test_object
363+
.authentication
364+
.get(KEY_APPLICATION_PASSWORDS)
365+
.unwrap(),
366+
AuthenticationProtocol::Other(_)
367+
))
368+
}
369+
370+
#[rstest]
371+
#[case(r#"{ "authentication": { "oauth2": { "authorize": "http:\/\/localhost\/oauth\/authorize", "token": "http:\/\/localhost\/oauth\/token", "me": "http:\/\/localhost\/oauth\/me", "version": "2.0", "software": "WP OAuth Server" } } }"#)]
372+
#[case(r#"{ "authentication": { "oauth2": { "endpoints": { "authorization": "https:\/\/public-api.wordpress.com\/oauth2\/authorize", "token": "https:\/\/public-api.wordpress.com\/oauth2\/token" } } } }"#)]
373+
fn test_authentication_with_valid_oauth2(#[case] input_json: &str) {
374+
let test_object: AuthenticationTest = serde_json::from_str(input_json).unwrap();
375+
println!("{:?}", test_object);
376+
assert!(matches!(
377+
test_object
378+
.authentication
379+
.get(KEY_OAUTH_2)
380+
.unwrap(),
381+
AuthenticationProtocol::OAuth2(_)
382+
));
383+
}
203384
}

0 commit comments

Comments
 (0)