1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
|
// 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"
"net/http"
"net/url"
"strconv"
"strings"
"time"
)
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, fmt.Errorf("http.PostForm: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("verification failed %v", resp.StatusCode)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("io.ReadAll: %w", err)
}
claims := new(jwtClaimsParse)
if err = json.Unmarshal(body, claims); err != nil {
return nil, fmt.Errorf("json.Unmarshal: %w", err)
}
expInt, err := strconv.ParseInt(claims.Expiration, 10, 64)
if err != nil {
return nil, fmt.Errorf("strconv.ParseInt: %w", 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 "", fmt.Errorf("auth.queryTokenInfo: %w", err)
}
if claims.Audience != DashboardAudience {
return "", fmt.Errorf("unexpected audience %v", claims.Audience)
}
if claims.Expiration.Before(now) {
return "", fmt.Errorf("token past expiration %v", claims.Expiration)
}
return OauthMagic + claims.Subject, nil
}
|