Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for signInWithPassword #76

Open
Zagitta opened this issue Apr 26, 2023 · 5 comments
Open

Support for signInWithPassword #76

Zagitta opened this issue Apr 26, 2023 · 5 comments

Comments

@Zagitta
Copy link

Zagitta commented Apr 26, 2023

Greetings!

A while back I asked about signInWithPassword in firestore-rs where I was clearly confused 😉
Since then I've figured stuff out and actually managed to implement support for email/password auth in gcloud-sdk-rs however I don't think the implementation is very good or particularly in line with what you'd wish for.

I've basically added the following in credentials.rs:

#[async_trait]
impl Source for Credentials {
    async fn token(&self) -> crate::error::Result<Token> {
        match self {
            Credentials::ServiceAccount(sa) => jwt::token(sa).await,
            Credentials::User(user) => oauth2::token(user).await,
            Credentials::UserAccount(user_account) => user_account::token(user_account).await,
            Credentials::ExternalAccount(external_account) => {
                external_account::token(external_account).await
            }
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct UserAccount {
    pub api_key: SecretValue,
    pub email: String,
    pub password: SecretValue,
}

mod user_account {

    use std::{convert::TryFrom, ops::Add};

    use secret_vault_value::SecretValue;

    use crate::token_source::{
        credentials::{httpc_post, UserAccount},
        Token,
    };

    #[derive(serde::Serialize)]
    struct Payload<'a> {
        key: &'a str,
        email: &'a str,
        password: &'a str,
        return_secure_token: bool,
    }

    #[derive(Debug, serde::Serialize, serde::Deserialize)]
    #[serde(rename_all = "camelCase")]
    struct Response {
        kind: String,
        id_token: SecretValue,
        expires_in: String,
    }

    impl TryFrom<Response> for Token {
        type Error = crate::error::Error;

        fn try_from(v: Response) -> Result<Self, Self::Error> {
            let expires_in = v.expires_in.parse().unwrap();
            if v.kind.is_empty() || v.id_token.as_sensitive_bytes().is_empty() || expires_in == 0 {
                Err(crate::error::ErrorKind::TokenData.into())
            } else {
                Ok(Token {
                    type_: "Bearer".into(),
                    token: v.id_token,
                    expiry: chrono::Utc::now().add(chrono::Duration::seconds(expires_in)),
                })
            }
        }
    }

    const TOKEN_URL: &str = "https://identitytoolkit.googleapis.com/v1/accounts:signInWithPassword";

    pub async fn token(user: &UserAccount) -> crate::error::Result<Token> {
        fetch_token(TOKEN_URL, user).await
    }

    pub(super) async fn fetch_token(url: &str, user: &UserAccount) -> crate::error::Result<Token> {
        let req = httpc_post(url).form(&Payload {
            key: &user.api_key.as_sensitive_str(),
            email: &user.email,
            password: user.password.as_sensitive_str(),
            return_secure_token: true,
        });
        let resp = req.send().await?;
        if resp.status().is_success() {
            let resp = resp.json::<Response>().await?;
            Token::try_from(resp)
        } else {
            Err(crate::error::ErrorKind::HttpStatus(resp.status()).into())
        }
    }
}

As well as some minor glue to TokenSource and GoogleEnvironment.
With the above modifications it's possible to use it in firestore-rs like this:

let db = firestore::FirestoreDb::with_options_token_source(
        FirestoreDbOptions::new("app".into()),
        vec![],
        gcloud_sdk::TokenSourceType::UserAccount(gcloud_sdk::UserAccount {
            api_key: "xxxxx".into(),
            email: "[email protected]".into(),
            password: "asdasdasd".into(),
        }),
    )
    .await?;

I'd be happy to submit it as a PR for it if you'd like to have a closer look at it. However, before doing that, I thought I'd create an issue and discuss if you have a better idea of how to implement it 😃

@abdolence
Copy link
Owner

abdolence commented Apr 27, 2023

Hey,

First of all, PRs always welcome :) So, I'll review it.

I mostly curious though why would you prefer this over service accounts? Can you describe a bit your case to understand what benefits this give you? (Again, even if they are rare I don't have anything against adding this, I just want to clarify it for myself - the needs).

@Zagitta
Copy link
Author

Zagitta commented Apr 27, 2023

My use case is that I'm using this app https://brewfather.app/ as part of my beer homebrewing process and while they have a public API it's very limited compared to what the app is actually capable of.
So I started reverse engineering and discovered it's just using firestore combined with the above sign in method.
As such it basically just boils down to me needing to use this in a client context rather than a server context since I don't have a service account to their backend 😃

I'll submit a PR, thank you 🥳

@abdolence
Copy link
Owner

As such it basically just boils down to me needing to use this in a client context rather than a server context since I don't have a service account to their backend

So, you can use the Firestore gRPC API using this approach on their cloud project? Have you tested it already? I find this really interesting.

@Zagitta
Copy link
Author

Zagitta commented Apr 27, 2023

It indeed works! Here's an example of it:

#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DefaultFermentables {
    #[serde(rename = "array_data")]
    pub array_data: Vec<DefaultFermentable>,
}

#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DefaultFermentable {
    pub origin: Option<String>,
    pub supplier: Option<String>,
    pub attenuation: Option<f64>,
    pub ibu_per_amount: Option<f64>,
    pub not_fermentable: Option<bool>,
    pub potential_percentage: Option<f64>,
    #[serde(rename = "type")]
    pub type_field: String,
    pub color: f64,
    pub potential: Option<f64>,
    pub name: String,
    #[serde(rename = "_id")]
    pub id: String,
    pub acid: Option<f64>,
    pub moisture: Option<f64>,
    pub coarse_fine_diff: Option<f64>,
    pub max_in_batch: Option<f64>,
    pub hidden: Option<bool>,
    pub protein: Option<f64>,
    pub notes: Option<String>,
    pub diastatic_power: Option<f64>,
    pub grain_category: Option<String>,
    pub lovibond: Option<f64>,
    pub p_h: Option<f64>,
    pub fgdb: Option<f64>,
}

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let db = firestore::FirestoreDb::with_options_token_source(
        FirestoreDbOptions::new("brewfather-app".into()),
        vec![],
        gcloud_sdk::TokenSourceType::UserAccount(gcloud_sdk::UserAccount {
            api_key: "nope".into(),
            email: "nope".into(),
            password: "nope".into(),
        }),
    )
    .await?;

    let foo = db
        .fluent()
        .select()
        .by_id_in("resources/default_ingredients/default_fermentables")
        .obj::<DefaultFermentables>()
        .one("default_0001")
        .await?;

    println!("{:#?}", &foo.unwrap().array_data[..2]);
    Ok(())
}

