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 }