/
stsclient.go
199 lines (182 loc) · 6.68 KB
/
stsclient.go
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
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
// Copyright Istio Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package stsclient is for oauth token exchange integration.
package stsclient
import (
"bytes"
"crypto/tls"
"encoding/json"
"fmt"
"io"
"net/http"
"time"
"istio.io/istio/pkg/bootstrap/platform"
"istio.io/istio/pkg/env"
"istio.io/istio/pkg/log"
sec_model "istio.io/istio/pkg/model"
"istio.io/istio/pkg/security"
"istio.io/istio/security/pkg/monitoring"
)
var (
// GKEClusterURL is the URL to send requests to the token exchange service.
GKEClusterURL = env.Register("GKE_CLUSTER_URL", "", "The url of GKE cluster").Get()
// SecureTokenEndpoint is the Endpoint the STS client calls to.
SecureTokenEndpoint = "https://sts.googleapis.com/v1/token"
stsClientLog = log.RegisterScope("stsclient", "STS client debugging")
)
const (
httpTimeout = time.Second * 5
contentType = "application/json"
Scope = "https://www.googleapis.com/auth/cloud-platform"
)
type federatedTokenResponse struct {
AccessToken string `json:"access_token"`
IssuedTokenType string `json:"issued_token_type"`
TokenType string `json:"token_type"`
ExpiresIn int64 `json:"expires_in"` // Expiration time in seconds
}
// SecureTokenServiceExchanger for google securetoken api interaction.
type SecureTokenServiceExchanger struct {
httpClient *http.Client
credFetcher security.CredFetcher
trustDomain string
backoff time.Duration
audience string
}
// NewSecureTokenServiceExchanger returns an instance of secure token service client plugin
func NewSecureTokenServiceExchanger(credFetcher security.CredFetcher, trustDomain string) (*SecureTokenServiceExchanger, error) {
aud, err := constructAudience(credFetcher, trustDomain)
if err != nil {
return nil, err
}
tlsConfig := &tls.Config{
MinVersion: tls.VersionTLS12,
}
sec_model.EnforceGoCompliance(tlsConfig)
return &SecureTokenServiceExchanger{
httpClient: &http.Client{
Timeout: httpTimeout,
Transport: &http.Transport{
TLSClientConfig: tlsConfig,
},
},
backoff: time.Millisecond * 50,
credFetcher: credFetcher,
trustDomain: trustDomain,
audience: aud,
}, nil
}
func retryable(code int) bool {
return code >= 500 &&
!(code == http.StatusNotImplemented ||
code == http.StatusHTTPVersionNotSupported ||
code == http.StatusNetworkAuthenticationRequired)
}
func (p *SecureTokenServiceExchanger) requestWithRetry(reqBytes []byte) ([]byte, error) {
attempts := 0
var lastError error
for attempts < 5 {
attempts++
req, err := http.NewRequest(http.MethodPost, SecureTokenEndpoint, bytes.NewBuffer(reqBytes))
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", contentType)
resp, err := p.httpClient.Do(req)
if err != nil {
lastError = err
stsClientLog.Errorf("token exchange request failed: %v", err)
time.Sleep(p.backoff)
monitoring.NumOutgoingRetries.With(monitoring.RequestType.Value(monitoring.TokenExchange)).Increment()
continue
}
if resp.StatusCode == http.StatusOK {
body, err := io.ReadAll(resp.Body)
resp.Body.Close()
return body, err
}
body, _ := io.ReadAll(resp.Body)
lastError = fmt.Errorf("token exchange request failed: status code %v body %v", resp.StatusCode, string(body))
resp.Body.Close()
if !retryable(resp.StatusCode) {
break
}
monitoring.NumOutgoingRetries.With(monitoring.RequestType.Value(monitoring.TokenExchange)).Increment()
if stsClientLog.DebugEnabled() {
stsClientLog.Debugf("token exchange request failed: status code %v, body %v", resp.StatusCode, string(body))
} else {
stsClientLog.Errorf("token exchange request failed: status code %v", resp.StatusCode)
}
time.Sleep(p.backoff)
}
return nil, fmt.Errorf("exchange failed all retries, last error: %v", lastError)
}
// ExchangeToken exchange oauth access token from trusted domain and k8s sa jwt.
func (p *SecureTokenServiceExchanger) ExchangeToken(k8sSAjwt string) (string, error) {
aud := p.audience
jsonStr, err := constructFederatedTokenRequest(aud, k8sSAjwt)
if err != nil {
return "", fmt.Errorf("failed to marshal federated token request: %v", err)
}
body, err := p.requestWithRetry(jsonStr)
if err != nil {
return "", fmt.Errorf("token exchange failed: %v, (aud: %s, STS endpoint: %s)", err, aud, SecureTokenEndpoint)
}
respData := &federatedTokenResponse{}
if err := json.Unmarshal(body, respData); err != nil {
// Normally the request should json - extremely hard to debug otherwise, not enough info in status/err
stsClientLog.Debugf("Unexpected unmarshal error, response was %s", string(body))
return "", fmt.Errorf("(aud: %s, STS endpoint: %s), failed to unmarshal response data of size %v: %v",
aud, SecureTokenEndpoint, len(body), err)
}
if respData.AccessToken == "" {
return "", fmt.Errorf(
"exchanged empty token (aud: %s, STS endpoint: %s), response: %v", aud, SecureTokenEndpoint, string(body))
}
return respData.AccessToken, nil
}
func constructAudience(credFetcher security.CredFetcher, trustDomain string) (string, error) {
provider := ""
if credFetcher != nil {
provider = credFetcher.GetIdentityProvider()
}
// For GKE, we do not register IdentityProvider explicitly. The provider name
// is GKEClusterURL by default.
if provider == "" {
if GKEClusterURL != "" {
provider = GKEClusterURL
} else if platform.IsGCP() {
if clusterURL, found := platform.NewGCP().Metadata()[platform.GCPClusterURL]; found && len(clusterURL) > 0 {
provider = clusterURL
stsClientLog.Infof("GKE_CLUSTER_URL is not set, fetched cluster URL from metadata server: %q", provider)
} else {
return "", fmt.Errorf("failed to get GCPClusterURL from Metadata(): found (%v), clusterURL (%v)",
found, clusterURL)
}
}
}
return fmt.Sprintf("identitynamespace:%s:%s", trustDomain, provider), nil
}
func constructFederatedTokenRequest(aud, jwt string) ([]byte, error) {
values := map[string]string{
"audience": aud,
"grantType": "urn:ietf:params:oauth:grant-type:token-exchange",
"requestedTokenType": "urn:ietf:params:oauth:token-type:access_token",
"subjectTokenType": "urn:ietf:params:oauth:token-type:jwt",
"subjectToken": jwt,
"scope": Scope,
}
jsonValue, err := json.Marshal(values)
return jsonValue, err
}