aboutsummaryrefslogtreecommitdiffstats
path: root/dashboard/dashapi
diff options
context:
space:
mode:
authorGreg Steuck <greg@nest.cx>2021-07-10 08:51:25 -0700
committerDmitry Vyukov <dvyukov@google.com>2021-07-14 07:16:41 +0200
commit6172cc679a71826349b603382cb18a441f16438f (patch)
tree8cca4c98128564c276de0a1a7d8b6f58e16b64dd /dashboard/dashapi
parentcfc934a81713b26518f1ae0fa94900a2da77553b (diff)
dashboard/dashapi: implement client-side JWT token authorization
Diffstat (limited to 'dashboard/dashapi')
-rw-r--r--dashboard/dashapi/dashapi.go9
-rw-r--r--dashboard/dashapi/jwt.go91
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)
+ }
+}