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  }