aboutsummaryrefslogtreecommitdiffstats
path: root/pkg/auth/auth.go
diff options
context:
space:
mode:
authorGreg Steuck <gnezdo@google.com>2021-07-28 11:03:25 -0700
committerDmitry Vyukov <dvyukov@google.com>2021-07-30 18:21:17 +0200
commit5bfcec7dfd4ba51d38b41cea770ecc96e7e59d4d (patch)
treeef129d2b1e22acfd1c790c8189d7776a82daba91 /pkg/auth/auth.go
parent14f590a6a765d9fbe53e2f7bacb5d9f6d7cb9063 (diff)
pkg/auth: move auth code into a new package for reuse in syz-hub
Diffstat (limited to 'pkg/auth/auth.go')
-rw-r--r--pkg/auth/auth.go129
1 files changed, 129 insertions, 0 deletions
diff --git a/pkg/auth/auth.go b/pkg/auth/auth.go
new file mode 100644
index 000000000..bd542bc32
--- /dev/null
+++ b/pkg/auth/auth.go
@@ -0,0 +1,129 @@
+// Copyright 2021 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.
+
+// Relies on tokeninfo because it is properly documented:
+// https://developers.google.com/identity/protocols/oauth2/openid-connect#validatinganidtoken
+
+// The client
+// The VM that wants to invoke the API:
+// 1) Gets a token from the metainfo server with this http request:
+// META=http://metadata.google.internal/computeMetadata/v1
+// AUD=https://syzkaller.appspot.com/api
+// curl -sH 'Metadata-Flavor: Google' \
+// "$META/instance/service-accounts/default/identity?audience=$AUD"
+// 2) Invokes /api with header 'Authorization: Bearer <token>'
+//
+// The AppEngine api server:
+// 1) Receive the token, invokes this http request:
+// curl -s "https://oauth2.googleapis.com/tokeninfo?id_token=<token>"
+// 2) Checks the resulting JSON having the expected audience and expiration.
+// 3) Looks up the permissions in the config using the value of sub.
+//
+// https://cloud.google.com/iap/docs/signed-headers-howto#retrieving_the_user_identity
+// from the IAP docs agrees to trust sub.
+
+// Package auth contains authentication related code supporting secret
+// passwords and oauth2 tokens on GCE.
+package auth
+
+import (
+ "encoding/json"
+ "fmt"
+ "io/ioutil"
+ "net/http"
+ "net/url"
+ "strconv"
+ "strings"
+ "time"
+
+ "github.com/google/syzkaller/dashboard/dashapi"
+)
+
+const (
+ // The official google oauth2 endpoint.
+ GoogleTokenInfoEndpoint = "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:"
+)
+
+// Represent a verification backend.
+type Endpoint struct {
+ // URL supporting tokeninfo auth2 protocol.
+ url string
+ // TODO(blackgnezdo): cache tokens with a bit of care for concurrency.
+}
+
+func MakeEndpoint(u string) Endpoint {
+ return Endpoint{url: u}
+}
+
+// The JSON representation of JWT claims.
+type jwtClaimsParse struct {
+ Subject string `json:"sub"`
+ Audience string `json:"aud"`
+ // The field in the JSON is a string but contains a UNIX time.
+ Expiration string `json:"exp"`
+}
+
+// The typed representation of JWT claims.
+type jwtClaims struct {
+ Subject string
+ Audience string
+ // The app uses the typed value.
+ Expiration time.Time
+}
+
+func (auth *Endpoint) queryTokenInfo(tokenValue string) (*jwtClaims, error) {
+ resp, err := http.PostForm(auth.url, 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(jwtClaimsParse)
+ if err = json.Unmarshal(body, claims); err != nil {
+ return nil, err
+ }
+ expInt, err := strconv.ParseInt(claims.Expiration, 10, 64)
+ if err != nil {
+ return nil, err
+ }
+ r := jwtClaims{
+ Subject: claims.Subject,
+ Audience: claims.Audience,
+ Expiration: time.Unix(expInt, 0),
+ }
+ return &r, nil
+}
+
+// Returns the verified subject value based on the provided header
+// value or "" if it can't be determined. A valid result starts with
+// auth.OauthMagic. The now parameter is the current time to compare the
+// claims against. The authHeader is styled as is typical for HTTP headers
+// which carry the tokens prefixed by "Bearer " string.
+func (auth *Endpoint) DetermineAuthSubj(now time.Time, authHeader []string) (string, error) {
+ if len(authHeader) != 1 || !strings.HasPrefix(authHeader[0], "Bearer") {
+ // This is a normal case when the client uses a password.
+ return "", nil
+ }
+ // 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 := auth.queryTokenInfo(tokenValue)
+ if err != nil {
+ return "", err
+ }
+ if claims.Audience != dashapi.DashboardAudience {
+ err := fmt.Errorf("unexpected audience %v %v", claims.Audience, claims)
+ return "", err
+ }
+ if claims.Expiration.Before(now) {
+ err := fmt.Errorf("token past expiration %v", claims.Expiration)
+ return "", err
+ }
+ return OauthMagic + claims.Subject, nil
+}