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 }