github.com/google/syzkaller@v0.0.0-20251211124644-a066d2bc4b02/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  	var resp *http.Response
    77  	var err error
    78  	for i := 0; i < 3; i++ {
    79  		resp, err = http.PostForm(auth.url, url.Values{"id_token": {tokenValue}})
    80  		if err == nil {
    81  			break
    82  		}
    83  	}
    84  	if err != nil {
    85  		return nil, fmt.Errorf("http.PostForm failed 3 times: %w", err)
    86  	}
    87  	defer resp.Body.Close()
    88  	if resp.StatusCode != http.StatusOK {
    89  		return nil, fmt.Errorf("verification failed %v", resp.StatusCode)
    90  	}
    91  	body, err := io.ReadAll(resp.Body)
    92  	if err != nil {
    93  		return nil, fmt.Errorf("io.ReadAll: %w", err)
    94  	}
    95  	claims := new(jwtClaimsParse)
    96  	if err = json.Unmarshal(body, claims); err != nil {
    97  		return nil, fmt.Errorf("json.Unmarshal: %w", err)
    98  	}
    99  	expInt, err := strconv.ParseInt(claims.Expiration, 10, 64)
   100  	if err != nil {
   101  		return nil, fmt.Errorf("strconv.ParseInt: %w", err)
   102  	}
   103  	r := jwtClaims{
   104  		Subject:    claims.Subject,
   105  		Audience:   claims.Audience,
   106  		Expiration: time.Unix(expInt, 0),
   107  	}
   108  	return &r, nil
   109  }
   110  
   111  // Returns the verified subject value based on the provided header
   112  // value or "" if it can't be determined. A valid result starts with
   113  // auth.OauthMagic. The now parameter is the current time to compare the
   114  // claims against. The authHeader is styled as is typical for HTTP headers
   115  // which carry the tokens prefixed by "Bearer " string.
   116  func (auth *Endpoint) DetermineAuthSubj(now time.Time, authHeader []string) (string, error) {
   117  	if len(authHeader) != 1 || !strings.HasPrefix(authHeader[0], "Bearer") {
   118  		// This is a normal case when the client uses a password.
   119  		return "", nil
   120  	}
   121  	// Values past this point are real authentication attempts. Whether
   122  	// or not they are valid is the question.
   123  	tokenValue := strings.TrimSpace(strings.TrimPrefix(authHeader[0], "Bearer"))
   124  	claims, err := auth.queryTokenInfo(tokenValue)
   125  	if err != nil {
   126  		return "", fmt.Errorf("auth.queryTokenInfo: %w", err)
   127  	}
   128  	if claims.Audience != DashboardAudience {
   129  		return "", fmt.Errorf("unexpected audience %v", claims.Audience)
   130  	}
   131  	if claims.Expiration.Before(now) {
   132  		return "", fmt.Errorf("token past expiration %v", claims.Expiration)
   133  	}
   134  	return OauthMagic + claims.Subject, nil
   135  }