blob: 8dc8217a076c3906ced457f8d0f3c8e3bc4ee57d [file] [log] [blame]
// Copyright 2020 The LUCI Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package auth
import (
"context"
"encoding/base64"
"fmt"
"net/http"
"net/url"
"testing"
"time"
"go.chromium.org/luci/common/clock"
"go.chromium.org/luci/common/clock/testclock"
"go.chromium.org/luci/common/testing/ftt"
"go.chromium.org/luci/common/testing/truth/assert"
"go.chromium.org/luci/common/testing/truth/should"
"go.chromium.org/luci/server/caching"
)
func TestMintIDTokenForServiceAccount(t *testing.T) {
t.Parallel()
ftt.Run("MintIDTokenForServiceAccount works", t, func(t *ftt.Test) {
ctx := context.Background()
ctx, _ = testclock.UseTime(ctx, testclock.TestRecentTimeUTC)
ctx = caching.WithEmptyProcessCache(ctx)
// Will be changed throughout the test.
var returnedToken *Token
var lastRequest string
generateTokenURL := fmt.Sprintf(
"https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/%s:generateIdToken?alt=json",
url.QueryEscape("abc@example.com"))
transport := &clientRPCTransportMock{
cb: func(r *http.Request, body string) string {
lastRequest = body
if r.URL.String() == generateTokenURL {
return fmt.Sprintf(`{"token":"%s"}`, returnedToken.Token)
}
t.Fatalf("Unexpected request to %s", r.URL.String())
return "unknown URL"
},
}
ctx = ModifyConfig(ctx, func(cfg Config) Config {
cfg.AccessTokenProvider = transport.getAccessToken
cfg.AnonymousTransport = transport.getTransport
return cfg
})
token1 := genFakeIDToken(ctx, "token1", time.Hour)
token2 := genFakeIDToken(ctx, "token2", 2*time.Hour)
token3 := genFakeIDToken(ctx, "token3", 2*time.Hour)
returnedToken = token1
tok, err := MintIDTokenForServiceAccount(ctx, MintIDTokenParams{
ServiceAccount: "abc@example.com",
Audience: "aud",
})
assert.Loosely(t, err, should.BeNil)
assert.Loosely(t, tok, should.Match(token1))
// Cached now.
assert.Loosely(t, actorIDTokenCache.lc.CachedLocally(ctx), should.Equal(1))
// On subsequence request the cached token is used.
returnedToken = token2
tok, err = MintIDTokenForServiceAccount(ctx, MintIDTokenParams{
ServiceAccount: "abc@example.com",
Audience: "aud",
})
assert.Loosely(t, err, should.BeNil)
assert.Loosely(t, tok, should.Match(token1)) // old one
// Unless it expires sooner than requested TTL.
clock.Get(ctx).(testclock.TestClock).Add(40 * time.Minute)
tok, err = MintIDTokenForServiceAccount(ctx, MintIDTokenParams{
ServiceAccount: "abc@example.com",
Audience: "aud",
MinTTL: 30 * time.Minute,
})
assert.Loosely(t, err, should.BeNil)
assert.Loosely(t, tok, should.Match(token2))
// Using delegates results in a different cache key.
returnedToken = token3
tok, err = MintIDTokenForServiceAccount(ctx, MintIDTokenParams{
ServiceAccount: "abc@example.com",
Audience: "aud",
Delegates: []string{"d2@example.com", "d1@example.com"},
MinTTL: 30 * time.Minute,
})
assert.Loosely(t, err, should.BeNil)
assert.Loosely(t, tok, should.Match(token3)) // new one
assert.Loosely(t, lastRequest, should.Equal(
`{"delegates":["projects/-/serviceAccounts/d2@example.com","projects/-/serviceAccounts/d1@example.com"],"audience":"aud","includeEmail":true}`))
})
}
func genFakeIDToken(ctx context.Context, name string, exp time.Duration) *Token {
expiry := clock.Now(ctx).Add(exp).Round(time.Second).UTC()
payload := fmt.Sprintf(`{"exp":%d}`, expiry.Unix())
return &Token{
Token: fmt.Sprintf("%s.%s.zzz", name, base64.RawURLEncoding.EncodeToString([]byte(payload))),
Expiry: expiry,
}
}