go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/server/auth/iap/iap_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 iap implements auth.Method for GCP's Identity Aware Proxy.
    16  // It does payload verification according to the guide for using signed
    17  // headers: https://cloud.google.com/iap/docs/signed-headers-howto#verifying_the_jwt_payload
    18  package iap
    19  
    20  import (
    21  	"context"
    22  	"fmt"
    23  
    24  	"google.golang.org/api/idtoken"
    25  
    26  	"go.chromium.org/luci/auth/identity"
    27  	"go.chromium.org/luci/common/errors"
    28  	"go.chromium.org/luci/common/logging"
    29  
    30  	"go.chromium.org/luci/server/auth"
    31  )
    32  
    33  const (
    34  	iapJWTAssertionHeader = "X-Goog-Iap-Jwt-Assertion"
    35  )
    36  
    37  // AudForGAE returns an audience string for the GAE application as it will be formatted
    38  // by IAP in the aseertion headers. This is a convenience method.
    39  // For production use, one should use the cloud.google.com/go/compute/metadata to get
    40  // the NumericProjectID and AppID properties on process startup.
    41  func AudForGAE(numericProjectID, appID string) string {
    42  	return fmt.Sprintf("/projects/%s/apps/%s", numericProjectID, appID)
    43  }
    44  
    45  // AudForGlobalBackendService returns an audience string for a GCE or GKE application as
    46  // it will be formatted by IAP in the aseertion headers. This is a convenience method.
    47  func AudForGlobalBackendService(projectNumber, backendServiceID string) string {
    48  	return fmt.Sprintf("/projects/%s/global/backendServices/%s", projectNumber, backendServiceID)
    49  }
    50  
    51  type idTokenValidator func(ctx context.Context, idToken string, audience string) (*idtoken.Payload, error)
    52  
    53  // IAPAuthMethod implements auth.Method for use with GCP's Identity Aware Proxy.
    54  type IAPAuthMethod struct {
    55  	// Aud is the audience string as it should appear in JWTs intended for
    56  	// validation by your service.
    57  	Aud string
    58  
    59  	// validator field is only intended for use by unit tests.
    60  	validator idTokenValidator
    61  }
    62  
    63  // Authenticate returns nil if no IAP assertion header is present, a User if authentication
    64  // is successful, or an error if unable to validate and identify a user from the assertion header.
    65  func (a *IAPAuthMethod) Authenticate(ctx context.Context, r auth.RequestMetadata) (*auth.User, auth.Session, error) {
    66  	iapJwt := r.Header(iapJWTAssertionHeader)
    67  	if iapJwt == "" {
    68  		logging.Errorf(ctx, "iap: missing assertion header")
    69  		return nil, nil, nil
    70  	}
    71  
    72  	validateFunc := a.validator
    73  	if validateFunc == nil {
    74  		validateFunc = idtoken.Validate
    75  	}
    76  
    77  	jwtPayload, err := validateFunc(ctx, iapJwt, a.Aud)
    78  	if err != nil {
    79  		return nil, nil, errors.Annotate(err, "couldn't validate jwt payload").Err()
    80  	}
    81  
    82  	email, ok := jwtPayload.Claims["email"].(string)
    83  	if !ok {
    84  		return nil, nil, fmt.Errorf("iap: no email claim")
    85  	}
    86  
    87  	return &auth.User{
    88  		Identity: identity.Identity("user:" + email),
    89  		Email:    email,
    90  	}, nil, nil
    91  }