aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorGreg Steuck <gnezdo@google.com>2021-06-15 16:20:55 -0700
committerDmitry Vyukov <dvyukov@google.com>2021-07-14 07:16:41 +0200
commit38c3a6bda5cb059d6b4ba450e7dcacafd96370cf (patch)
tree2a7311bd5039672f62e92e49ae47ba3ba0171128
parent484502bddac5f2c0fc591227b634da2f7f42dbf0 (diff)
dashboard/app: server-side support for OAuth /api authentication
-rw-r--r--dashboard/app/api.go25
-rw-r--r--dashboard/app/auth.go101
-rw-r--r--dashboard/dashapi/dashapi.go4
3 files changed, 107 insertions, 23 deletions
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)