dermesser / yup-oauth2

An oauth2 client implementation providing the Device, Installed, Service Account, and several more flows.
https://docs.rs/yup-oauth2/
Apache License 2.0
223 stars 116 forks source link

ADC Service Impersonation missing client_id #212

Open stevemk14ebr opened 9 months ago

stevemk14ebr commented 9 months ago

Executing:

gcloud auth application-default login --impersonate-service-account <account_name>.iam.gserviceaccount.com

With the code:

async fn get_user_adc_auth() -> Result<Authenticator<HttpsConnector<HttpConnector>>> {
    let home = std::env::var("HOME").unwrap();

    let user_secret = oauth2::read_authorized_user_secret(format!(
        "{}/.config/gcloud/application_default_credentials.json",
        home
    ))
    .await
    .expect("user secret");

    let authenticator = oauth2::AuthorizedUserAuthenticator::builder(user_secret)
             .build()
             .await
             .expect("failed to create authenticator");

    Ok(authenticator)
}

Results in:

user secret: Custom { kind: InvalidData, error: "Bad authorized user secret: missing field `client_id` at line 13 column 1" }
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

This is unexpected, as the client_id value is present

{
  "delegates": [],
  "service_account_impersonation_url": "<redacted>.iam.gserviceaccount.com:ge>
  "source_credentials": {
    "account": "",
    "client_id": "<redacted>.apps.googleusercontent.com",
    "client_secret": "<redacted>",
    "refresh_token": "<redacted>",
    "type": "authorized_user",
    "universe_domain": "googleapis.com"
  },
  "type": "impersonated_service_account"
}

this service_account_impersonation example also fails with this credential but with the error:

Token retrieval failed: JSON Error; this might be a bug with unexpected server responses! missing field `accessToken`

At the time of use instead.

This is important as this gcloud command is the recommended way to do local development without code changes. A user is expected to impersonate as a service account and the application will then behave as if it is live in production actually using said service account.

dermesser commented 9 months ago

can you try just placing the source_credentials object in the credentials file? You will see that it expects different fields than the top-level ones in your example.

SirMishaa commented 6 months ago

@dermesser Also not working for me (Failed to authenticate: JSONError(Error("missing field `accessToken`", line: 17, column: 1))). The documentation is really not clear at all. Despite hours of trying, I'm unable to connect in Impersonation to my service account just to view and add events to my calendar into my principal account.

It would be really great if you can provide a fully working example from A to Z, so the good way to do it. Thank you in advance, it helps a lot.

Reproduce :

Code :

let user_secret =
        read_authorized_user_secret("private/service-account-credentials.json").await?;
    let email = "calendar@homelab-422502.iam.gserviceaccount.com";

    info!("User secret: {:?}", user_secret);

    let auth = ServiceAccountImpersonationAuthenticator::builder(user_secret.clone(), email)
        .build()
        .await
        .expect("authenticator");

    let scopes = &["https://www.googleapis.com/auth/calendar"];
    let token = match auth.token(scopes).await {
        Ok(token) => token,
        Err(e) => {
            error!("Failed to authenticate: {:?}", e);
            process::exit(1);
        }
    };

    info!("Token: {:?}", token);

    let hub = CalendarHub::new(
        hyper::Client::builder().build(
            hyper_rustls::HttpsConnectorBuilder::new()
                .with_native_roots()
                .https_only()
                .enable_http2()
                .build(),
        ),
        auth,
    );

    let calendar = hub.calendar_list().list().doit().await?;

    info!("Calendar ID: {:?}", calendar.1);

Tried following your comment about source_credentials private/service-account-credentials.json:

{
  "account": "",
  "client_id": "<>.apps.googleusercontent.com",
  "client_secret": "<Redacted>",
  "refresh_token": "<Redacted>",
  "type": "authorized_user", // also tried with impersonated_service_account
  "universe_domain": "googleapis.com"
}
stevemk14ebr commented 5 months ago

Seems similar to https://github.com/firebase/firebase-admin-node/issues/1861 like ImpersonatedServiceAccount type needs implemented. Otherwise seems similar to AuthorizedUserAuthenticator

stevemk14ebr commented 4 months ago

I finally found a workaround. First go to your service account in GCP and under permissions click grant access

image

Add your personal user account as a principal to the service account with the service token creator role:

image

Wait a few minutes. And then you can do this:

gcloud auth application-default login

And in rust use those crediantials while impersonating by this:

async fn get_user_impersonation_auth() -> Result<Authenticator<HttpsConnector<HttpConnector>>> {
    let home_dir = env::var("HOME")?;

    // Construct the full path
    let path = format!("{}/{}", home_dir, ".config/gcloud/application_default_credentials.json");

    // Continue with the rest of your code 
    let secret = oauth2::read_authorized_user_secret(&path).await.unwrap(); 
    let service_account = "my_service_account@blah.iam.gserviceaccount.com";
    let authenticator = oauth2::ServiceAccountImpersonationAuthenticator::builder(secret, service_account).build().await?;
    Ok(authenticator)
}