diff --git a/src/go/cmd/token-vendor/api/v1/v1_test.go b/src/go/cmd/token-vendor/api/v1/v1_test.go index b53903db..c65f8f1b 100644 --- a/src/go/cmd/token-vendor/api/v1/v1_test.go +++ b/src/go/cmd/token-vendor/api/v1/v1_test.go @@ -27,6 +27,7 @@ import ( const ( testPubKey = "testdata/rsa_cert.pem" jwtBodyPrefix = "grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer&assertion=" + saName = "robot-service@testproject.iam.gserviceaccount.com" jwtCorrect = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJ0ZXN0YXVkIiwiaXNzIjoicm9ib3QtZGV2LXRlc3R1c2VyIiwiZXhwIjoxOTEzMzczMDEwLCJzY29wZXMiOiJ0ZXN0c2NvcGVzIiwiY2xhaW1zIjoidGVzdGNsYWltcyJ9.WJP0shiqynW9ZrmV4k78W3_nn_YA86XLK58IJYyqUF-8LAG92MraNqVqD0t6i-s90VBL64hCXlsA7zP3WlsMHOEvXCyRkGffhbJNIlJqIVTVfGvyF-ZmuaAr352n5kmKTrfTRi7h9LWTcvDgSosN438J8Jy9BT1FE9P-BHfyBUegZ15DWFAiAhz0r_Fgj7hAMXUnRdZfj3_dE0Nhi5IGs3L-0XzU-dE150ZJvtGMdIjc_QCqYHV3wtSgETKDYQoonD08n6g5GqC8nNkqrWFMttafLdPaDAsr8KWtj1dD1w9sw1YJClEzF9JOc63WNPZf8CgdU2enFW-V-2vHbUaekg" jwtWrongSig = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJ0ZXN0YXVkIiwiaXNzIjoicm9ib3QtZGV2LXRlc3R1c2VyIiwiZXhwIjoxOTEzMzczMDEwLCJzY29wZXMiOiIuLi4iLCJjbGFpbXMiOiIuLi4ifQ.krAYHjkConzVudfXJUMiDNbVHF3RwkvOAhSCyTvOaJdlJ6sxh-TjPXo6W0yVT31qjLwhl1NYI-JlhcHX7TLiZbLCbGVXlQN2Nn4LvpbGdAH0KvSJkthqX7ld9tlVQGdlOUHCE5bBDG_9uBtpdOAv1zKUTquhyDM0qWVrQV1qUVOtwBCO6nt21l1eXgTwz50FVN33f1ZmhZfHW1u7Dq_XwBJmHFwN3aiD0NZohU7MpQiz-0u94Q9yZ588IjdZEUhSEUKrVtJjoPcxDhrXxoRMA8iP8_bMeOHteiAdYeBVBwFhu1d8pfcn6uoZROYD1xB1LWDTJx4GfQh6v3wtAwFu7Q" @@ -129,7 +130,7 @@ func runPublicKeyReadHandlerWithK8sCase(t *testing.T, test *publicKeyReadHandler t.Fatal(err) } // Setup app & API handler - tv, err := app.NewTokenVendor(context.TODO(), kcl, nil, nil, "aud") + tv, err := app.NewTokenVendor(context.TODO(), kcl, nil, nil, "aud", saName) if err != nil { t.Fatal(err) } @@ -252,7 +253,7 @@ func runPublicKeyPublishHandlerWithK8sCase(t *testing.T, test *publicKeyPublishH t.Fatal(err) } // Setup app & API handler - tv, err := app.NewTokenVendor(context.TODO(), kcl, nil, nil, "aud") + tv, err := app.NewTokenVendor(context.TODO(), kcl, nil, nil, "aud", saName) if err != nil { t.Fatal(err) } @@ -544,7 +545,7 @@ func runVerifyTokenHandlerTest(t *testing.T, test *VerifyTokenHandlerTest) { if err != nil { t.Fatal(err.Error()) } - tv, err := app.NewTokenVendor(context.TODO(), nil, tver, nil, "aud") + tv, err := app.NewTokenVendor(context.TODO(), nil, tver, nil, "aud", saName) if err != nil { t.Fatal(err.Error()) } @@ -636,7 +637,7 @@ func TestTokenOAuth2HandlerExpired(t *testing.T) { func runTokenOAuth2HandlerTestWithK8s(t *testing.T, test TokenOAuth2HandlerTest) { // fake GCP IAM response for an access token fakeIAMAPI := func(req *http.Request) *http.Response { - const wantUrl = "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/testsa@testproject.iam.gserviceaccount.com:generateAccessToken?alt=json&prettyPrint=false" + const wantUrl = "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/robot-service@testproject.iam.gserviceaccount.com:generateAccessToken?alt=json&prettyPrint=false" if req.URL.String() != wantUrl { t.Fatalf("wrong IAM URL, got %q, want %q", req.URL, wantUrl) } @@ -652,8 +653,7 @@ func runTokenOAuth2HandlerTestWithK8s(t *testing.T, test TokenOAuth2HandlerTest) } // setup app and http client clientIAM := NewTestHTTPClient(fakeIAMAPI) - ts, err := tokensource.NewGCPTokenSource(context.TODO(), clientIAM, "testproject", "testsa", - test.scopes) + ts, err := tokensource.NewGCPTokenSource(context.TODO(), clientIAM, test.scopes) if err != nil { t.Fatal(err) } @@ -677,7 +677,7 @@ func runTokenOAuth2HandlerTestWithK8s(t *testing.T, test TokenOAuth2HandlerTest) if err != nil { t.Fatal(err) } - tv, err := app.NewTokenVendor(context.TODO(), r, nil, ts, test.acceptedAud) + tv, err := app.NewTokenVendor(context.TODO(), r, nil, ts, test.acceptedAud, saName) if err != nil { t.Fatal(err) } @@ -771,7 +771,7 @@ func Test_verifyJWTHandler(t *testing.T) { if err != nil { t.Fatal(err) } - tv, err := app.NewTokenVendor(context.TODO(), r, nil, nil, "testaud") + tv, err := app.NewTokenVendor(context.TODO(), r, nil, nil, "testaud", saName) if err != nil { t.Fatal(err) } diff --git a/src/go/cmd/token-vendor/app/tokenvendor.go b/src/go/cmd/token-vendor/app/tokenvendor.go index 4eb162ba..d5edbd00 100644 --- a/src/go/cmd/token-vendor/app/tokenvendor.go +++ b/src/go/cmd/token-vendor/app/tokenvendor.go @@ -33,17 +33,18 @@ import ( ) type TokenVendor struct { - repo repository.PubKeyRepository - v *oauth.TokenVerifier - ts *tokensource.GCPTokenSource - accAud string + repo repository.PubKeyRepository + v *oauth.TokenVerifier + ts *tokensource.GCPTokenSource + accAud string + defaultSAName string } -func NewTokenVendor(ctx context.Context, repo repository.PubKeyRepository, v *oauth.TokenVerifier, ts *tokensource.GCPTokenSource, acceptedAudience string) (*TokenVendor, error) { +func NewTokenVendor(ctx context.Context, repo repository.PubKeyRepository, v *oauth.TokenVerifier, ts *tokensource.GCPTokenSource, acceptedAudience, defaultSAName string) (*TokenVendor, error) { if acceptedAudience == "" { return nil, errors.New("accepted audience must not be empty") } - return &TokenVendor{repo: repo, v: v, accAud: acceptedAudience, ts: ts}, nil + return &TokenVendor{repo: repo, v: v, ts: ts, accAud: acceptedAudience, defaultSAName: defaultSAName}, nil } func (tv *TokenVendor) PublishPublicKey(ctx context.Context, deviceID, publicKey string) error { @@ -128,6 +129,9 @@ func (tv *TokenVendor) getOAuth2Token(ctx context.Context, jwtk string) (*tokens if err != nil { return nil, err } + if sa == "" { + sa = tv.defaultSAName + } cloudToken, err := tv.ts.Token(ctx, sa) if err != nil { return nil, errors.Wrapf(err, "failed to retrieve a cloud token for device %q", deviceID) diff --git a/src/go/cmd/token-vendor/main.go b/src/go/cmd/token-vendor/main.go index c8ae4f46..dd537fa9 100644 --- a/src/go/cmd/token-vendor/main.go +++ b/src/go/cmd/token-vendor/main.go @@ -139,12 +139,13 @@ func main() { slog.Error("Failed to make verifier", ilog.Err(err)) os.Exit(1) } - ts, err := tokensource.NewGCPTokenSource(ctx, nil, *project, *robotName, scopes) + ts, err := tokensource.NewGCPTokenSource(ctx, nil, scopes) if err != nil { slog.Error("Failed to make token source", ilog.Err(err)) os.Exit(1) } - tv, err := app.NewTokenVendor(ctx, rep, verifier, ts, *acceptedAudience) + saName := fmt.Sprintf("%s@%s.iam.gserviceaccount.com", *robotName, *project) + tv, err := app.NewTokenVendor(ctx, rep, verifier, ts, *acceptedAudience, saName) if err != nil { slog.Error("Failed to make token vendor", ilog.Err(err)) os.Exit(1) diff --git a/src/go/cmd/token-vendor/tokensource/gcp.go b/src/go/cmd/token-vendor/tokensource/gcp.go index d6caf99e..6f6b929b 100644 --- a/src/go/cmd/token-vendor/tokensource/gcp.go +++ b/src/go/cmd/token-vendor/tokensource/gcp.go @@ -15,9 +15,7 @@ import ( type GCPTokenSource struct { service *iam.Service - // the FQN of the service account - defaultResource string - scopes []string + scopes []string } type TokenResponse struct { @@ -34,13 +32,12 @@ type TokenResponse struct { // authentication information is looked up from the environment. // `defaultSAName` specifies the GCP IAM service accoutn name to use if no // dedicated service account is configurred on the key. -func NewGCPTokenSource(ctx context.Context, client *http.Client, project, defaultSAName string, scopes []string) (*GCPTokenSource, error) { +func NewGCPTokenSource(ctx context.Context, client *http.Client, scopes []string) (*GCPTokenSource, error) { service, err := iam.NewService(ctx, option.WithHTTPClient(client)) if err != nil { return nil, errors.Wrap(err, "failed to create IAM service client") } - resource := fmt.Sprintf("projects/-/serviceAccounts/%s@%s.iam.gserviceaccount.com", defaultSAName, project) - return &GCPTokenSource{service: service, defaultResource: resource, scopes: scopes}, nil + return &GCPTokenSource{service: service, scopes: scopes}, nil } // Token returns an access token for the configured service account and scopes. @@ -48,10 +45,10 @@ func NewGCPTokenSource(ctx context.Context, client *http.Client, project, defaul // API: https://cloud.google.com/iam/docs/reference/credentials/rest/v1/projects.serviceAccounts/generateAccessToken func (g *GCPTokenSource) Token(ctx context.Context, saName string) (*TokenResponse, error) { req := iam.GenerateAccessTokenRequest{Scope: g.scopes} - resource := g.defaultResource - if saName != "" { - resource = "projects/-/serviceAccounts/" + saName + if saName == "" { + return nil, fmt.Errorf("saName must not be empty") } + resource := "projects/-/serviceAccounts/" + saName // We don't set a 'lifetime' on the request, so we get the default value (3600 sec = 1h). // This needs to be in sync with the min(cookie-expire,cookie-refresh) duration // configured on oauth2-proxy.