github.com/google/syzkaller@v0.0.0-20240517125934-c0f1611a36d6/pkg/auth/auth.go (about)

     1  // Copyright 2021 syzkaller project authors. All rights reserved.
     2  // Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file.
     3  
     4  // Relies on tokeninfo because it is properly documented:
     5  // https://developers.google.com/identity/protocols/oauth2/openid-connect#validatinganidtoken
     6  
     7  // The client
     8  // The VM that wants to invoke the API:
     9  // 1) Gets a token from the metainfo server with this http request:
    10  //      META=http://metadata.google.internal/computeMetadata/v1
    11  //      AUD=https://syzkaller.appspot.com/api
    12  //      curl -sH 'Metadata-Flavor: Google' \
    13  //           "$META/instance/service-accounts/default/identity?audience=$AUD"
    14  // 2) Invokes /api with header 'Authorization: Bearer <token>'
    15  //
    16  // The AppEngine api server:
    17  // 1) Receive the token, invokes this http request:
    18  //      curl -s "https://oauth2.googleapis.com/tokeninfo?id_token=<token>"
    19  // 2) Checks the resulting JSON having the expected audience and expiration.
    20  // 3) Looks up the permissions in the config using the value of sub.
    21  //
    22  // https://cloud.google.com/iap/docs/signed-headers-howto#retrieving_the_user_identity
    23  // from the IAP docs agrees to trust sub.
    24  
    25  // Package auth contains authentication related code supporting secret
    26  // passwords and oauth2 tokens on GCE.
    27  package auth
    28  
    29  import (
    30  	"encoding/json"
    31  	"fmt"
    32  	"io"
    33  	"net/http"
    34  	"net/url"
    35  	"strconv"
    36  	"strings"
    37  	"time"
    38  )
    39  
    40  const (
    41  	// The official google oauth2 endpoint.
    42  	GoogleTokenInfoEndpoint = "https://oauth2.googleapis.com/tokeninfo"
    43  	// Used in the config map as a prefix to distinguish auth identifiers from secret passwords
    44  	// (which contain arbitrary strings, that can't have this prefix).
    45  	OauthMagic = "OauthSubject:"
    46  )
    47  
    48  // Represent a verification backend.
    49  type Endpoint struct {
    50  	// URL supporting tokeninfo auth2 protocol.
    51  	url string
    52  	// TODO(blackgnezdo): cache tokens with a bit of care for concurrency.
    53  }
    54  
    55  func MakeEndpoint(u string) Endpoint {
    56  	return Endpoint{url: u}
    57  }
    58  
    59  // The JSON representation of JWT claims.
    60  type jwtClaimsParse struct {
    61  	Subject  string `json:"sub"`
    62  	Audience string `json:"aud"`
    63  	// The field in the JSON is a string but contains a UNIX time.
    64  	Expiration string `json:"exp"`
    65  }
    66  
    67  // The typed representation of JWT claims.
    68  type jwtClaims struct {
    69  	Subject  string
    70  	Audience string
    71  	// The app uses the typed value.
    72  	Expiration time.Time
    73  }
    74  
    75  func (auth *Endpoint) queryTokenInfo(tokenValue string) (*jwtClaims, error) {
    76  	resp, err := http.PostForm(auth.url, url.Values{"id_token": {tokenValue}})
    77  	if err != nil {
    78  		return nil, err
    79  	}
    80  	defer resp.Body.Close()
    81  	if resp.StatusCode != http.StatusOK {
    82  		return nil, fmt.Errorf("verification failed %v", resp.StatusCode)
    83  	}
    84  	body, err := io.ReadAll(resp.Body)
    85  	if err != nil {
    86  		return nil, err
    87  	}
    88  	claims := new(jwtClaimsParse)
    89  	if err = json.Unmarshal(body, claims); err != nil {
    90  		return nil, err
    91  	}
    92  	expInt, err := strconv.ParseInt(claims.Expiration, 10, 64)
    93  	if err != nil {
    94  		return nil, err
    95  	}
    96  	r := jwtClaims{
    97  		Subject:    claims.Subject,
    98  		Audience:   claims.Audience,
    99  		Expiration: time.Unix(expInt, 0),
   100  	}
   101  	return &r, nil
   102  }
   103  
   104  // Returns the verified subject value based on the provided header
   105  // value or "" if it can't be determined. A valid result starts with
   106  // auth.OauthMagic. The now parameter is the current time to compare the
   107  // claims against. The authHeader is styled as is typical for HTTP headers
   108  // which carry the tokens prefixed by "Bearer " string.
   109  func (auth *Endpoint) DetermineAuthSubj(now time.Time, authHeader []string) (string, error) {
   110  	if len(authHeader) != 1 || !strings.HasPrefix(authHeader[0], "Bearer") {
   111  		// This is a normal case when the client uses a password.
   112  		return "", nil
   113  	}
   114  	// Values past this point are real authentication attempts. Whether
   115  	// or not they are valid is the question.
   116  	tokenValue := strings.TrimSpace(strings.TrimPrefix(authHeader[0], "Bearer"))
   117  	claims, err := auth.queryTokenInfo(tokenValue)
   118  	if err != nil {
   119  		return "", err
   120  	}
   121  	if claims.Audience != DashboardAudience {
   122  		err := fmt.Errorf("unexpected audience %v", claims.Audience)
   123  		return "", err
   124  	}
   125  	if claims.Expiration.Before(now) {
   126  		err := fmt.Errorf("token past expiration %v", claims.Expiration)
   127  		return "", err
   128  	}
   129  	return OauthMagic + claims.Subject, nil
   130  }