google-api-go-client icon indicating copy to clipboard operation
google-api-go-client copied to clipboard

idtoken: support authorized_user credential type

Open bouk opened this issue 3 years ago • 20 comments

Right now there's no way to get an ID token if your credentials are user credentials e.g. if you're signed into gcloud locally.

Right now I have to exec out into gcloud auth print-identity-token to get a token, it would be great if this library supported it natively.

bouk avatar Feb 12 '21 21:02 bouk

I managed to work around this, as follows:

import (
	"context"
	"fmt"

	"golang.org/x/oauth2"
	"golang.org/x/oauth2/google"
	"google.golang.org/api/idtoken"
)

func IDTokenTokenSource(ctx context.Context, audience string) (oauth2.TokenSource, error) {
	// First we try the idtoken package, which only works for service accounts
	ts, err := idtoken.NewTokenSource(ctx, audience)
	if err != nil {
		if err.Error() != `idtoken: credential must be service_account, found "authorized_user"` {
			return nil, err
		}
		// If that fails, we use our Application Default Credentials to fetch an id_token on the fly
		gts, err := google.DefaultTokenSource(ctx)
		if err != nil {
			return nil, err
		}
		ts = oauth2.ReuseTokenSource(nil, &idTokenSource{TokenSource: gts})
	}
	return ts, nil
}

// idTokenSource is an oauth2.TokenSource that wraps another
// It takes the id_token from TokenSource and passes that on as a bearer token
type idTokenSource struct {
	TokenSource oauth2.TokenSource
}

func (s *idTokenSource) Token() (*oauth2.Token, error) {
	token, err := s.TokenSource.Token()
	if err != nil {
		return nil, err
	}

	idToken, ok := token.Extra("id_token").(string)
	if !ok {
		return nil, fmt.Errorf("token did not contain an id_token")
	}

	return &oauth2.Token{
		AccessToken: idToken,
		TokenType:   "Bearer",
		Expiry:      token.Expiry,
	}, nil
}

This code will wrap the google oauth TokenSource to pass through its id_token

bouk avatar Feb 16 '21 11:02 bouk

Hey @bouk you are correct that we don't currently support this, but this was done intentionally at the time of writing. The identity token spec we follow states this is optional: AIP 4116. That being said I am curious if the other language libraries we support currently have this feature. @bshaffer do you know if user credentials are used in other languages?

codyoss avatar Feb 16 '21 15:02 codyoss

The python SDK seems to support it at least, since that's what gcloud uses under the hood (I assume?)

bouk avatar Feb 16 '21 17:02 bouk

@bouk It looks like the python auth library only works with service accounts as well. I am not against this request, but I would prefer to be consistent with what our other libraries in other languages are doing.

codyoss avatar Feb 16 '21 22:02 codyoss

~If service accounts can be impersonated it follows that anything that requires a service account should also work with an impersonated service account and the platform has decreed that impersonating a service account is a thing...~

~@codyoss being consistent is great but in this case it would require all the libraries in all the languages be updated at the same time, virtually impossible, some library has to be first.~

~Very keen to see this feature added.~

Edit: thought this issue was that impersonated_service_account wasn't supported.

j0hnsmith avatar Jan 12 '22 11:01 j0hnsmith

Any update on this? Would be nice if we don't have to manually do the workaround mentioned above :) @codyoss

liufuyang avatar Sep 12 '22 08:09 liufuyang

At least it would be great to have suport for impersonated service accounts to test locally.

idtoken: credential must be service_account, found "impersonated_service_account"

tdi avatar Sep 30 '22 10:09 tdi

I hope there is not a "deadlock" in pushing request changes like this, as what we were saying above is that "python lib does that logic, so we gonna do it the same here". I am not sure but I hope it won't be like some python lib users encountered the same and they created some issue (or maybe not as less people use python to work with google-apis?) saying to change this and they got a reply saying "golang package does that so we cannot change". :)

liufuyang avatar Sep 30 '22 14:09 liufuyang

@codyoss would you be open in pull request for at least accepting impersonated_service_account? I think not supporting authorized_user makes sense but impersonated_service_account is a valid ask.

tdi avatar Sep 30 '22 14:09 tdi

@tdi google.FindDefaultCredentials already supports that type of file out of the box.

See code: impersonatedServiceAccount

codyoss avatar Sep 30 '22 14:09 codyoss

ts, err := idtoken.NewTokenSource(ctx, audience)

@codyoss Would this line of code in the above example work when an impersonated_service_account is used?

The idtoken is from "google.golang.org/api/idtoken"

Thank you :)

