golang.org/x/build@v0.0.0-20240506185731-218518f32b70/internal/access/access.go (about) 1 // Copyright 2021 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 package access 6 7 import ( 8 "context" 9 "fmt" 10 "log" 11 "net/http" 12 "time" 13 14 grpcauth "github.com/grpc-ecosystem/go-grpc-middleware/auth" 15 "google.golang.org/api/idtoken" 16 "google.golang.org/grpc" 17 "google.golang.org/grpc/codes" 18 "google.golang.org/grpc/metadata" 19 "google.golang.org/grpc/status" 20 ) 21 22 type contextKeyIAP string 23 24 const ( 25 // contextIAP is the key used to store IAP provided fields in the context. 26 contextIAP contextKeyIAP = contextKeyIAP("IAP-JWT") 27 28 // IAPHeaderJWT is the header IAP stores the JWT token in. 29 iapHeaderJWT = "X-Goog-IAP-JWT-Assertion" 30 // iapHeaderEmail is the header IAP stores the email in. 31 iapHeaderEmail = "X-Goog-Authenticated-User-Email" 32 // iapHeaderID is the header IAP stores the user id in. 33 iapHeaderID = "X-Goog-Authenticated-User-Id" 34 35 // IAPSkipAudienceValidation is the audience string used when the validation is not 36 // necessary. https://pkg.go.dev/google.golang.org/api/idtoken#Validate 37 IAPSkipAudienceValidation = "" 38 ) 39 40 // IAPFields contains the values for the headers retrieved from Identity Aware 41 // Proxy. 42 type IAPFields struct { 43 // Email contains the user's email address 44 // For example, "accounts.google.com:example@gmail.com" 45 Email string 46 // ID contains a unique identifier for the user 47 // For example, "accounts.google.com:userIDvalue" 48 ID string 49 } 50 51 // IAPFromContext retrieves the IAPFields stored in the context if it exists. 52 func IAPFromContext(ctx context.Context) (*IAPFields, error) { 53 v := ctx.Value(contextIAP) 54 if v == nil { 55 return nil, fmt.Errorf("IAP fields not found in context") 56 } 57 iap, ok := v.(IAPFields) 58 if !ok { 59 return nil, fmt.Errorf("context value retrieved does not match expected type") 60 } 61 return &iap, nil 62 } 63 64 func RequireIAPAuthHandler(h http.Handler, audience string) http.Handler { 65 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 66 jwt := r.Header.Get("x-goog-iap-jwt-assertion") 67 if jwt == "" { 68 w.WriteHeader(http.StatusUnauthorized) 69 fmt.Fprintf(w, "must run under IAP\n") 70 return 71 } 72 if err := validateIAPJWT(r.Context(), jwt, audience, idtoken.Validate); err != nil { 73 w.WriteHeader(http.StatusUnauthorized) 74 log.Printf("JWT validation error: %v", err) 75 return 76 } 77 h.ServeHTTP(w, r) 78 }) 79 } 80 81 // iapAuthFunc creates an authentication function used to create a GRPC interceptor. 82 // It ensures that the caller has successfully authenticated via IAP. If the caller 83 // has authenticated, the headers created by IAP will be added to the request scope 84 // context passed down to the server implementation. 85 // https://cloud.google.com/iap/docs/signed-headers-howto 86 func iapAuthFunc(audience string, validatorFn validator) grpcauth.AuthFunc { 87 return func(ctx context.Context) (context.Context, error) { 88 md, ok := metadata.FromIncomingContext(ctx) 89 if !ok { 90 return ctx, status.Error(codes.Internal, codes.Internal.String()) 91 } 92 jwtHeaders := md.Get(iapHeaderJWT) 93 if len(jwtHeaders) == 0 { 94 return ctx, status.Error(codes.Unauthenticated, "IAP JWT not found in request") 95 } 96 if err := validateIAPJWT(ctx, jwtHeaders[0], audience, validatorFn); err != nil { 97 return nil, err 98 } 99 ctx, err := contextWithIAPMD(ctx, md) 100 if err != nil { 101 log.Printf("access: unable to set IAP fields in context: %s", err) 102 return ctx, status.Error(codes.Unauthenticated, "unable to authenticate") 103 } 104 return ctx, nil 105 } 106 } 107 108 func validateIAPJWT(ctx context.Context, jwt, audience string, validatorFn validator) error { 109 payload, err := validatorFn(ctx, jwt, audience) 110 if err != nil { 111 log.Printf("access: error validating JWT: %s", err) 112 return status.Error(codes.Unauthenticated, "unable to authenticate") 113 } 114 if payload.Issuer != "https://cloud.google.com/iap" { 115 log.Printf("access: incorrect issuer: %q", payload.Issuer) 116 return status.Error(codes.Unauthenticated, "incorrect issuer") 117 } 118 if payload.Expires+30 < time.Now().Unix() || payload.IssuedAt-30 > time.Now().Unix() { 119 log.Printf("Bad JWT times: expires %v, issued %v", time.Unix(payload.Expires, 0), time.Unix(payload.IssuedAt, 0)) 120 return status.Error(codes.Unauthenticated, "JWT timestamp invalid") 121 } 122 return nil 123 } 124 125 // contextWithIAPMD copies the headers set by IAP into the context. 126 func contextWithIAPMD(ctx context.Context, md metadata.MD) (context.Context, error) { 127 retrieveFn := func(fmd metadata.MD, mdKey string) (string, error) { 128 val := fmd.Get(mdKey) 129 if len(val) == 0 || val[0] == "" { 130 return "", fmt.Errorf("unable to retrieve %s from GRPC metadata", mdKey) 131 } 132 return val[0], nil 133 } 134 var iap IAPFields 135 var err error 136 if iap.Email, err = retrieveFn(md, iapHeaderEmail); err != nil { 137 return ctx, fmt.Errorf("unable to retrieve metadata field: %s", iapHeaderEmail) 138 } 139 if iap.ID, err = retrieveFn(md, iapHeaderID); err != nil { 140 return ctx, fmt.Errorf("unable to retrieve metadata field: %s", iapHeaderID) 141 } 142 return ContextWithIAP(ctx, iap), nil 143 } 144 145 // ContextWithIAP adds the iap fields to the context. 146 func ContextWithIAP(ctx context.Context, iap IAPFields) context.Context { 147 return context.WithValue(ctx, contextIAP, iap) 148 } 149 150 // RequireIAPAuthUnaryInterceptor creates an authentication interceptor for a GRPC 151 // server. This requires Identity Aware Proxy authentication. Upon a successful authentication 152 // the associated headers will be copied into the request context. 153 func RequireIAPAuthUnaryInterceptor(audience string) grpc.UnaryServerInterceptor { 154 return grpcauth.UnaryServerInterceptor(iapAuthFunc(audience, idtoken.Validate)) 155 } 156 157 // RequireIAPAuthStreamInterceptor creates an authentication interceptor for a GRPC 158 // streaming server. This requires Identity Aware Proxy authentication. Upon a successful 159 // authentication the associated headers will be copied into the request context. 160 func RequireIAPAuthStreamInterceptor(audience string) grpc.StreamServerInterceptor { 161 return grpcauth.StreamServerInterceptor(iapAuthFunc(audience, idtoken.Validate)) 162 } 163 164 // validator is a function type for the validator function. The primary purpose is to be able to 165 // replace the validator function. 166 type validator func(ctx context.Context, token, audiance string) (*idtoken.Payload, error) 167 168 // IAPAudienceGCE returns the jwt audience for GCE and GKE services. 169 // The project number is the numerical GCP project number the service is deployed in. 170 // The service ID is the identifier for the backend service used to route IAP requests. 171 // https://cloud.google.com/iap/docs/signed-headers-howto 172 func IAPAudienceGCE(projectNumber int64, serviceID string) string { 173 return fmt.Sprintf("/projects/%d/global/backendServices/%s", projectNumber, serviceID) 174 } 175 176 // IAPAudienceAppEngine returns the JWT audience for App Engine services. 177 // The project number is the numerical GCP project number the service is deployed in. 178 // The project ID is the textual identifier for the GCP project that the App Engine instance is deployed in. 179 // https://cloud.google.com/iap/docs/signed-headers-howto 180 func IAPAudienceAppEngine(projectNumber int64, projectID string) string { 181 return fmt.Sprintf("/projects/%d/apps/%s", projectNumber, projectID) 182 } 183 184 // FakeContextWithOutgoingIAPAuth adds the iap fields to the metadata of an outgoing GRPC request and 185 // should only be used for testing. 186 func FakeContextWithOutgoingIAPAuth(ctx context.Context, iap IAPFields) context.Context { 187 md := metadata.New(map[string]string{ 188 iapHeaderEmail: iap.Email, 189 iapHeaderID: iap.ID, 190 iapHeaderJWT: "test-jwt", 191 }) 192 return metadata.NewOutgoingContext(ctx, md) 193 } 194 195 // FakeIAPAuthFunc provides a fake IAP authentication validation and should only be used for testing. 196 func FakeIAPAuthFunc() grpcauth.AuthFunc { 197 return iapAuthFunc("TESTING", func(ctx context.Context, token, audience string) (*idtoken.Payload, error) { 198 return &idtoken.Payload{ 199 Issuer: "https://cloud.google.com/iap", 200 Audience: audience, 201 Expires: time.Now().Add(time.Minute).Unix(), 202 IssuedAt: time.Now().Add(-time.Minute).Unix(), 203 }, nil 204 }) 205 } 206 207 // FakeIAPAuthInterceptorOptions provides the GRPC server options for fake IAP authentication 208 // and should only be used for testing. 209 func FakeIAPAuthInterceptorOptions() []grpc.ServerOption { 210 return []grpc.ServerOption{ 211 grpc.UnaryInterceptor(grpcauth.UnaryServerInterceptor(FakeIAPAuthFunc())), 212 grpc.StreamInterceptor(grpcauth.StreamServerInterceptor(FakeIAPAuthFunc())), 213 } 214 }