go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/server/auth/openid/id_token_method_test.go (about)

     1  // Copyright 2020 The LUCI Authors.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package openid
    16  
    17  import (
    18  	"context"
    19  	"encoding/json"
    20  	"fmt"
    21  	"net/http"
    22  	"net/http/httptest"
    23  	"testing"
    24  	"time"
    25  
    26  	"go.chromium.org/luci/common/clock"
    27  	"go.chromium.org/luci/common/clock/testclock"
    28  
    29  	"go.chromium.org/luci/server/auth"
    30  	"go.chromium.org/luci/server/auth/authtest"
    31  	"go.chromium.org/luci/server/auth/signing/signingtest"
    32  	"go.chromium.org/luci/server/caching"
    33  
    34  	. "github.com/smartystreets/goconvey/convey"
    35  	. "go.chromium.org/luci/common/testing/assertions"
    36  )
    37  
    38  func TestGoogleIDTokenAuthMethod(t *testing.T) {
    39  	t.Parallel()
    40  
    41  	ctx := context.Background()
    42  	ctx = caching.WithEmptyProcessCache(ctx)
    43  	ctx = authtest.MockAuthConfig(ctx)
    44  	ctx, _ = testclock.UseTime(ctx, time.Unix(1442540000, 0))
    45  
    46  	provider := &fakeIdentityProvider{
    47  		Signer:       signingtest.NewSigner(nil),
    48  		SigningKeyID: "signing-key",
    49  		Issuer:       "https://issuer.example.com",
    50  	}
    51  	provider.start()
    52  	defer provider.stop()
    53  
    54  	const fakeHost = "fake-host.example.com"
    55  
    56  	method := GoogleIDTokenAuthMethod{
    57  		Audience:      []string{"aud1", "aud2"},
    58  		AudienceCheck: AudienceMatchesHost,
    59  		discoveryURL:  provider.discoveryURL,
    60  	}
    61  	call := func(authHeader string) (*auth.User, error) {
    62  		req := authtest.NewFakeRequestMetadata()
    63  		req.FakeHost = fakeHost
    64  		req.FakeHeader.Set("Authorization", authHeader)
    65  		u, _, err := method.Authenticate(ctx, req)
    66  		return u, err
    67  	}
    68  
    69  	Convey("Skipped if no header", t, func() {
    70  		user, err := call("")
    71  		So(err, ShouldBeNil)
    72  		So(user, ShouldBeNil)
    73  	})
    74  
    75  	Convey("Skipped if not Bearer", t, func() {
    76  		user, err := call("OAuth zzz")
    77  		So(err, ShouldBeNil)
    78  		So(user, ShouldBeNil)
    79  	})
    80  
    81  	Convey("Not JWT token and SkipNonJWT == false", t, func() {
    82  		user, err := call("Bearer " + "im-not-a-jwt")
    83  		So(err, ShouldErrLike, "bad ID token: bad JWT")
    84  		So(user, ShouldBeNil)
    85  	})
    86  
    87  	Convey("Not JWT token and SkipNonJWT == true", t, func() {
    88  		method.SkipNonJWT = true
    89  
    90  		user, err := call("Bearer " + "im-not-a-jwt")
    91  		So(err, ShouldBeNil)
    92  		So(user, ShouldBeNil)
    93  	})
    94  
    95  	Convey("Regular user", t, func() {
    96  		Convey("Happy path", func() {
    97  			user, err := call("Bearer " + provider.mintIDToken(ctx, IDToken{
    98  				Iss:           provider.Issuer,
    99  				EmailVerified: true,
   100  				Sub:           "some-sub",
   101  				Email:         "user@example.com",
   102  				Name:          "Some Dude",
   103  				Picture:       "https://picture/url/s64/photo.jpg",
   104  				Aud:           "some-client-id",
   105  				Iat:           clock.Now(ctx).Unix(),
   106  				Exp:           clock.Now(ctx).Add(time.Hour).Unix(),
   107  			}))
   108  			So(err, ShouldBeNil)
   109  			So(user, ShouldResemble, &auth.User{
   110  				Identity: "user:user@example.com",
   111  				Email:    "user@example.com",
   112  				Name:     "Some Dude",
   113  				Picture:  "https://picture/url/s64/photo.jpg",
   114  				ClientID: "some-client-id",
   115  			})
   116  		})
   117  
   118  		Convey("Expired token", func() {
   119  			_, err := call("Bearer " + provider.mintIDToken(ctx, IDToken{
   120  				Iss:           provider.Issuer,
   121  				EmailVerified: true,
   122  				Sub:           "some-sub",
   123  				Email:         "user@example.com",
   124  				Name:          "Some Dude",
   125  				Picture:       "https://picture/url/s64/photo.jpg",
   126  				Aud:           "some-client-id",
   127  				Iat:           clock.Now(ctx).Add(-2 * time.Hour).Unix(),
   128  				Exp:           clock.Now(ctx).Add(-1 * time.Hour).Unix(),
   129  			}))
   130  			So(err, ShouldErrLike, "bad ID token: expired")
   131  		})
   132  	})
   133  
   134  	Convey("Service account", t, func() {
   135  		Convey("Happy path using Audience field", func() {
   136  			user, err := call("Bearer " + provider.mintIDToken(ctx, IDToken{
   137  				Iss:           provider.Issuer,
   138  				EmailVerified: true,
   139  				Sub:           "some-sub",
   140  				Email:         "example@example.gserviceaccount.com",
   141  				Aud:           "aud2",
   142  				Iat:           clock.Now(ctx).Unix(),
   143  				Exp:           clock.Now(ctx).Add(time.Hour).Unix(),
   144  			}))
   145  			So(err, ShouldBeNil)
   146  			So(user, ShouldResemble, &auth.User{
   147  				Identity: "user:example@example.gserviceaccount.com",
   148  				Email:    "example@example.gserviceaccount.com",
   149  			})
   150  		})
   151  
   152  		Convey("Happy path using AudienceCheck field (direct host hit)", func() {
   153  			user, err := call("Bearer " + provider.mintIDToken(ctx, IDToken{
   154  				Iss:           provider.Issuer,
   155  				EmailVerified: true,
   156  				Sub:           "some-sub",
   157  				Email:         "example@example.gserviceaccount.com",
   158  				Aud:           "https://" + fakeHost,
   159  				Iat:           clock.Now(ctx).Unix(),
   160  				Exp:           clock.Now(ctx).Add(time.Hour).Unix(),
   161  			}))
   162  			So(err, ShouldBeNil)
   163  			So(user, ShouldResemble, &auth.User{
   164  				Identity: "user:example@example.gserviceaccount.com",
   165  				Email:    "example@example.gserviceaccount.com",
   166  			})
   167  		})
   168  
   169  		Convey("Happy path using AudienceCheck field (host prefix hit)", func() {
   170  			user, err := call("Bearer " + provider.mintIDToken(ctx, IDToken{
   171  				Iss:           provider.Issuer,
   172  				EmailVerified: true,
   173  				Sub:           "some-sub",
   174  				Email:         "example@example.gserviceaccount.com",
   175  				Aud:           "https://" + fakeHost + "/some/path",
   176  				Iat:           clock.Now(ctx).Unix(),
   177  				Exp:           clock.Now(ctx).Add(time.Hour).Unix(),
   178  			}))
   179  			So(err, ShouldBeNil)
   180  			So(user, ShouldResemble, &auth.User{
   181  				Identity: "user:example@example.gserviceaccount.com",
   182  				Email:    "example@example.gserviceaccount.com",
   183  			})
   184  		})
   185  
   186  		Convey("Happy path using OAuth2 client ID", func() {
   187  			user, err := call("Bearer " + provider.mintIDToken(ctx, IDToken{
   188  				Iss:           provider.Issuer,
   189  				EmailVerified: true,
   190  				Sub:           "some-sub",
   191  				Email:         "example@example.gserviceaccount.com",
   192  				Aud:           "blah.apps.googleusercontent.com",
   193  				Iat:           clock.Now(ctx).Unix(),
   194  				Exp:           clock.Now(ctx).Add(time.Hour).Unix(),
   195  			}))
   196  			So(err, ShouldBeNil)
   197  			So(user, ShouldResemble, &auth.User{
   198  				Identity: "user:example@example.gserviceaccount.com",
   199  				Email:    "example@example.gserviceaccount.com",
   200  				ClientID: "blah.apps.googleusercontent.com",
   201  			})
   202  		})
   203  
   204  		Convey("Unknown audience", func() {
   205  			_, err := call("Bearer " + provider.mintIDToken(ctx, IDToken{
   206  				Iss:           provider.Issuer,
   207  				EmailVerified: true,
   208  				Sub:           "some-sub",
   209  				Email:         "example@example.gserviceaccount.com",
   210  				Aud:           "what is this",
   211  				Iat:           clock.Now(ctx).Unix(),
   212  				Exp:           clock.Now(ctx).Add(time.Hour).Unix(),
   213  			}))
   214  			So(err, ShouldEqual, auth.ErrBadAudience)
   215  		})
   216  	})
   217  }
   218  
   219  type fakeIdentityProvider struct {
   220  	Signer       *signingtest.Signer
   221  	SigningKeyID string
   222  	Issuer       string
   223  
   224  	ts           *httptest.Server
   225  	discoveryURL string
   226  }
   227  
   228  func (f *fakeIdentityProvider) start() {
   229  	jwks := jwksForTest(f.SigningKeyID, &f.Signer.KeyForTest().PublicKey)
   230  
   231  	// Serve the fake discovery document and singing keys.
   232  	f.ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
   233  		switch r.URL.Path {
   234  		case "/discovery":
   235  			w.Write([]byte(fmt.Sprintf(`{
   236  					"issuer": "%s",
   237  					"jwks_uri": "%s/jwks"
   238  				}`, f.Issuer, f.ts.URL)))
   239  		case "/jwks":
   240  			json.NewEncoder(w).Encode(jwks)
   241  		default:
   242  			http.Error(w, "Not found", http.StatusNotFound)
   243  		}
   244  	}))
   245  
   246  	f.discoveryURL = f.ts.URL + "/discovery"
   247  }
   248  
   249  func (f *fakeIdentityProvider) stop() {
   250  	f.ts.Close()
   251  }
   252  
   253  func (f *fakeIdentityProvider) mintIDToken(ctx context.Context, tok IDToken) string {
   254  	return idTokenForTest(ctx, &tok, f.SigningKeyID, f.Signer)
   255  }