2 Copyright 2016 The Kubernetes Authors.
4 Licensed under the Apache License, Version 2.0 (the "License");
5 you may not use this file except in compliance with the License.
6 You may obtain a copy of the License at
8 http://www.apache.org/licenses/LICENSE-2.0
10 Unless required by applicable law or agreed to in writing, software
11 distributed under the License is distributed on an "AS IS" BASIS,
12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 See the License for the specific language governing permissions and
14 limitations under the License.
32 "k8s.io/apimachinery/pkg/util/net"
33 restclient "k8s.io/client-go/rest"
38 cfgIssuerUrl = "idp-issuer-url"
39 cfgClientID = "client-id"
40 cfgClientSecret = "client-secret"
41 cfgCertificateAuthority = "idp-certificate-authority"
42 cfgCertificateAuthorityData = "idp-certificate-authority-data"
43 cfgIDToken = "id-token"
44 cfgRefreshToken = "refresh-token"
46 // Unused. Scopes aren't sent during refreshing.
47 cfgExtraScopes = "extra-scopes"
51 if err := restclient.RegisterAuthProviderPlugin("oidc", newOIDCAuthProvider); err != nil {
52 klog.Fatalf("Failed to register oidc auth plugin: %v", err)
56 // expiryDelta determines how earlier a token should be considered
57 // expired than its actual expiration time. It is used to avoid late
58 // expirations due to client-server time mismatches.
60 // NOTE(ericchiang): this is take from golang.org/x/oauth2
61 const expiryDelta = 10 * time.Second
63 var cache = newClientCache()
65 // Like TLS transports, keep a cache of OIDC clients indexed by issuer URL. This ensures
66 // current requests from different clients don't concurrently attempt to refresh the same
67 // set of credentials.
68 type clientCache struct {
71 cache map[cacheKey]*oidcAuthProvider
74 func newClientCache() *clientCache {
75 return &clientCache{cache: make(map[cacheKey]*oidcAuthProvider)}
78 type cacheKey struct {
79 // Canonical issuer URL string of the provider.
84 func (c *clientCache) getClient(issuer, clientID string) (*oidcAuthProvider, bool) {
87 client, ok := c.cache[cacheKey{issuer, clientID}]
91 // setClient attempts to put the client in the cache but may return any clients
92 // with the same keys set before. This is so there's only ever one client for a provider.
93 func (c *clientCache) setClient(issuer, clientID string, client *oidcAuthProvider) *oidcAuthProvider {
96 key := cacheKey{issuer, clientID}
98 // If another client has already initialized a client for the given provider we want
99 // to use that client instead of the one we're trying to set. This is so all transports
100 // share a client and can coordinate around the same mutex when refreshing and writing
101 // to the kubeconfig.
102 if oldClient, ok := c.cache[key]; ok {
106 c.cache[key] = client
110 func newOIDCAuthProvider(_ string, cfg map[string]string, persister restclient.AuthProviderConfigPersister) (restclient.AuthProvider, error) {
111 issuer := cfg[cfgIssuerUrl]
113 return nil, fmt.Errorf("Must provide %s", cfgIssuerUrl)
116 clientID := cfg[cfgClientID]
118 return nil, fmt.Errorf("Must provide %s", cfgClientID)
121 // Check cache for existing provider.
122 if provider, ok := cache.getClient(issuer, clientID); ok {
126 if len(cfg[cfgExtraScopes]) > 0 {
127 klog.V(2).Infof("%s auth provider field depricated, refresh request don't send scopes",
131 var certAuthData []byte
133 if cfg[cfgCertificateAuthorityData] != "" {
134 certAuthData, err = base64.StdEncoding.DecodeString(cfg[cfgCertificateAuthorityData])
140 clientConfig := restclient.Config{
141 TLSClientConfig: restclient.TLSClientConfig{
142 CAFile: cfg[cfgCertificateAuthority],
143 CAData: certAuthData,
147 trans, err := restclient.TransportFor(&clientConfig)
151 hc := &http.Client{Transport: trans}
153 provider := &oidcAuthProvider{
157 persister: persister,
160 return cache.setClient(issuer, clientID, provider), nil
163 type oidcAuthProvider struct {
166 // Method for determining the current time.
169 // Mutex guards persisting to the kubeconfig file and allows synchronized
170 // updates to the in-memory config. It also ensures concurrent calls to
171 // the RoundTripper only trigger a single refresh request.
173 cfg map[string]string
174 persister restclient.AuthProviderConfigPersister
177 func (p *oidcAuthProvider) WrapTransport(rt http.RoundTripper) http.RoundTripper {
178 return &roundTripper{
184 func (p *oidcAuthProvider) Login() error {
185 return errors.New("not yet implemented")
188 type roundTripper struct {
189 provider *oidcAuthProvider
190 wrapped http.RoundTripper
193 var _ net.RoundTripperWrapper = &roundTripper{}
195 func (r *roundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
196 if len(req.Header.Get("Authorization")) != 0 {
197 return r.wrapped.RoundTrip(req)
199 token, err := r.provider.idToken()
204 // shallow copy of the struct
205 r2 := new(http.Request)
207 // deep copy of the Header so we don't modify the original
208 // request's Header (as per RoundTripper contract).
209 r2.Header = make(http.Header)
210 for k, s := range req.Header {
213 r2.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
215 return r.wrapped.RoundTrip(r2)
218 func (t *roundTripper) WrappedRoundTripper() http.RoundTripper { return t.wrapped }
220 func (p *oidcAuthProvider) idToken() (string, error) {
224 if idToken, ok := p.cfg[cfgIDToken]; ok && len(idToken) > 0 {
225 valid, err := idTokenExpired(p.now, idToken)
230 // If the cached id token is still valid use it.
235 // Try to request a new token using the refresh token.
236 rt, ok := p.cfg[cfgRefreshToken]
237 if !ok || len(rt) == 0 {
238 return "", errors.New("No valid id-token, and cannot refresh without refresh-token")
241 // Determine provider's OAuth2 token endpoint.
242 tokenURL, err := tokenEndpoint(p.client, p.cfg[cfgIssuerUrl])
247 config := oauth2.Config{
248 ClientID: p.cfg[cfgClientID],
249 ClientSecret: p.cfg[cfgClientSecret],
250 Endpoint: oauth2.Endpoint{TokenURL: tokenURL},
253 ctx := context.WithValue(context.Background(), oauth2.HTTPClient, p.client)
254 token, err := config.TokenSource(ctx, &oauth2.Token{RefreshToken: rt}).Token()
256 return "", fmt.Errorf("failed to refresh token: %v", err)
259 idToken, ok := token.Extra("id_token").(string)
261 // id_token isn't a required part of a refresh token response, so some
262 // providers (Okta) don't return this value.
264 // See https://github.com/kubernetes/kubernetes/issues/36847
265 return "", fmt.Errorf("token response did not contain an id_token, either the scope \"openid\" wasn't requested upon login, or the provider doesn't support id_tokens as part of the refresh response.")
268 // Create a new config to persist.
269 newCfg := make(map[string]string)
270 for key, val := range p.cfg {
274 // Update the refresh token if the server returned another one.
275 if token.RefreshToken != "" && token.RefreshToken != rt {
276 newCfg[cfgRefreshToken] = token.RefreshToken
278 newCfg[cfgIDToken] = idToken
280 // Persist new config and if successful, update the in memory config.
281 if err = p.persister.Persist(newCfg); err != nil {
282 return "", fmt.Errorf("could not persist new tokens: %v", err)
289 // tokenEndpoint uses OpenID Connect discovery to determine the OAuth2 token
290 // endpoint for the provider, the endpoint the client will use the refresh
292 func tokenEndpoint(client *http.Client, issuer string) (string, error) {
293 // Well known URL for getting OpenID Connect metadata.
295 // https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfig
296 wellKnown := strings.TrimSuffix(issuer, "/") + "/.well-known/openid-configuration"
297 resp, err := client.Get(wellKnown)
301 defer resp.Body.Close()
303 body, err := ioutil.ReadAll(resp.Body)
307 if resp.StatusCode != http.StatusOK {
308 // Don't produce an error that's too huge (e.g. if we get HTML back for some reason).
311 body = append(body[:n], []byte("...")...)
313 return "", fmt.Errorf("oidc: failed to query metadata endpoint %s: %q", resp.Status, body)
316 // Metadata object. We only care about the token_endpoint, the thing endpoint
317 // we'll be refreshing against.
319 // https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata
320 var metadata struct {
321 TokenURL string `json:"token_endpoint"`
323 if err := json.Unmarshal(body, &metadata); err != nil {
324 return "", fmt.Errorf("oidc: failed to decode provider discovery object: %v", err)
326 if metadata.TokenURL == "" {
327 return "", fmt.Errorf("oidc: discovery object doesn't contain a token_endpoint")
329 return metadata.TokenURL, nil
332 func idTokenExpired(now func() time.Time, idToken string) (bool, error) {
333 parts := strings.Split(idToken, ".")
335 return false, fmt.Errorf("ID Token is not a valid JWT")
338 payload, err := base64.RawURLEncoding.DecodeString(parts[1])
343 Expiry jsonTime `json:"exp"`
345 if err := json.Unmarshal(payload, &claims); err != nil {
346 return false, fmt.Errorf("parsing claims: %v", err)
349 return now().Add(expiryDelta).Before(time.Time(claims.Expiry)), nil
352 // jsonTime is a json.Unmarshaler that parses a unix timestamp.
353 // Because JSON numbers don't differentiate between ints and floats,
354 // we want to ensure we can parse either.
355 type jsonTime time.Time
357 func (j *jsonTime) UnmarshalJSON(b []byte) error {
359 if err := json.Unmarshal(b, &n); err != nil {
364 if t, err := n.Int64(); err == nil {
367 f, err := n.Float64()
373 *j = jsonTime(time.Unix(unix, 0))
377 func (j jsonTime) MarshalJSON() ([]byte, error) {
378 return json.Marshal(time.Time(j).Unix())