github.com/google/syzkaller@v0.0.0-20240517125934-c0f1611a36d6/pkg/auth/jwt.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 package auth 5 6 import ( 7 "encoding/base64" 8 "encoding/json" 9 "fmt" 10 "io" 11 "net/http" 12 "strings" 13 "sync" 14 "time" 15 ) 16 17 const ( 18 DashboardAudience = "https://syzkaller.appspot.com/api" 19 ) 20 21 type expiringToken struct { 22 value string 23 expiration time.Time 24 } 25 26 // Returns the unverified expiration value from the given JWT token. 27 func extractJwtExpiration(token string) (time.Time, error) { 28 // https://datatracker.ietf.org/doc/html/rfc7519#section-3 29 pieces := strings.Split(token, ".") 30 if len(pieces) != 3 { 31 return time.Time{}, fmt.Errorf("unexpected number of JWT components %v", len(pieces)) 32 } 33 decoded, err := base64.RawURLEncoding.DecodeString(pieces[1]) 34 if err != nil { 35 return time.Time{}, err 36 } 37 claims := struct { 38 Expiration int64 `json:"exp"` 39 }{-123456} // Hopefully a notably broken value. 40 if err = json.Unmarshal(decoded, &claims); err != nil { 41 return time.Time{}, err 42 } 43 return time.Unix(claims.Expiration, 0), nil 44 } 45 46 type ( 47 // The types of ctor and doer are the same as in http.NewRequest and 48 // http.DefaultClient.Do. 49 requestCtor func(method, url string, body io.Reader) (*http.Request, error) 50 requestDoer func(req *http.Request) (*http.Response, error) 51 ) 52 53 // Queries the metadata server and returns the bearer token of the 54 // service account. The token is scoped for the official dashboard. 55 func retrieveJwtToken(ctor requestCtor, doer requestDoer) (*expiringToken, error) { 56 const v1meta = "http://metadata.google.internal/computeMetadata/v1" 57 req, err := ctor("GET", v1meta+"/instance/service-accounts/default/identity?audience="+DashboardAudience, nil) 58 if err != nil { 59 return nil, err 60 } 61 req.Header.Add("Metadata-Flavor", "Google") 62 resp, err := doer(req) 63 if err != nil { 64 return nil, err 65 } 66 defer resp.Body.Close() 67 data, err := io.ReadAll(resp.Body) 68 if err != nil { 69 return nil, err 70 } 71 token := string(data) 72 if resp.StatusCode != http.StatusOK { 73 return nil, fmt.Errorf("failed metadata get %v: %s", resp.Status, token) 74 } 75 expiration, err := extractJwtExpiration(token) 76 if err != nil { 77 return nil, err 78 } 79 return &expiringToken{token, expiration}, nil 80 } 81 82 // TokenCache keeps the tokens for reuse by Get. 83 type TokenCache struct { 84 lock sync.Mutex 85 token *expiringToken 86 ctor requestCtor 87 doer requestDoer 88 } 89 90 // MakeCache creates a new cache or returns an error if tokens aren't 91 // available. 92 func MakeCache(ctor func(method, url string, body io.Reader) (*http.Request, error), 93 doer func(req *http.Request) (*http.Response, error)) (*TokenCache, error) { 94 token, err := retrieveJwtToken(ctor, doer) 95 if err != nil { 96 return nil, err 97 } 98 return &TokenCache{sync.Mutex{}, token, ctor, doer}, nil 99 } 100 101 // Get returns a potentially cached value of the token or renews as 102 // necessary. The now parameter provides the current time for cache 103 // expiration. The returned value is suitable for Authorization header 104 // and syz-hub Key requests. 105 func (cache *TokenCache) Get(now time.Time) (string, error) { 106 cache.lock.Lock() 107 defer cache.lock.Unlock() 108 // A typical token returned by metadata server is valid for an hour. 109 // Refreshing a minute early should give the recipient plenty of time 110 // to verify the token. 111 if cache.token.expiration.Sub(now) < time.Minute { 112 // Keeping the lock while making http request is dubious, but 113 // making multiple concurrent requests is not any better. 114 t, err := retrieveJwtToken(cache.ctor, cache.doer) 115 if err != nil { 116 // Can't get a new token, so returning the error preemptively. 117 return "", err 118 } 119 cache.token = t 120 } 121 return "Bearer " + cache.token.value, nil 122 }