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 }