prints:

    Finished dev [unoptimized + debuginfo] target(s) in 6.31s
     Running `target\debug\brewmaster.exe`
[
    DefaultFermentable {
        origin: Some(
            "Germany",
        ),
        supplier: Some(
            "Avangard",
        ),
        attenuation: Some(
            0.77,
        ),
        ibu_per_amount: None,
        not_fermentable: None,
        potential_percentage: Some(
            81.0,
        ),
        type_field: "Grain",
        color: 9.5,
        potential: Some(
            1.03726,
        ),
        name: "Munich Malt, Germany",
        id: "default-dea89b1",
        acid: None,
        moisture: None,
        coarse_fine_diff: None,
        max_in_batch: None,
        hidden: None,
        protein: None,
        notes: None,
        diastatic_power: None,
        grain_category: None,
        lovibond: None,
        p_h: None,
        fgdb: None,
    },
    DefaultFermentable {
        origin: Some(
            "Germany",
        ),
        supplier: Some(
            "Avangard",
        ),
        attenuation: Some(
            0.81,
        ),
        ibu_per_amount: None,
        not_fermentable: None,
        potential_percentage: Some(
            80.0,
        ),
        type_field: "Grain",
        color: 3.0,
        potential: Some(
            1.0368,
        ),
        name: "Pale Ale Malt",
        id: "default-4e2cb5c",
        acid: None,
        moisture: None,
        coarse_fine_diff: None,
        max_in_batch: None,
        hidden: None,
        protein: None,
        notes: None,
        diastatic_power: None,
        grain_category: None,
        lovibond: None,
        p_h: None,
        fgdb: None,
    },
]

@abdolence
Copy link
Owner

Well, that's setup I've never heard before, but if it works, then fine with me. Let's review it 👍🏻

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants