go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/server/auth/openid/gcevm.go (about) 1 // Copyright 2022 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 "fmt" 20 "strings" 21 22 "go.chromium.org/luci/auth/identity" 23 "go.chromium.org/luci/common/errors" 24 "go.chromium.org/luci/common/logging" 25 26 "go.chromium.org/luci/server/auth" 27 "go.chromium.org/luci/server/auth/signing" 28 ) 29 30 // GoogleComputeAuthMethod implements auth.Method by checking a header which is 31 // expected to have an OpenID Connect ID token generated via GCE VM identity 32 // metadata endpoint. 33 // 34 // Such tokens identify a particular VM via `google.compute_engine` claim. ID 35 // tokens without that claim (even if they pass the signature checks) are 36 // rejected. 37 // 38 // The authenticated identity has form "bot:<instance-name>@gce.<project>", but 39 // instead of parsing it, better to use GetGoogleComputeTokenInfo to get the 40 // information extracted from the token in a structured form. 41 type GoogleComputeAuthMethod struct { 42 // Header is a HTTP header to read the token from. Required. 43 Header string 44 // AudienceCheck is a callback to use to check tokens audience. Required. 45 AudienceCheck func(ctx context.Context, r auth.RequestMetadata, aud string) (valid bool, err error) 46 47 // certs are used in tests in place of Google certificates. 48 certs *signing.PublicCertificates 49 } 50 51 // Make sure all extra interfaces are implemented. 52 var _ interface { 53 auth.Method 54 auth.Warmable 55 } = (*GoogleComputeAuthMethod)(nil) 56 57 // GoogleComputeTokenInfo contains information extracted from the GCM VM token. 58 type GoogleComputeTokenInfo struct { 59 // Audience is the audience in the token as it was checked by AudienceCheck. 60 Audience string 61 // ServiceAccount is the service account email the GCE VM runs under. 62 ServiceAccount string 63 // Instance is a GCE VM instance name asserted in the token. 64 Instance string 65 // Zone is the GCE zone with the VM. 66 Zone string 67 // Project is a GCP project name the VM belong to. 68 Project string 69 } 70 71 // GetGoogleComputeTokenInfo returns GCE VM info as asserted by the VM token. 72 // 73 // Works only from within a request handler and only if the call was 74 // authenticated via a GCE VM token. In all other cases (anonymous calls, calls 75 // authenticated via some other mechanism, etc.) returns nil. 76 func GetGoogleComputeTokenInfo(ctx context.Context) *GoogleComputeTokenInfo { 77 info, _ := auth.CurrentUser(ctx).Extra.(*GoogleComputeTokenInfo) 78 return info 79 } 80 81 // Authenticate extracts user information from the incoming request. 82 // 83 // It returns: 84 // - (*User, nil, nil) on success. 85 // - (nil, nil, nil) if the method is not applicable. 86 // - (nil, nil, error) if the method is applicable, but credentials are bad. 87 func (m *GoogleComputeAuthMethod) Authenticate(ctx context.Context, r auth.RequestMetadata) (*auth.User, auth.Session, error) { 88 token := strings.TrimSpace(strings.TrimPrefix(r.Header(m.Header), "Bearer ")) 89 if token == "" { 90 return nil, nil, nil // skip this auth method 91 } 92 93 // Grab root Google OAuth2 keys to verify JWT signature. They are most likely 94 // already cached in the process memory. 95 certs := m.certs 96 if certs == nil { 97 var err error 98 if certs, err = signing.FetchGoogleOAuth2Certificates(ctx); err != nil { 99 return nil, nil, err 100 } 101 } 102 103 // Verify and deserialize the token. GCE VM tokens are always issued by 104 // accounts.google.com. 105 verifiedToken, err := VerifyIDToken(ctx, token, certs, "https://accounts.google.com") 106 if err != nil { 107 return nil, nil, err 108 } 109 110 // Tokens can either be in "full" or "standard" format. We want "full", since 111 // "standard" doesn't have details about the VM. 112 if verifiedToken.Google.ComputeEngine.ProjectID == "" { 113 return nil, nil, errors.Reason("no google.compute_engine in the GCE VM token, use 'full' format").Err() 114 } 115 116 // Convert "<realm>:<project>" to "<project>.<realm>" for "bot:..." string. 117 domain := verifiedToken.Google.ComputeEngine.ProjectID 118 if chunks := strings.SplitN(domain, ":", 2); len(chunks) == 2 { 119 domain = fmt.Sprintf("%s.%s", chunks[1], chunks[0]) 120 } 121 122 // Generate some "bot" identity just to have something representative in the 123 // context for e.g. logs. This also verifies there are no funky characters 124 // in the instance and project names. Full information about the token will be 125 // exposed via GoogleComputeTokenInfo in auth.User.Extra. 126 ident, err := identity.MakeIdentity(fmt.Sprintf("bot:%s@gce.%s", 127 verifiedToken.Google.ComputeEngine.InstanceName, domain)) 128 if err != nil { 129 return nil, nil, err 130 } 131 132 // Check the audience in the token. 133 if m.AudienceCheck == nil { 134 return nil, nil, errors.Reason("GoogleComputeAuthMethod has no AudienceCheck").Err() 135 } 136 switch valid, err := m.AudienceCheck(ctx, r, verifiedToken.Aud); { 137 case err != nil: 138 return nil, nil, err 139 case !valid: 140 logging.Errorf(ctx, "openid: GCE VM token from %s has unrecognized audience %q", ident, verifiedToken.Aud) 141 return nil, nil, auth.ErrBadAudience 142 } 143 144 // Success. 145 return &auth.User{ 146 Identity: ident, 147 Extra: &GoogleComputeTokenInfo{ 148 Audience: verifiedToken.Aud, 149 ServiceAccount: verifiedToken.Email, 150 Instance: verifiedToken.Google.ComputeEngine.InstanceName, 151 Zone: verifiedToken.Google.ComputeEngine.Zone, 152 Project: verifiedToken.Google.ComputeEngine.ProjectID, 153 }, 154 }, nil, nil 155 } 156 157 // Warmup prepares local caches. It's optional. 158 // 159 // Implements auth.Warmable. 160 func (m *GoogleComputeAuthMethod) Warmup(ctx context.Context) error { 161 _, err := signing.FetchGoogleOAuth2Certificates(ctx) 162 return err 163 }