firebase_rs_sdk/auth/oauth/
credential.rs1use std::convert::TryFrom;
2
3use serde_json::{json, Value};
4use url::form_urlencoded::Serializer;
5
6use crate::auth::error::{AuthError, AuthResult};
7use crate::auth::model::AuthCredential;
8
9#[derive(Debug, Clone)]
16pub struct OAuthCredential {
17 provider_id: String,
18 sign_in_method: String,
19 raw_nonce: Option<String>,
20 token_response: Value,
21}
22
23impl OAuthCredential {
24 pub fn new(
25 provider_id: impl Into<String>,
26 sign_in_method: impl Into<String>,
27 token_response: Value,
28 ) -> Self {
29 Self {
30 provider_id: provider_id.into(),
31 sign_in_method: sign_in_method.into(),
32 raw_nonce: None,
33 token_response,
34 }
35 }
36
37 pub fn with_raw_nonce(mut self, nonce: Option<String>) -> Self {
38 self.raw_nonce = nonce;
39 self
40 }
41
42 pub fn provider_id(&self) -> &str {
43 &self.provider_id
44 }
45
46 pub fn sign_in_method(&self) -> &str {
47 &self.sign_in_method
48 }
49
50 pub fn token_response(&self) -> &Value {
51 &self.token_response
52 }
53
54 pub fn raw_nonce(&self) -> Option<&String> {
55 self.raw_nonce.as_ref()
56 }
57
58 pub fn to_json(&self) -> Value {
60 let mut value = json!({
61 "providerId": self.provider_id,
62 "signInMethod": self.sign_in_method,
63 "tokenResponse": self.token_response,
64 });
65 if let Some(nonce) = &self.raw_nonce {
66 value["rawNonce"] = json!(nonce);
67 }
68 value
69 }
70
71 pub fn to_json_string(&self) -> AuthResult<String> {
73 serde_json::to_string(&self.to_json())
74 .map_err(|err| AuthError::InvalidCredential(err.to_string()))
75 }
76
77 pub fn from_json(value: Value) -> AuthResult<Self> {
79 let raw_nonce = value
80 .get("rawNonce")
81 .and_then(Value::as_str)
82 .map(|value| value.to_owned());
83 let auth = AuthCredential::from_json(value)?;
84 let oauth = OAuthCredential::try_from(auth)?;
85 Ok(oauth.with_raw_nonce(raw_nonce))
86 }
87
88 pub fn from_json_str(data: &str) -> AuthResult<Self> {
90 let value: Value = serde_json::from_str(data)
91 .map_err(|err| AuthError::InvalidCredential(err.to_string()))?;
92 Self::from_json(value)
93 }
94
95 pub fn build_post_body(&self) -> AuthResult<String> {
97 let mut serializer = Serializer::new(String::new());
98 let mut has_credential = false;
99
100 if let Some(id_token) = self
101 .token_response
102 .get("idToken")
103 .or_else(|| self.token_response.get("oauthIdToken"))
104 .and_then(Value::as_str)
105 {
106 serializer.append_pair("id_token", id_token);
107 has_credential = true;
108 }
109
110 if let Some(access_token) = self
111 .token_response
112 .get("accessToken")
113 .or_else(|| self.token_response.get("oauthAccessToken"))
114 .and_then(Value::as_str)
115 {
116 serializer.append_pair("access_token", access_token);
117 has_credential = true;
118 }
119
120 if let Some(code) = self.token_response.get("code").and_then(Value::as_str) {
121 serializer.append_pair("code", code);
122 has_credential = true;
123 }
124
125 if let Some(verifier) = self
126 .token_response
127 .get("codeVerifier")
128 .and_then(Value::as_str)
129 {
130 serializer.append_pair("codeVerifier", verifier);
131 }
132
133 if !has_credential {
134 return Err(AuthError::InvalidCredential(
135 "OAuth token response missing id_token/access_token/code".into(),
136 ));
137 }
138
139 if let Some(nonce) = self.raw_nonce() {
140 serializer.append_pair("nonce", nonce);
141 }
142
143 serializer.append_pair("providerId", &self.provider_id);
144
145 Ok(serializer.finish())
146 }
147}
148
149impl From<OAuthCredential> for AuthCredential {
150 fn from(value: OAuthCredential) -> Self {
151 let OAuthCredential {
152 provider_id,
153 sign_in_method,
154 raw_nonce,
155 mut token_response,
156 } = value;
157
158 if let Some(nonce) = raw_nonce {
159 match token_response {
160 Value::Object(ref mut map) => {
161 map.entry("nonce".to_string())
162 .or_insert_with(|| json!(nonce.clone()));
163 }
164 _ => {
165 token_response = json!({
166 "nonce": nonce,
167 });
168 }
169 }
170 }
171
172 Self {
173 provider_id,
174 sign_in_method,
175 token_response,
176 }
177 }
178}
179
180impl TryFrom<AuthCredential> for OAuthCredential {
181 type Error = AuthError;
182
183 fn try_from(credential: AuthCredential) -> Result<Self, Self::Error> {
184 let raw_nonce = credential
185 .token_response
186 .get("nonce")
187 .and_then(Value::as_str)
188 .map(|value| value.to_owned());
189
190 Ok(Self {
191 provider_id: credential.provider_id,
192 sign_in_method: credential.sign_in_method,
193 raw_nonce,
194 token_response: credential.token_response,
195 })
196 }
197}
198
199#[cfg(test)]
200mod tests {
201 use super::*;
202 use serde_json::json;
203
204 #[test]
205 fn post_body_includes_id_token() {
206 let credential = AuthCredential {
207 provider_id: "google.com".into(),
208 sign_in_method: "google.com".into(),
209 token_response: json!({ "idToken": "test-id-token" }),
210 };
211
212 let oauth = OAuthCredential::try_from(credential).unwrap();
213 let result = oauth.build_post_body().unwrap();
214 assert!(result.contains("id_token=test-id-token"));
215 assert!(result.contains("providerId=google.com"));
216 }
217
218 #[test]
219 fn post_body_includes_access_token() {
220 let credential = AuthCredential {
221 provider_id: "github.com".into(),
222 sign_in_method: "github.com".into(),
223 token_response: json!({ "accessToken": "gh-token" }),
224 };
225
226 let oauth = OAuthCredential::try_from(credential).unwrap();
227 let result = oauth.build_post_body().unwrap();
228 assert!(result.contains("access_token=gh-token"));
229 assert!(result.contains("providerId=github.com"));
230 }
231
232 #[test]
233 fn post_body_errors_when_missing_tokens() {
234 let credential = AuthCredential {
235 provider_id: "google.com".into(),
236 sign_in_method: "google.com".into(),
237 token_response: json!({}),
238 };
239
240 let oauth = OAuthCredential::try_from(credential).unwrap();
241 let err = oauth.build_post_body().unwrap_err();
242 assert!(matches!(err, AuthError::InvalidCredential(_)));
243 }
244
245 #[test]
246 fn post_body_includes_code_verifier() {
247 let credential = AuthCredential {
248 provider_id: "twitter.com".into(),
249 sign_in_method: "twitter.com".into(),
250 token_response: json!({
251 "code": "auth-code",
252 "codeVerifier": "verifier"
253 }),
254 };
255
256 let oauth = OAuthCredential::try_from(credential).unwrap();
257 let result = oauth.build_post_body().unwrap();
258 assert!(result.contains("code=auth-code"));
259 assert!(result.contains("codeVerifier=verifier"));
260 }
261
262 #[test]
263 fn oauth_credential_json_roundtrip() {
264 let credential =
265 OAuthCredential::new("apple.com", "apple.com", json!({ "idToken": "token" }))
266 .with_raw_nonce(Some("nonce123".to_string()));
267
268 let json_value = credential.to_json();
269 let restored = OAuthCredential::from_json(json_value.clone()).unwrap();
270 assert_eq!(restored.provider_id(), "apple.com");
271 assert_eq!(restored.raw_nonce(), Some(&"nonce123".to_string()));
272
273 let json_string = credential.to_json_string().unwrap();
274 let from_str = OAuthCredential::from_json_str(&json_string).unwrap();
275 assert_eq!(from_str.provider_id(), "apple.com");
276
277 let auth: AuthCredential = credential.into();
278 assert_eq!(
279 auth.token_response.get("nonce").and_then(Value::as_str),
280 Some("nonce123")
281 );
282 }
283}