blob: 0a7a3ee44d0f13d9fd422be27a37485cc9d3a7ad [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 openid
import (
"context"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"testing"
"time"
"go.chromium.org/luci/common/clock"
"go.chromium.org/luci/common/clock/testclock"
"go.chromium.org/luci/server/auth"
"go.chromium.org/luci/server/auth/authtest"
"go.chromium.org/luci/server/auth/signing/signingtest"
"go.chromium.org/luci/server/caching"
. "github.com/smartystreets/goconvey/convey"
. "go.chromium.org/luci/common/testing/assertions"
)
func TestGoogleIDTokenAuthMethod(t *testing.T) {
t.Parallel()
ctx := context.Background()
ctx = caching.WithEmptyProcessCache(ctx)
ctx = authtest.MockAuthConfig(ctx)
ctx, _ = testclock.UseTime(ctx, time.Unix(1442540000, 0))
provider := &fakeIdentityProvider{
Signer: signingtest.NewSigner(nil),
SigningKeyID: "signing-key",
Issuer: "https://issuer.example.com",
}
provider.start()
defer provider.stop()
const fakeHost = "fake-host.example.com"
method := GoogleIDTokenAuthMethod{
Audience: []string{"aud1", "aud2"},
AudienceCheck: AudienceMatchesHost,
discoveryURL: provider.discoveryURL,
}
call := func(authHeader string) (*auth.User, error) {
req := authtest.NewFakeRequestMetadata()
req.FakeHost = fakeHost
req.FakeHeader.Set("Authorization", authHeader)
u, _, err := method.Authenticate(ctx, req)
return u, err
}
Convey("Skipped if no header", t, func() {
user, err := call("")
So(err, ShouldBeNil)
So(user, ShouldBeNil)
})
Convey("Skipped if not Bearer", t, func() {
user, err := call("OAuth zzz")
So(err, ShouldBeNil)
So(user, ShouldBeNil)
})
Convey("Not JWT token and SkipNonJWT == false", t, func() {
user, err := call("Bearer " + "im-not-a-jwt")
So(err, ShouldErrLike, "bad ID token: bad JWT")
So(user, ShouldBeNil)
})
Convey("Not JWT token and SkipNonJWT == true", t, func() {
method.SkipNonJWT = true
user, err := call("Bearer " + "im-not-a-jwt")
So(err, ShouldBeNil)
So(user, ShouldBeNil)
})
Convey("Regular user", t, func() {
Convey("Happy path", func() {
user, err := call("Bearer " + provider.mintIDToken(ctx, IDToken{
Iss: provider.Issuer,
EmailVerified: true,
Sub: "some-sub",
Email: "user@example.com",
Name: "Some Dude",
Picture: "https://picture/url/s64/photo.jpg",
Aud: "some-client-id",
Iat: clock.Now(ctx).Unix(),
Exp: clock.Now(ctx).Add(time.Hour).Unix(),
}))
So(err, ShouldBeNil)
So(user, ShouldResemble, &auth.User{
Identity: "user:user@example.com",
Email: "user@example.com",
Name: "Some Dude",
Picture: "https://picture/url/s64/photo.jpg",
ClientID: "some-client-id",
})
})
Convey("Expired token", func() {
_, err := call("Bearer " + provider.mintIDToken(ctx, IDToken{
Iss: provider.Issuer,
EmailVerified: true,
Sub: "some-sub",
Email: "user@example.com",
Name: "Some Dude",
Picture: "https://picture/url/s64/photo.jpg",
Aud: "some-client-id",
Iat: clock.Now(ctx).Add(-2 * time.Hour).Unix(),
Exp: clock.Now(ctx).Add(-1 * time.Hour).Unix(),
}))
So(err, ShouldErrLike, "bad ID token: expired")
})
})
Convey("Service account", t, func() {
Convey("Happy path using Audience field", func() {
user, err := call("Bearer " + provider.mintIDToken(ctx, IDToken{
Iss: provider.Issuer,
EmailVerified: true,
Sub: "some-sub",
Email: "example@example.gserviceaccount.com",
Aud: "aud2",
Iat: clock.Now(ctx).Unix(),
Exp: clock.Now(ctx).Add(time.Hour).Unix(),
}))
So(err, ShouldBeNil)
So(user, ShouldResemble, &auth.User{
Identity: "user:example@example.gserviceaccount.com",
Email: "example@example.gserviceaccount.com",
})
})
Convey("Happy path using AudienceCheck field (direct host hit)", func() {
user, err := call("Bearer " + provider.mintIDToken(ctx, IDToken{
Iss: provider.Issuer,
EmailVerified: true,
Sub: "some-sub",
Email: "example@example.gserviceaccount.com",
Aud: "https://" + fakeHost,
Iat: clock.Now(ctx).Unix(),
Exp: clock.Now(ctx).Add(time.Hour).Unix(),
}))
So(err, ShouldBeNil)
So(user, ShouldResemble, &auth.User{
Identity: "user:example@example.gserviceaccount.com",
Email: "example@example.gserviceaccount.com",
})
})
Convey("Happy path using AudienceCheck field (host prefix hit)", func() {
user, err := call("Bearer " + provider.mintIDToken(ctx, IDToken{
Iss: provider.Issuer,
EmailVerified: true,
Sub: "some-sub",
Email: "example@example.gserviceaccount.com",
Aud: "https://" + fakeHost + "/some/path",
Iat: clock.Now(ctx).Unix(),
Exp: clock.Now(ctx).Add(time.Hour).Unix(),
}))
So(err, ShouldBeNil)
So(user, ShouldResemble, &auth.User{
Identity: "user:example@example.gserviceaccount.com",
Email: "example@example.gserviceaccount.com",
})
})
Convey("Happy path using OAuth2 client ID", func() {
user, err := call("Bearer " + provider.mintIDToken(ctx, IDToken{
Iss: provider.Issuer,
EmailVerified: true,
Sub: "some-sub",
Email: "example@example.gserviceaccount.com",
Aud: "blah.apps.googleusercontent.com",
Iat: clock.Now(ctx).Unix(),
Exp: clock.Now(ctx).Add(time.Hour).Unix(),
}))
So(err, ShouldBeNil)
So(user, ShouldResemble, &auth.User{
Identity: "user:example@example.gserviceaccount.com",
Email: "example@example.gserviceaccount.com",
ClientID: "blah.apps.googleusercontent.com",
})
})
Convey("Unknown audience", func() {
_, err := call("Bearer " + provider.mintIDToken(ctx, IDToken{
Iss: provider.Issuer,
EmailVerified: true,
Sub: "some-sub",
Email: "example@example.gserviceaccount.com",
Aud: "what is this",
Iat: clock.Now(ctx).Unix(),
Exp: clock.Now(ctx).Add(time.Hour).Unix(),
}))
So(err, ShouldEqual, auth.ErrBadAudience)
})
})
}
type fakeIdentityProvider struct {
Signer *signingtest.Signer
SigningKeyID string
Issuer string
ts *httptest.Server
discoveryURL string
}
func (f *fakeIdentityProvider) start() {
jwks := jwksForTest(f.SigningKeyID, &f.Signer.KeyForTest().PublicKey)
// Serve the fake discovery document and singing keys.
f.ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/discovery":
w.Write([]byte(fmt.Sprintf(`{
"issuer": "%s",
"jwks_uri": "%s/jwks"
}`, f.Issuer, f.ts.URL)))
case "/jwks":
json.NewEncoder(w).Encode(jwks)
default:
http.Error(w, "Not found", http.StatusNotFound)
}
}))
f.discoveryURL = f.ts.URL + "/discovery"
}
func (f *fakeIdentityProvider) stop() {
f.ts.Close()
}
func (f *fakeIdentityProvider) mintIDToken(ctx context.Context, tok IDToken) string {
return idTokenForTest(ctx, &tok, f.SigningKeyID, f.Signer)
}