go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/server/gerritauth/method.go (about)

     1  // Copyright 2021 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 gerritauth
    16  
    17  import (
    18  	"context"
    19  	"time"
    20  
    21  	"go.chromium.org/luci/auth/identity"
    22  	"go.chromium.org/luci/auth/jwt"
    23  	"go.chromium.org/luci/common/clock"
    24  	"go.chromium.org/luci/common/errors"
    25  
    26  	"go.chromium.org/luci/server/auth"
    27  	"go.chromium.org/luci/server/auth/signing"
    28  )
    29  
    30  // Method is the auth.Method instance that checks Gerrit JWTs.
    31  //
    32  // It is initialized by the server module by default. Use it in your production
    33  // code. In tests it is better to construct AuthMethod instances explicitly.
    34  var Method AuthMethod
    35  
    36  // AssertedInfo is information extracted from the JWT signed by Gerrit.
    37  //
    38  // JWTs are usually obtained by Gerrit frontend plugins when they want to make
    39  // an external call on behalf of the Gerrit user. Information contained in JWTs
    40  // identifies the Gerrit end-user (including all their linked Gerrit accounts)
    41  // and the CL the plugin was operating in.
    42  //
    43  // Use GetAssertedInfo(ctx) to grab AssertedInfo from within a request handler.
    44  type AssertedInfo struct {
    45  	User   AssertedUser
    46  	Change AssertedChange
    47  }
    48  
    49  // AssertedUser is part of the Gerrit JWT, it points to a Gerrit user.
    50  type AssertedUser struct {
    51  	AccountID      int64    `json:"account_id"`      // e.g. 1234, local to the Gerrit host
    52  	Emails         []string `json:"emails"`          // list of all user emails
    53  	PreferredEmail string   `json:"preferred_email"` // the email shown in the Gerrit UI
    54  }
    55  
    56  // AssertedChange is part of the Gerrit JWT, it points to a Gerrit CL.
    57  type AssertedChange struct {
    58  	Host         string `json:"host"`          // e.g. "chromium"
    59  	Repository   string `json:"repository"`    // e.g. "infra/infra"
    60  	ChangeNumber int64  `json:"change_number"` // e.g. 1254633
    61  }
    62  
    63  // GetAssertedInfo returns Gerrit CL and user info as asserted in the JWT.
    64  //
    65  // Works only from within a request handler and only if the call was
    66  // authenticated via a Gerrit JWT. In all other cases (anonymous calls, calls
    67  // authenticated via some other mechanism, etc.) returns nil.
    68  func GetAssertedInfo(ctx context.Context) *AssertedInfo {
    69  	info, _ := auth.CurrentUser(ctx).Extra.(*AssertedInfo)
    70  	return info
    71  }
    72  
    73  // AuthMethod is an auth.Method implementation that checks Gerrit JWTs.
    74  //
    75  // On success puts *AssertedInfo into User.Extra field. Use GetAssertedInfo
    76  // to access it.
    77  type AuthMethod struct {
    78  	// Header is a name of the request header to check for JWTs.
    79  	Header string
    80  	// SignerAccounts are emails of services account that sign Gerrit JWTs.
    81  	SignerAccounts []string
    82  	// Audience is an expected "aud" field of JWTs.
    83  	Audience string
    84  
    85  	testCerts *signing.PublicCertificates // for usage in tests
    86  }
    87  
    88  var _ interface {
    89  	auth.Method
    90  	auth.Warmable
    91  } = (*AuthMethod)(nil)
    92  
    93  // gerritJWT is a body of the JWT token produced by Gerrit.
    94  type gerritJWT struct {
    95  	Aud            string         `json:"aud"`
    96  	Iss            string         `json:"iss"`
    97  	Exp            int64          `json:"exp"`
    98  	AssertedUser   AssertedUser   `json:"asserted_user"`
    99  	AssertedChange AssertedChange `json:"asserted_change"`
   100  }
   101  
   102  // isConfigured is true if the method is fully configured and active.
   103  func (m *AuthMethod) isConfigured() bool {
   104  	return m.Header != "" && len(m.SignerAccounts) != 0
   105  }
   106  
   107  // Authenticate extracts user information from the incoming request.
   108  //
   109  // It is part of auth.Method interface.
   110  func (m *AuthMethod) Authenticate(ctx context.Context, r auth.RequestMetadata) (*auth.User, auth.Session, error) {
   111  	if !m.isConfigured() {
   112  		return nil, nil, nil // skip, not configured
   113  	}
   114  
   115  	encodedJWT := r.Header(m.Header)
   116  	if encodedJWT == "" {
   117  		return nil, nil, nil // skip, no auth header
   118  	}
   119  
   120  	// Peek inside the token to see what account it was supposedly signed by.
   121  	var unverifiedTok gerritJWT
   122  	if err := jwt.UnsafeDecode(encodedJWT, &unverifiedTok); err != nil {
   123  		return nil, nil, errors.Annotate(err, "bad Gerrit JWT").Err()
   124  	}
   125  
   126  	// It must be one of the accounts we know.
   127  	knownIssuer := ""
   128  	for _, email := range m.SignerAccounts {
   129  		if email == unverifiedTok.Iss {
   130  			knownIssuer = email
   131  			break
   132  		}
   133  	}
   134  	if knownIssuer == "" {
   135  		return nil, nil, errors.Reason("bad Gerrit JWT: unrecognized issuer %q", unverifiedTok.Iss).Err()
   136  	}
   137  
   138  	// Grab the signing keys we trust. Note: this usually hits the process cache.
   139  	certs := m.testCerts
   140  	if certs == nil {
   141  		var err error
   142  		certs, err = signing.FetchCertificatesForServiceAccount(ctx, knownIssuer)
   143  		if err != nil {
   144  			return nil, nil, errors.Annotate(err, "could not fetch Gerrit public keys").Err()
   145  		}
   146  	}
   147  
   148  	// Verify the signature and deserialize the token.
   149  	var tok gerritJWT
   150  	if err := jwt.VerifyAndDecode(encodedJWT, &tok, certs); err != nil {
   151  		return nil, nil, errors.Annotate(err, "bad Gerrit JWT").Err()
   152  	}
   153  
   154  	// Check the token was addressed to us.
   155  	if tok.Aud != m.Audience {
   156  		return nil, nil, errors.Reason("bad Gerrit JWT: wrong audience %q, expecting %q", tok.Aud, m.Audience).Err()
   157  	}
   158  
   159  	// Check the token expiration time. Allow 30 sec clock skew.
   160  	now := clock.Now(ctx)
   161  	exp := time.Unix(tok.Exp, 0)
   162  	if exp.Add(30 * time.Second).Before(now) {
   163  		return nil, nil, errors.Reason("bad Gerrit JWT: expired %s ago", now.Sub(exp)).Err()
   164  	}
   165  
   166  	// Use "preferred_email", but fallback to "emails[0]" if empty, which
   167  	// theoretically may happen if the preferred email is not backed by an
   168  	// external ID.
   169  	preferredEmail := tok.AssertedUser.PreferredEmail
   170  	if preferredEmail == "" {
   171  		if len(tok.AssertedUser.Emails) == 0 {
   172  			return nil, nil, errors.Reason("bad Gerrit JWT: asserted_user.preferred_email and asserted_user.emails are empty").Err()
   173  		}
   174  		preferredEmail = tok.AssertedUser.Emails[0]
   175  	}
   176  
   177  	// It must be syntactically a valid email address.
   178  	ident, err := identity.MakeIdentity("user:" + preferredEmail)
   179  	if err != nil {
   180  		return nil, nil, errors.Annotate(err, "bad Gerrit JWT: unrecognized email format").Err()
   181  	}
   182  
   183  	// Success.
   184  	return &auth.User{
   185  		Identity: ident,
   186  		Email:    preferredEmail,
   187  		Extra: &AssertedInfo{
   188  			User:   tok.AssertedUser,
   189  			Change: tok.AssertedChange,
   190  		},
   191  	}, nil, nil
   192  }
   193  
   194  // Warmup may be called to precache the data needed by the method.
   195  //
   196  // It is part of auth.Warmable interface.
   197  func (m *AuthMethod) Warmup(ctx context.Context) error {
   198  	if m.isConfigured() && m.testCerts == nil {
   199  		var merr errors.MultiError
   200  		for _, email := range m.SignerAccounts {
   201  			_, err := signing.FetchCertificatesForServiceAccount(ctx, email)
   202  			merr.MaybeAdd(err)
   203  		}
   204  		return merr.AsError()
   205  	}
   206  	return nil
   207  }