go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/server/auth/openid/id_token_method.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 "strings" 20 21 "go.chromium.org/luci/auth/jwt" 22 "go.chromium.org/luci/common/errors" 23 "go.chromium.org/luci/common/logging" 24 25 "go.chromium.org/luci/server/auth" 26 "go.chromium.org/luci/server/auth/internal" 27 ) 28 29 // GoogleIDTokenAuthMethod implements auth.Method by checking `Authorization` 30 // header which is expected to have an OpenID Connect ID token signed by Google. 31 // 32 // The header value should have form "Bearer <base64 JWT>". 33 // 34 // There are two variants of tokens signed by Google: 35 // - ID tokens identifying end users. They always have an OAuth2 Client ID as 36 // an audience (`aud` field). Their `aud` is placed into User.ClientID, so 37 // it is later checked against an allowlist of client IDs by the LUCI auth 38 // stack. 39 // - ID tokens identifying service accounts. They generally can have anything 40 // at all as an audience, but usually have an URL of the service being 41 // called. Their `aud` is first checked against Audience list and 42 // AudienceCheck callback below. If after these check the audience is still 43 // not recognized, but it looks like a Google OAuth2 Client ID, it is placed 44 // into User.ClientID, to be subjected to the regular check against an 45 // allowlist of OAuth2 Client IDs. 46 type GoogleIDTokenAuthMethod struct { 47 // Audience is a list of allowed audiences for tokens that identify Google 48 // service accounts ("*.gserviceaccount.com" emails). 49 Audience []string 50 51 // AudienceCheck is an optional callback to use to check tokens audience in 52 // case enumerating all expected audiences is not viable. 53 // 54 // Works in conjunction with Audience. Also, just like Audience, this check is 55 // used only for tokens that identify service accounts. 56 AudienceCheck func(ctx context.Context, r auth.RequestMetadata, aud string) (valid bool, err error) 57 58 // SkipNonJWT indicates to ignore tokens that don't look like JWTs. 59 // 60 // This is useful when chaining together multiple auth methods that all search 61 // for tokens in the `Authorization` header. 62 // 63 // If the `Authorization` header contains a malformed JWT and SkipNonJWT is 64 // false, Authenticate would return an error, which eventually would result in 65 // Unauthenticated response code (e.g. HTTP 401). But If SkipNonJWT is true, 66 // Authenticate would return (nil, nil, nil) instead, which (per auth.Method 67 // API) instructs the auth stack to try the next registered authentication 68 // method (or treat the request as anonymous if there are no more methods to 69 // try). 70 SkipNonJWT bool 71 72 // discoveryURL is used in tests to override GoogleDiscoveryURL. 73 discoveryURL string 74 } 75 76 // Make sure all extra interfaces are implemented. 77 var _ interface { 78 auth.Method 79 auth.Warmable 80 } = (*GoogleIDTokenAuthMethod)(nil) 81 82 // AudienceMatchesHost can be used as a AudienceCheck callback. 83 // 84 // It verifies token's audience matches "Host" request header. Suitable for 85 // environments where "Host" header can be trusted. 86 func AudienceMatchesHost(ctx context.Context, r auth.RequestMetadata, aud string) (valid bool, err error) { 87 if host := r.Host(); host != "" { 88 return aud == "https://"+host || strings.HasPrefix(aud, "https://"+host+"/"), nil 89 } 90 return false, nil 91 } 92 93 // Authenticate extracts user information from the incoming request. 94 // 95 // It returns: 96 // - (*User, nil, nil) on success. 97 // - (nil, nil, nil) if the method is not applicable. 98 // - (nil, nil, error) if the method is applicable, but credentials are bad. 99 func (m *GoogleIDTokenAuthMethod) Authenticate(ctx context.Context, r auth.RequestMetadata) (*auth.User, auth.Session, error) { 100 typ, token := internal.SplitAuthHeader(r.Header("Authorization")) 101 if typ != "bearer" { 102 return nil, nil, nil // this auth method is not applicable 103 } 104 105 // Grab (usually already cached) discovery document. 106 doc, err := m.discoveryDoc(ctx) 107 if err != nil { 108 return nil, nil, errors.Annotate(err, "openid: failed to fetch the OpenID discovery doc").Err() 109 } 110 111 // Validate token's signature and expiration. Extract user info from it. 112 tok, user, err := UserFromIDToken(ctx, token, doc) 113 if err != nil { 114 if m.SkipNonJWT && jwt.NotJWT.In(err) { 115 return nil, nil, nil 116 } 117 return nil, nil, err 118 } 119 120 // For tokens identifying end users, populate user.ClientID to let the LUCI 121 // auth stack check it against an allowlist of OAuth2 Client IDs in the 122 // AuthDB. Tokens identifying end users always have OAuth2 Client ID as an 123 // audience. 124 if !strings.HasSuffix(user.Email, ".gserviceaccount.com") { 125 user.ClientID = tok.Aud 126 return user, nil, nil 127 } 128 129 // For service accounts we want to check `aud` right here first, since it is 130 // generally not an OAuth2 Client ID and can be anything at all. 131 for _, aud := range m.Audience { 132 if tok.Aud == aud { 133 return user, nil, nil 134 } 135 } 136 if m.AudienceCheck != nil { 137 switch valid, err := m.AudienceCheck(ctx, r, tok.Aud); { 138 case err != nil: 139 return nil, nil, err 140 case valid: 141 return user, nil, nil 142 } 143 } 144 145 // If unrecognized `aud` looks like Google OAuth2 Client ID, put it into the 146 // returned `user`. This will trigger a check against an allowlist of OAuth2 147 // client IDs. 148 if strings.HasSuffix(tok.Aud, ".apps.googleusercontent.com") { 149 user.ClientID = tok.Aud 150 return user, nil, nil 151 } 152 153 logging.Errorf(ctx, "openid: token from %s has unrecognized audience %q", user.Email, tok.Aud) 154 return nil, nil, auth.ErrBadAudience 155 } 156 157 // Warmup prepares local caches. It's optional. 158 // 159 // Implements auth.Warmable. 160 func (m *GoogleIDTokenAuthMethod) Warmup(ctx context.Context) error { 161 _, err := m.discoveryDoc(ctx) 162 return err 163 } 164 165 // discoveryDoc fetches (and caches) the discovery document. 166 func (m *GoogleIDTokenAuthMethod) discoveryDoc(ctx context.Context) (*DiscoveryDoc, error) { 167 url := GoogleDiscoveryURL 168 if m.discoveryURL != "" { 169 url = m.discoveryURL 170 } 171 return FetchDiscoveryDoc(ctx, url) 172 }