firebase_rs_sdk/auth/oauth/
credential.rs

1use std::convert::TryFrom;
2
3use serde_json::Value;
4use url::form_urlencoded::Serializer;
5
6use crate::auth::error::{AuthError, AuthResult};
7use crate::auth::model::AuthCredential;
8
9/// Wraps the OAuth credential payload returned by popup/redirect handlers.
10///
11/// The credential format mirrors the JS SDK: the handler supplies a JSON blob
12/// describing the provider response (ID token, access token, pending post body).
13/// `OAuthCredential` exposes helpers to build the `postBody` required by the
14/// `signInWithIdp` REST endpoint.
15#[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    /// Builds the `postBody` query string expected by `signInWithIdp`.
59    pub fn build_post_body(&self) -> AuthResult<String> {
60        let mut serializer = Serializer::new(String::new());
61        let mut has_credential = false;
62
63        if let Some(id_token) = self
64            .token_response
65            .get("idToken")
66            .or_else(|| self.token_response.get("oauthIdToken"))
67            .and_then(Value::as_str)
68        {
69            serializer.append_pair("id_token", id_token);
70            has_credential = true;
71        }
72
73        if let Some(access_token) = self
74            .token_response
75            .get("accessToken")
76            .or_else(|| self.token_response.get("oauthAccessToken"))
77            .and_then(Value::as_str)
78        {
79            serializer.append_pair("access_token", access_token);
80            has_credential = true;
81        }
82
83        if let Some(code) = self.token_response.get("code").and_then(Value::as_str) {
84            serializer.append_pair("code", code);
85            has_credential = true;
86        }
87
88        if !has_credential {
89            return Err(AuthError::InvalidCredential(
90                "OAuth token response missing id_token/access_token/code".into(),
91            ));
92        }
93
94        if let Some(nonce) = self.raw_nonce() {
95            serializer.append_pair("nonce", nonce);
96        }
97
98        serializer.append_pair("providerId", &self.provider_id);
99
100        Ok(serializer.finish())
101    }
102}
103
104impl TryFrom<AuthCredential> for OAuthCredential {
105    type Error = AuthError;
106
107    fn try_from(credential: AuthCredential) -> Result<Self, Self::Error> {
108        let raw_nonce = credential
109            .token_response
110            .get("nonce")
111            .and_then(Value::as_str)
112            .map(|value| value.to_owned());
113
114        Ok(Self {
115            provider_id: credential.provider_id,
116            sign_in_method: credential.sign_in_method,
117            raw_nonce,
118            token_response: credential.token_response,
119        })
120    }
121}
122
123#[cfg(test)]
124mod tests {
125    use super::*;
126    use serde_json::json;
127
128    #[test]
129    fn post_body_includes_id_token() {
130        let credential = AuthCredential {
131            provider_id: "google.com".into(),
132            sign_in_method: "google.com".into(),
133            token_response: json!({ "idToken": "test-id-token" }),
134        };
135
136        let oauth = OAuthCredential::try_from(credential).unwrap();
137        let result = oauth.build_post_body().unwrap();
138        assert!(result.contains("id_token=test-id-token"));
139        assert!(result.contains("providerId=google.com"));
140    }
141
142    #[test]
143    fn post_body_includes_access_token() {
144        let credential = AuthCredential {
145            provider_id: "github.com".into(),
146            sign_in_method: "github.com".into(),
147            token_response: json!({ "accessToken": "gh-token" }),
148        };
149
150        let oauth = OAuthCredential::try_from(credential).unwrap();
151        let result = oauth.build_post_body().unwrap();
152        assert!(result.contains("access_token=gh-token"));
153        assert!(result.contains("providerId=github.com"));
154    }
155
156    #[test]
157    fn post_body_errors_when_missing_tokens() {
158        let credential = AuthCredential {
159            provider_id: "google.com".into(),
160            sign_in_method: "google.com".into(),
161            token_response: json!({}),
162        };
163
164        let oauth = OAuthCredential::try_from(credential).unwrap();
165        let err = oauth.build_post_body().unwrap_err();
166        assert!(matches!(err, AuthError::InvalidCredential(_)));
167    }
168}