From 38c3a6bda5cb059d6b4ba450e7dcacafd96370cf Mon Sep 17 00:00:00 2001 From: Greg Steuck Date: Tue, 15 Jun 2021 16:20:55 -0700 Subject: dashboard/app: server-side support for OAuth /api authentication --- dashboard/app/api.go | 25 +---------- dashboard/app/auth.go | 101 +++++++++++++++++++++++++++++++++++++++++++ dashboard/dashapi/dashapi.go | 4 ++ 3 files changed, 107 insertions(+), 23 deletions(-) create mode 100644 dashboard/app/auth.go diff --git a/dashboard/app/api.go b/dashboard/app/api.go index 99dd0e7fd..484d1dfcc 100644 --- a/dashboard/app/api.go +++ b/dashboard/app/api.go @@ -104,8 +104,9 @@ func handleAPI(c context.Context, r *http.Request) (reply interface{}, err error client := r.PostFormValue("client") method := r.PostFormValue("method") log.Infof(c, "api %q from %q", method, client) + subj := determineAuthSubj(c, r.Header["Authorization"]) // Somewhat confusingly the "key" parameter is the password. - ns, err := checkClient(c, client, r.PostFormValue("key")) + ns, err := checkClient(client, r.PostFormValue("key"), subj) if err != nil { if client != "" { log.Errorf(c, "%v", err) @@ -143,28 +144,6 @@ func handleAPI(c context.Context, r *http.Request) (reply interface{}, err error return nsHandler(c, ns, r, payload) } -func checkClient(c context.Context, name0, password0 string) (string, error) { - for name, password := range config.Clients { - if name == name0 { - if password != password0 { - return "", ErrAccess - } - return "", nil - } - } - for ns, cfg := range config.Namespaces { - for name, password := range cfg.Clients { - if name == name0 { - if password != password0 { - return "", ErrAccess - } - return ns, nil - } - } - } - return "", ErrAccess -} - func apiLogError(c context.Context, r *http.Request, payload []byte) (interface{}, error) { req := new(dashapi.LogEntry) if err := json.Unmarshal(payload, req); err != nil { diff --git a/dashboard/app/auth.go b/dashboard/app/auth.go new file mode 100644 index 000000000..ac9b2454a --- /dev/null +++ b/dashboard/app/auth.go @@ -0,0 +1,101 @@ +// 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 main + +import ( + "encoding/json" + "io/ioutil" + "net/http" + "net/url" + "strings" + "time" + + "github.com/google/syzkaller/dashboard/dashapi" + "golang.org/x/net/context" + "google.golang.org/appengine/log" +) + +const ( + tokenInfoEndpoint = "https://oauth2.googleapis.com/tokeninfo" + // Used in the config map as a prefix to distinguish auth identifiers from secret passwords + // (which contain arbitrary strings, that can't have this prefix). + oauthMagic = "OauthSubject:" +) + +type jwtClaims struct { + subject string `json:sub` + expiration float64 `json:exp` + audience string `json:aud` +} + +func queryTokenInfo(tokenValue string) (*jwtClaims, error) { + resp, err := http.PostForm(tokenInfoEndpoint, url.Values{"id_token": {tokenValue}}) + if err != nil { + return nil, err + } + defer resp.Body.Close() + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, err + } + claims := new(jwtClaims) + if err = json.Unmarshal(body, claims); err != nil { + return nil, err + } + return claims, nil +} + +// Returns the verified subject value based on the provided header +// value or "" if it can't be determined. A valid result starts with +// oauthMagic. +func determineAuthSubj(c context.Context, authHeader []string) string { + if len(authHeader) != 1 || !strings.HasPrefix("Bearer", authHeader[0]) { + // This is a normal case when the client uses a password. + return "" + } + // Values past this point are real authentication attempts. Whether + // or not they are valid is the question. + tokenValue := strings.TrimSpace(strings.TrimPrefix(authHeader[0], "Bearer")) + claims, err := queryTokenInfo(tokenValue) + if err != nil { + log.Errorf(c, "Failed token validation %v", err) + return "" + } + if claims.audience != dashapi.DashboardAudience { + log.Errorf(c, "Unexpected audience %v", claims.audience) + return "" + } + if claims.expiration < float64(time.Now().Unix()) { + log.Errorf(c, "Token past expiration %v", claims.expiration) + return "" + } + return oauthMagic + claims.subject +} + +// Verifies that the given credentials are acceptable and returns the +// corresponding namespace. +func checkClient(name0, secretPassword, oauthSubject string) (string, error) { + checkAuth := func(ns, a string) (string, error) { + if strings.HasPrefix(oauthMagic, a) && a == oauthSubject { + return ns, nil + } + if a != secretPassword { + return ns, ErrAccess + } + return ns, nil + } + for name, authenticator := range config.Clients { + if name == name0 { + return checkAuth("", authenticator) + } + } + for ns, cfg := range config.Namespaces { + for name, authenticator := range cfg.Clients { + if name == name0 { + return checkAuth(ns, authenticator) + } + } + } + return "", ErrAccess +} diff --git a/dashboard/dashapi/dashapi.go b/dashboard/dashapi/dashapi.go index 1eb536404..cdd557994 100644 --- a/dashboard/dashapi/dashapi.go +++ b/dashboard/dashapi/dashapi.go @@ -561,6 +561,10 @@ const ( ReportBisectFix // Fix bisection result for an already reported bug. ) +const ( + DashboardAudience = "https://syzkaller.appspot.com/api" +) + func (dash *Dashboard) Query(method string, req, reply interface{}) error { if dash.logger != nil { dash.logger("API(%v): %#v", method, req) -- cgit mrf-deployment