diff options
| author | Greg Steuck <greg@nest.cx> | 2021-07-10 08:51:25 -0700 |
|---|---|---|
| committer | Dmitry Vyukov <dvyukov@google.com> | 2021-07-14 07:16:41 +0200 |
| commit | 6172cc679a71826349b603382cb18a441f16438f (patch) | |
| tree | 8cca4c98128564c276de0a1a7d8b6f58e16b64dd /dashboard/dashapi | |
| parent | cfc934a81713b26518f1ae0fa94900a2da77553b (diff) | |
dashboard/dashapi: implement client-side JWT token authorization
Diffstat (limited to 'dashboard/dashapi')
| -rw-r--r-- | dashboard/dashapi/dashapi.go | 9 | ||||
| -rw-r--r-- | dashboard/dashapi/jwt.go | 91 |
2 files changed, 100 insertions, 0 deletions
diff --git a/dashboard/dashapi/dashapi.go b/dashboard/dashapi/dashapi.go index cdd557994..d36adaa68 100644 --- a/dashboard/dashapi/dashapi.go +++ b/dashboard/dashapi/dashapi.go @@ -40,8 +40,17 @@ type ( RequestLogger func(msg string, args ...interface{}) ) +// key == "" indicates that the ambient GCE service account authority +// should be used as a bearer token. func NewCustom(client, addr, key string, ctor RequestCtor, doer RequestDoer, logger RequestLogger, errorHandler func(error)) (*Dashboard, error) { + if key == "" { + token, err := retrieveJwtToken(ctor, doer) + if err != nil { + return nil, err + } + doer = atachJwtToken(ctor, doer, token) + } return &Dashboard{ Client: client, Addr: addr, diff --git a/dashboard/dashapi/jwt.go b/dashboard/dashapi/jwt.go new file mode 100644 index 000000000..adda89bd1 --- /dev/null +++ b/dashboard/dashapi/jwt.go @@ -0,0 +1,91 @@ +// Copyright 2017 syzkaller project authors. All rights reserved. +// Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file. + +package dashapi + +import ( + "encoding/base64" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "strings" + "sync" + "time" +) + +type expiringToken struct { + token string + expiration time.Time +} + +// Returns the unverified expiration value from the given JWT token. +func extractJwtExpiration(token string) (time.Time, error) { + // https://datatracker.ietf.org/doc/html/rfc7519#section-3 + pieces := strings.Split(token, ".") + if len(pieces) != 3 { + return time.Time{}, fmt.Errorf("unexpected number of JWT components %v", len(pieces)) + } + decoded, err := base64.URLEncoding.WithPadding(base64.NoPadding).DecodeString(pieces[1]) + if err != nil { + return time.Time{}, err + } + claims := struct { + Expiration int64 `json:"exp"` + }{-123456} // Hopefully a notably broken value. + if err = json.Unmarshal(decoded, &claims); err != nil { + return time.Time{}, err + } + return time.Unix(claims.Expiration, 0), nil +} + +// Queries the metadata server and returns the bearer token of the service account. +// The token is scoped for the official dashboard. +func retrieveJwtToken(ctor RequestCtor, doer RequestDoer) (*expiringToken, error) { + const v1meta = "http://metadata.google.internal/computeMetadata/v1" + req, err := ctor("GET", v1meta+"/instance/service-accounts/default/identity?audience="+DashboardAudience, nil) + if err != nil { + return nil, err + } + req.Header.Add("Metadata-Flavor", "Google") + resp, err := doer(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + data, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, err + } + token := string(data) + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("failed metadata get %v: %s", resp.Status, data) + } + expiration, err := extractJwtExpiration(token) + if err != nil { + return nil, err + } + return &expiringToken{token, expiration}, nil +} + +// Augments the given doer with an authorization header carrying the +// given token. The token gets refreshed when it becomes stale. +func atachJwtToken(ctor RequestCtor, doer RequestDoer, token *expiringToken) RequestDoer { + lock := sync.Mutex{} + return func(req *http.Request) (*http.Response, error) { + lock.Lock() + if token.expiration.Before(time.Now()) { + // Keeping the lock while making http request is dubious, but + // making multiple concurrent requests is not any better. + t, err := retrieveJwtToken(ctor, doer) + if err != nil { + // Can't get a new token, so returning the error preemptively. + return nil, err + } + *token = *t + } + req.Header.Add("Authorization", "Bearer "+token.token) + lock.Unlock() + return doer(req) + } +} |
