blob: bad7b9b954a8ebc3db6bf163acd3f1584868b7cd [file] [log] [blame]
// Copyright 2015 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/json"
"net/http"
"net/http/httptest"
"testing"
"time"
"golang.org/x/oauth2"
"go.chromium.org/luci/common/clock"
"go.chromium.org/luci/common/clock/testclock"
"go.chromium.org/luci/server/caching"
"go.chromium.org/luci/server/caching/cachingtest"
. "github.com/smartystreets/goconvey/convey"
. "go.chromium.org/luci/common/testing/assertions"
)
const testScope = "https://example.com/scopes/user.email"
type tokenInfo struct {
Audience string `json:"aud"`
Email string `json:"email"`
EmailVerified string `json:"email_verified"`
Error string `json:"error_description"`
ExpiresIn string `json:"expires_in"`
Scope string `json:"scope"`
}
func TestGoogleOAuth2Method(t *testing.T) {
t.Parallel()
Convey("with mock backend", t, func(c C) {
ctx := caching.WithEmptyProcessCache(context.Background())
ctx = cachingtest.WithGlobalCache(ctx, map[string]caching.BlobCache{
oauthValidationCache.Parameters().GlobalNamespace: cachingtest.NewBlobCache(),
})
ctx, tc := testclock.UseTime(ctx, testclock.TestRecentTimeUTC)
tc.SetTimerCallback(func(d time.Duration, t clock.Timer) {
if testclock.HasTags(t, "oauth-tokeninfo-retry") {
tc.Add(d)
}
})
goodUser := &User{
Identity: "user:abc@example.com",
Email: "abc@example.com",
ClientID: "client_id",
}
checks := 0
info := tokenInfo{
Audience: "client_id",
Email: "abc@example.com",
EmailVerified: "true",
ExpiresIn: "3600",
Scope: testScope + " other stuff",
}
status := http.StatusOK
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
checks++
w.WriteHeader(status)
c.So(json.NewEncoder(w).Encode(&info), ShouldBeNil)
}))
defer ts.Close()
ctx = ModifyConfig(ctx, func(cfg Config) Config {
cfg.AnonymousTransport = func(context.Context) http.RoundTripper {
return http.DefaultTransport
}
return cfg
})
call := func(header string) (*User, error) {
m := GoogleOAuth2Method{
Scopes: []string{testScope},
tokenInfoEndpoint: ts.URL,
}
req := makeRequest()
req.FakeHeader.Set("Authorization", header)
u, _, err := m.Authenticate(ctx, req)
return u, err
}
Convey("Works", func() {
u, err := call("Bearer access_token")
So(err, ShouldBeNil)
So(u, ShouldResemble, goodUser)
})
Convey("Valid tokens are cached", func() {
u, err := call("Bearer access_token")
So(err, ShouldBeNil)
So(u, ShouldResemble, goodUser)
So(checks, ShouldEqual, 1)
// Hit the process cache.
u, err = call("Bearer access_token")
So(err, ShouldBeNil)
So(u, ShouldResemble, goodUser)
// Hit the global cache by clearing the local one.
ctx = caching.WithEmptyProcessCache(ctx)
u, err = call("Bearer access_token")
So(err, ShouldBeNil)
So(u, ShouldResemble, goodUser)
// No new calls to the token endpoints.
So(checks, ShouldEqual, 1)
// Advance time until the token expires, but the validation outcome is
// still cached.
tc.Add(time.Hour + time.Second)
status = http.StatusBadRequest
// Correctly identified as expired, no new calls to the token endpoint.
_, err = call("Bearer access_token")
So(err, ShouldEqual, ErrBadOAuthToken)
So(checks, ShouldEqual, 1)
// Advance time a bit more until the token is evicted from the cache.
tc.Add(15 * time.Minute)
// Correctly identified as expired, via a call to the token endpoint.
_, err = call("Bearer access_token")
So(err, ShouldEqual, ErrBadOAuthToken)
So(checks, ShouldEqual, 2)
})
Convey("Bad tokens are cached", func() {
status = http.StatusBadRequest
_, err := call("Bearer access_token")
So(err, ShouldEqual, ErrBadOAuthToken)
So(checks, ShouldEqual, 1)
// Hit the process cache.
_, err = call("Bearer access_token")
So(err, ShouldEqual, ErrBadOAuthToken)
// Hit the global cache by clearing the local one.
ctx = caching.WithEmptyProcessCache(ctx)
_, err = call("Bearer access_token")
So(err, ShouldEqual, ErrBadOAuthToken)
// Advance time a little bit, the outcome is still cached.
tc.Add(5 * time.Minute)
_, err = call("Bearer access_token")
So(err, ShouldEqual, ErrBadOAuthToken)
So(checks, ShouldEqual, 1)
// Advance time until the cache entry expire, the token is rechecked.
tc.Add(15 * time.Minute)
_, err = call("Bearer access_token")
So(err, ShouldEqual, ErrBadOAuthToken)
So(checks, ShouldEqual, 2)
})
Convey("Bad header", func() {
_, err := call("broken")
So(err, ShouldErrLike, "oauth: bad Authorization header")
})
Convey("HTTP 500", func() {
status = http.StatusInternalServerError
_, err := call("Bearer access_token")
So(err, ShouldErrLike, "transient error")
})
Convey("Error response", func() {
status = http.StatusBadRequest
info.Error = "OMG, error"
_, err := call("Bearer access_token")
So(err, ShouldEqual, ErrBadOAuthToken)
})
Convey("No email", func() {
info.Email = ""
_, err := call("Bearer access_token")
So(err, ShouldEqual, ErrBadOAuthToken)
})
Convey("Email not verified", func() {
info.EmailVerified = "false"
_, err := call("Bearer access_token")
So(err, ShouldEqual, ErrBadOAuthToken)
})
Convey("Bad expires_in", func() {
info.ExpiresIn = "not a number"
_, err := call("Bearer access_token")
So(err, ShouldErrLike, "transient error") // see the comment in GetTokenInfo
})
Convey("Zero expires_in", func() {
info.ExpiresIn = "0"
_, err := call("Bearer access_token")
So(err, ShouldEqual, ErrBadOAuthToken)
})
Convey("No audience", func() {
info.Audience = ""
_, err := call("Bearer access_token")
So(err, ShouldEqual, ErrBadOAuthToken)
})
Convey("No scope", func() {
info.Scope = ""
_, err := call("Bearer access_token")
So(err, ShouldEqual, ErrBadOAuthToken)
})
Convey("Bad email", func() {
info.Email = "@@@@"
_, err := call("Bearer access_token")
So(err, ShouldEqual, ErrBadOAuthToken)
})
Convey("Missing required scope", func() {
info.Scope = "some other scopes"
_, err := call("Bearer access_token")
So(err, ShouldEqual, ErrBadOAuthToken)
})
})
}
func TestGetUserCredentials(t *testing.T) {
t.Parallel()
ctx := context.Background()
m := GoogleOAuth2Method{}
call := func(hdr string) (*oauth2.Token, error) {
req := makeRequest()
req.FakeHeader.Set("Authorization", hdr)
return m.GetUserCredentials(ctx, req)
}
Convey("Works", t, func() {
tok, err := call("Bearer abc.def")
So(err, ShouldBeNil)
So(tok, ShouldResemble, &oauth2.Token{
AccessToken: "abc.def",
TokenType: "Bearer",
})
})
Convey("Bad headers", t, func() {
_, err := call("")
So(err, ShouldEqual, ErrBadAuthorizationHeader)
_, err = call("abc.def")
So(err, ShouldEqual, ErrBadAuthorizationHeader)
_, err = call("Basic abc.def")
So(err, ShouldEqual, ErrBadAuthorizationHeader)
})
}