googleapis / google-api-nodejs-client

Google's officially supported Node.js client library for accessing Google APIs. Support for authorization and authentication with OAuth 2.0, API Keys and JWT (Service Tokens) is included.
https://googleapis.dev/nodejs/googleapis/latest/
Apache License 2.0
11.27k stars 1.91k forks source link

Support Google Workspace Impersonation Without Service Account Key #2896

Open jmkrimm opened 2 years ago

jmkrimm commented 2 years ago

Is your feature request related to a problem? Please describe. Trying to authorize a Gmail API request to get a Google Workspace account's Gmail delegates via DWD delegated Service Account without a service account key.

Specifically:

  1. the solution cannot use any key file
  2. the solution must use impersonation (Gmail API call will use a different "subject" than the service account itself)
  3. the solution must use OAuth 2 to interact with the API (this requirement comes from the Gmail API itself)

    Describe the solution you'd like

For the below code to just work. If a new method is needed for this auth that is fine too.

  const auth = new google.auth.GoogleAuth({
    clientOptions: {
      subject: accountEmail // impersonate the user
    },
    scopes: ['https://www.googleapis.com/auth/gmail.readonly']
  });

  const authClient = await auth.getClient();

  const gmail = google.gmail({
      version: 'v1',
      auth: authClient
  });

  const delegatesRes = await gmail.users.settings.delegates.list({
      userId: accountEmail
  });

Describe alternatives you've considered Contacted Google Support and they confirmed there is no way to do this within the current nodejs client library. I am not going to try and create the auth manually without a client library.

Additional context I am running the code on App Engine and using the default service account for App Engine which has been authorized with DWD to Google Workspace. So no key file should be necessary to authorize the Google API requests.

Existing open issue that is related but may not be the exact same use case as me. https://github.com/googleapis/google-auth-library-nodejs/issues/916

This issue is also present in the Python client library but there is at least a workaround. https://github.com/GoogleCloudPlatform/professional-services/blob/master/examples/gce-to-adminsdk/main.py

projektorius96 commented 2 years ago

uplifting to be seen .

jhecking commented 1 year ago

This issue is also present in the Python client library but there is at least a workaround.

Is the same work-around possible using the Node.js client library?

jmkrimm commented 1 year ago

No. The node.js client library does not provide any workaround options.

jhecking commented 1 year ago

I found that I can use IAMCredentialsClient from the @google-cloud/iam-credentials package to sign a JWT for the service account with the required subject address. And I’m wondering whether I could then use the IdTokenClient from the google-auth-library to convert that JWT back into an OAuth2 access token similar to what “gce-to-adminsdk” example does.

So far today I haven’t been able to get this to work yet. But any thoughts on this approach?

jmkrimm commented 1 year ago

Let us know if you get that to work and please share code example.

jhecking commented 1 year ago

The relevant part is this function, which uses the IAMCredentialsClient to sign JWT and use that token to create a new IdTokenClient. But when I try to use that client to query the Google Admin User Directory API, I'm still getting an "Invalid Credentials" error, and I'm not sure why.

function getIdTokenClient (
  authClient: AuthClient,
  targetAudience: string,
  serviceAccountEmail: string,
  subjectEmail: string,
  scopes: string[]
): IdTokenClient {
  const name = `projects/-/serviceAccounts/${serviceAccountEmail}`
  const iamClient = new IAMCredentialsClient({ authClient: authClient as any })
  const idTokenProvider: IdTokenProvider = {
    async fetchIdToken (audience: string): Promise<string> {
      const [{ signedJwt }] = await iamClient.signJwt({
        name,
        payload: JSON.stringify({
          iss: serviceAccountEmail,
          sub: subjectEmail,
          aud: audience,
          scope: scopes.join(' ')
        })
      })
      return signedJwt as string
    }
  }

  const idTokenClient = new IdTokenClient({ idTokenProvider, targetAudience })
  idTokenClient.refreshHandler = async () => { 
    const [{ accessToken, expireTime }] = await iamClient.generateAccessToken({
      name,
      scope: scopes
    })
    return { access_token: accessToken as string, expiry_date: expireTime as number }
  }
  return idTokenClient
}
antnat96 commented 2 weeks ago

This would still be great to have. The workaround in this comment functions as expected but seems like unnecessary risk that the library user has to accept in order to get the same functionality as the Python and Java libraries.