liufuyang avatar Oct 19 '22 09:10 liufuyang

@codyoss I think it is this line creating problems here https://github.com/googleapis/google-api-go-client/blob/d530a938a76a37ae219aeeba548c197dc171f83b/idtoken/idtoken.go#L139

This blocks impersonated_service_account and authorized_user to be used.

I know you mentioned that on the Python package it is the same logic

But I think perhaps all those packages should update their rules to have impersonated_service_account allowed to pass as well? (Perhaps the change can be done from the golang-packages side?)

liufuyang avatar Oct 19 '22 09:10 liufuyang

Another question, reading this https://google.aip.dev/auth/4116 It says:

When a target audience is provided by the developer, ADC should fetch an ID token with the credentials provided. For example, if service account keys are provided, ADC will request an ID token from the OAuth token endpoint.

But the page seems didn't mention that impersonated_service_account and authorized_user should be blocked? Why don't allow them to follow the same logic as service_account? :)

liufuyang avatar Oct 19 '22 09:10 liufuyang

This package predates impersonated_service_account, so that is why it has not been considered. Maybe enhancements should be made to support that type. I will try to look at this next week to see if there are any technical blockers for this or if it could proceed.

codyoss avatar Oct 20 '22 14:10 codyoss

Ah, that is very nice of you, thank you very much 😃

liufuyang avatar Oct 25 '22 06:10 liufuyang

@codyoss I believe to enable impersonated_service_account, this issue https://github.com/golang/oauth2/issues/600 on the auth2 package side will be needed to be solved first or? Thanks :)

liufuyang avatar Nov 14 '22 15:11 liufuyang

Having some level of support here for a service account or service account impersonation would be awesome for the developer experience. Mostly just commenting here to register a bit more demand and follow any outcome :D

GusPrice avatar Nov 18 '22 16:11 GusPrice

@liufuyang I think this package could support impersonated_service_account after taking a closer look, but might have to do so by reincorporating some of the logic in the oauth2 package here. Glanced at the issue you filed. The reason for all of that is the way the impersonated service accounts authenticate is quite different than a normal flow. They actually are just calling the https://cloud.google.com/iam/docs/reference/credentials/rest/v1/projects.serviceAccounts/generateAccessTokenunder the hood.

This package could do something similar and have a wrapper around calling https://cloud.google.com/iam/docs/reference/credentials/rest/v1/projects.serviceAccounts/generateIdToken. So although it is not as convenient today this can be achieved by calling that API to create a TokenSource. We do have a helper for doing this today with https://pkg.go.dev/google.golang.org/api/impersonate#IDTokenSource, but it does not work auto-magically with impersonated_service_account .

codyoss avatar Nov 18 '22 21:11 codyoss

@codyoss @adrianajg Thank you and I saw some nice code has been merged but has this been correctly implemented?

I tested with google.golang.org/api v0.109.0 and this is what I see if I use an impersonated account: the idToken field has empty value. image

While using a normal service account's json key file, this is what I see:

There is an idToken field in raw containing value. image


I glanced the code a bit while debugging, perhaps we need to set the id_token in the raw field here as well?

https://github.com/googleapis/google-api-go-client/blob/1651c38b919944699bb8a705311c15b6ada8be11/impersonate/idtoken.go#L124-L128

Or, do we supposed to just use that AccessToken field on the Token struct and never use that Extra() method?

Thank you :)

liufuyang avatar Feb 08 '23 15:02 liufuyang

Impersonated service account still unsupported: https://github.com/googleapis/google-api-go-client/blob/b1c45c2251d9c3d524d7ca0170d5becfda31b1d5/idtoken/idtoken.go#L96

Also creating the option via option.ImpersonateCredentials is deprecated and in the docs it is written that this is should be done from different package and also WithTokenSource option:

https://github.com/googleapis/google-api-go-client/blob/b1c45c2251d9c3d524d7ca0170d5becfda31b1d5/option/option.go#L315

which is also unsupported: https://github.com/googleapis/google-api-go-client/blob/b1c45c2251d9c3d524d7ca0170d5becfda31b1d5/idtoken/idtoken.go#L92C8-L92C19

Also regarding this workaround https://github.com/googleapis/google-api-go-client/issues/873#issuecomment-779788114 , I can't use it when working with IAP service, we have to send a different audience and the ts = oauth2.ReuseTokenSource(nil, &idTokenSource{TokenSource: gts}) is using the default audience.

The only thing that works is to download some service account save it a file and run with it locally. which isn't ideal.

brachipa avatar Nov 21 '23 17:11 brachipa