storj.io/minio@v0.0.0-20230509071714-0cbc90f649b1/cmd/config/identity/openid/jwt.go (about)

     1  /*
     2   * MinIO Cloud Storage, (C) 2018-2019 MinIO, Inc.
     3   *
     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
     7   *
     8   *     http://www.apache.org/licenses/LICENSE-2.0
     9   *
    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.
    15   */
    16  
    17  package openid
    18  
    19  import (
    20  	"crypto"
    21  	"encoding/json"
    22  	"errors"
    23  	"fmt"
    24  	"io"
    25  	"net/http"
    26  	"strconv"
    27  	"strings"
    28  	"sync"
    29  	"time"
    30  
    31  	jwtgo "github.com/dgrijalva/jwt-go"
    32  
    33  	"storj.io/minio/cmd/config"
    34  	"storj.io/minio/pkg/auth"
    35  	"storj.io/minio/pkg/env"
    36  	iampolicy "storj.io/minio/pkg/iam/policy"
    37  	xnet "storj.io/minio/pkg/net"
    38  )
    39  
    40  // Config - OpenID Config
    41  // RSA authentication target arguments
    42  type Config struct {
    43  	JWKS struct {
    44  		URL *xnet.URL `json:"url"`
    45  	} `json:"jwks"`
    46  	URL          *xnet.URL `json:"url,omitempty"`
    47  	ClaimPrefix  string    `json:"claimPrefix,omitempty"`
    48  	ClaimName    string    `json:"claimName,omitempty"`
    49  	DiscoveryDoc DiscoveryDoc
    50  	ClientID     string
    51  	publicKeys   map[string]crypto.PublicKey
    52  	transport    *http.Transport
    53  	closeRespFn  func(io.ReadCloser)
    54  	mutex        *sync.Mutex
    55  }
    56  
    57  // PopulatePublicKey - populates a new publickey from the JWKS URL.
    58  func (r *Config) PopulatePublicKey() error {
    59  	r.mutex.Lock()
    60  	defer r.mutex.Unlock()
    61  
    62  	if r.JWKS.URL == nil || r.JWKS.URL.String() == "" {
    63  		return nil
    64  	}
    65  	transport := http.DefaultTransport
    66  	if r.transport != nil {
    67  		transport = r.transport
    68  	}
    69  	client := &http.Client{
    70  		Transport: transport,
    71  	}
    72  	resp, err := client.Get(r.JWKS.URL.String())
    73  	if err != nil {
    74  		return err
    75  	}
    76  	defer r.closeRespFn(resp.Body)
    77  	if resp.StatusCode != http.StatusOK {
    78  		return errors.New(resp.Status)
    79  	}
    80  
    81  	var jwk JWKS
    82  	if err = json.NewDecoder(resp.Body).Decode(&jwk); err != nil {
    83  		return err
    84  	}
    85  
    86  	for _, key := range jwk.Keys {
    87  		r.publicKeys[key.Kid], err = key.DecodePublicKey()
    88  		if err != nil {
    89  			return err
    90  		}
    91  	}
    92  
    93  	return nil
    94  }
    95  
    96  // UnmarshalJSON - decodes JSON data.
    97  func (r *Config) UnmarshalJSON(data []byte) error {
    98  	// subtype to avoid recursive call to UnmarshalJSON()
    99  	type subConfig Config
   100  	var sr subConfig
   101  
   102  	if err := json.Unmarshal(data, &sr); err != nil {
   103  		return err
   104  	}
   105  
   106  	ar := Config(sr)
   107  	if ar.JWKS.URL == nil || ar.JWKS.URL.String() == "" {
   108  		*r = ar
   109  		return nil
   110  	}
   111  
   112  	*r = ar
   113  	return nil
   114  }
   115  
   116  // JWT - rs client grants provider details.
   117  type JWT struct {
   118  	Config
   119  }
   120  
   121  // GetDefaultExpiration - returns the expiration seconds expected.
   122  func GetDefaultExpiration(dsecs string) (time.Duration, error) {
   123  	defaultExpiryDuration := time.Duration(60) * time.Minute // Defaults to 1hr.
   124  	if dsecs != "" {
   125  		expirySecs, err := strconv.ParseInt(dsecs, 10, 64)
   126  		if err != nil {
   127  			return 0, auth.ErrInvalidDuration
   128  		}
   129  
   130  		// The duration, in seconds, of the role session.
   131  		// The value can range from 900 seconds (15 minutes)
   132  		// up to 7 days.
   133  		if expirySecs < 900 || expirySecs > 604800 {
   134  			return 0, auth.ErrInvalidDuration
   135  		}
   136  
   137  		defaultExpiryDuration = time.Duration(expirySecs) * time.Second
   138  	}
   139  	return defaultExpiryDuration, nil
   140  }
   141  
   142  func updateClaimsExpiry(dsecs string, claims map[string]interface{}) error {
   143  	expStr := claims["exp"]
   144  	if expStr == "" {
   145  		return ErrTokenExpired
   146  	}
   147  
   148  	// No custom duration requested, the claims can be used as is.
   149  	if dsecs == "" {
   150  		return nil
   151  	}
   152  
   153  	expAt, err := auth.ExpToInt64(expStr)
   154  	if err != nil {
   155  		return err
   156  	}
   157  
   158  	defaultExpiryDuration, err := GetDefaultExpiration(dsecs)
   159  	if err != nil {
   160  		return err
   161  	}
   162  
   163  	// Verify if JWT expiry is lesser than default expiry duration,
   164  	// if that is the case then set the default expiration to be
   165  	// from the JWT expiry claim.
   166  	if time.Unix(expAt, 0).UTC().Sub(time.Now().UTC()) < defaultExpiryDuration {
   167  		defaultExpiryDuration = time.Unix(expAt, 0).UTC().Sub(time.Now().UTC())
   168  	} // else honor the specified expiry duration.
   169  
   170  	expiry := time.Now().UTC().Add(defaultExpiryDuration).Unix()
   171  	claims["exp"] = strconv.FormatInt(expiry, 10) // update with new expiry.
   172  	return nil
   173  }
   174  
   175  // Validate - validates the access token.
   176  func (p *JWT) Validate(token, dsecs string) (map[string]interface{}, error) {
   177  	jp := new(jwtgo.Parser)
   178  	jp.ValidMethods = []string{
   179  		"RS256", "RS384", "RS512", "ES256", "ES384", "ES512",
   180  		"RS3256", "RS3384", "RS3512", "ES3256", "ES3384", "ES3512",
   181  	}
   182  
   183  	keyFuncCallback := func(jwtToken *jwtgo.Token) (interface{}, error) {
   184  		kid, ok := jwtToken.Header["kid"].(string)
   185  		if !ok {
   186  			return nil, fmt.Errorf("Invalid kid value %v", jwtToken.Header["kid"])
   187  		}
   188  		return p.publicKeys[kid], nil
   189  	}
   190  
   191  	var claims jwtgo.MapClaims
   192  	jwtToken, err := jp.ParseWithClaims(token, &claims, keyFuncCallback)
   193  	if err != nil {
   194  		// Re-populate the public key in-case the JWKS
   195  		// pubkeys are refreshed
   196  		if err = p.PopulatePublicKey(); err != nil {
   197  			return nil, err
   198  		}
   199  		jwtToken, err = jwtgo.ParseWithClaims(token, &claims, keyFuncCallback)
   200  		if err != nil {
   201  			return nil, err
   202  		}
   203  	}
   204  
   205  	if !jwtToken.Valid {
   206  		return nil, ErrTokenExpired
   207  	}
   208  
   209  	if err = updateClaimsExpiry(dsecs, claims); err != nil {
   210  		return nil, err
   211  	}
   212  
   213  	return claims, nil
   214  }
   215  
   216  // ID returns the provider name and authentication type.
   217  func (p *JWT) ID() ID {
   218  	return "jwt"
   219  }
   220  
   221  // OpenID keys and envs.
   222  const (
   223  	JwksURL     = "jwks_url"
   224  	ConfigURL   = "config_url"
   225  	ClaimName   = "claim_name"
   226  	ClaimPrefix = "claim_prefix"
   227  	ClientID    = "client_id"
   228  	Scopes      = "scopes"
   229  
   230  	EnvIdentityOpenIDClientID    = "MINIO_IDENTITY_OPENID_CLIENT_ID"
   231  	EnvIdentityOpenIDJWKSURL     = "MINIO_IDENTITY_OPENID_JWKS_URL"
   232  	EnvIdentityOpenIDURL         = "MINIO_IDENTITY_OPENID_CONFIG_URL"
   233  	EnvIdentityOpenIDClaimName   = "MINIO_IDENTITY_OPENID_CLAIM_NAME"
   234  	EnvIdentityOpenIDClaimPrefix = "MINIO_IDENTITY_OPENID_CLAIM_PREFIX"
   235  	EnvIdentityOpenIDScopes      = "MINIO_IDENTITY_OPENID_SCOPES"
   236  )
   237  
   238  // DiscoveryDoc - parses the output from openid-configuration
   239  // for example https://accounts.google.com/.well-known/openid-configuration
   240  type DiscoveryDoc struct {
   241  	Issuer                           string   `json:"issuer,omitempty"`
   242  	AuthEndpoint                     string   `json:"authorization_endpoint,omitempty"`
   243  	TokenEndpoint                    string   `json:"token_endpoint,omitempty"`
   244  	UserInfoEndpoint                 string   `json:"userinfo_endpoint,omitempty"`
   245  	RevocationEndpoint               string   `json:"revocation_endpoint,omitempty"`
   246  	JwksURI                          string   `json:"jwks_uri,omitempty"`
   247  	ResponseTypesSupported           []string `json:"response_types_supported,omitempty"`
   248  	SubjectTypesSupported            []string `json:"subject_types_supported,omitempty"`
   249  	IDTokenSigningAlgValuesSupported []string `json:"id_token_signing_alg_values_supported,omitempty"`
   250  	ScopesSupported                  []string `json:"scopes_supported,omitempty"`
   251  	TokenEndpointAuthMethods         []string `json:"token_endpoint_auth_methods_supported,omitempty"`
   252  	ClaimsSupported                  []string `json:"claims_supported,omitempty"`
   253  	CodeChallengeMethodsSupported    []string `json:"code_challenge_methods_supported,omitempty"`
   254  }
   255  
   256  func parseDiscoveryDoc(u *xnet.URL, transport *http.Transport, closeRespFn func(io.ReadCloser)) (DiscoveryDoc, error) {
   257  	d := DiscoveryDoc{}
   258  	req, err := http.NewRequest(http.MethodGet, u.String(), nil)
   259  	if err != nil {
   260  		return d, err
   261  	}
   262  	clnt := http.Client{
   263  		Transport: transport,
   264  	}
   265  	resp, err := clnt.Do(req)
   266  	if err != nil {
   267  		clnt.CloseIdleConnections()
   268  		return d, err
   269  	}
   270  	defer closeRespFn(resp.Body)
   271  	if resp.StatusCode != http.StatusOK {
   272  		return d, err
   273  	}
   274  	dec := json.NewDecoder(resp.Body)
   275  	if err = dec.Decode(&d); err != nil {
   276  		return d, err
   277  	}
   278  	return d, nil
   279  }
   280  
   281  // DefaultKVS - default config for OpenID config
   282  var (
   283  	DefaultKVS = config.KVS{
   284  		config.KV{
   285  			Key:   ConfigURL,
   286  			Value: "",
   287  		},
   288  		config.KV{
   289  			Key:   ClientID,
   290  			Value: "",
   291  		},
   292  		config.KV{
   293  			Key:   ClaimName,
   294  			Value: iampolicy.PolicyName,
   295  		},
   296  		config.KV{
   297  			Key:   ClaimPrefix,
   298  			Value: "",
   299  		},
   300  		config.KV{
   301  			Key:   Scopes,
   302  			Value: "",
   303  		},
   304  		config.KV{
   305  			Key:   JwksURL,
   306  			Value: "",
   307  		},
   308  	}
   309  )
   310  
   311  // Enabled returns if jwks is enabled.
   312  func Enabled(kvs config.KVS) bool {
   313  	return kvs.Get(JwksURL) != ""
   314  }
   315  
   316  // LookupConfig lookup jwks from config, override with any ENVs.
   317  func LookupConfig(kvs config.KVS, transport *http.Transport, closeRespFn func(io.ReadCloser)) (c Config, err error) {
   318  	if err = config.CheckValidKeys(config.IdentityOpenIDSubSys, kvs, DefaultKVS); err != nil {
   319  		return c, err
   320  	}
   321  
   322  	jwksURL := env.Get(EnvIamJwksURL, "") // Legacy
   323  	if jwksURL == "" {
   324  		jwksURL = env.Get(EnvIdentityOpenIDJWKSURL, kvs.Get(JwksURL))
   325  	}
   326  
   327  	c = Config{
   328  		ClaimName:   env.Get(EnvIdentityOpenIDClaimName, kvs.Get(ClaimName)),
   329  		ClaimPrefix: env.Get(EnvIdentityOpenIDClaimPrefix, kvs.Get(ClaimPrefix)),
   330  		publicKeys:  make(map[string]crypto.PublicKey),
   331  		ClientID:    env.Get(EnvIdentityOpenIDClientID, kvs.Get(ClientID)),
   332  		transport:   transport,
   333  		closeRespFn: closeRespFn,
   334  		mutex:       &sync.Mutex{}, // allocate for copying
   335  	}
   336  
   337  	configURL := env.Get(EnvIdentityOpenIDURL, kvs.Get(ConfigURL))
   338  	if configURL != "" {
   339  		c.URL, err = xnet.ParseHTTPURL(configURL)
   340  		if err != nil {
   341  			return c, err
   342  		}
   343  		c.DiscoveryDoc, err = parseDiscoveryDoc(c.URL, transport, closeRespFn)
   344  		if err != nil {
   345  			return c, err
   346  		}
   347  	}
   348  
   349  	if scopeList := env.Get(EnvIdentityOpenIDScopes, kvs.Get(Scopes)); scopeList != "" {
   350  		var scopes []string
   351  		for _, scope := range strings.Split(scopeList, ",") {
   352  			scope = strings.TrimSpace(scope)
   353  			if scope == "" {
   354  				return c, config.Errorf("empty scope value is not allowed '%s', please refer to our documentation", scopeList)
   355  			}
   356  			scopes = append(scopes, scope)
   357  		}
   358  		// Replace the discovery document scopes by client customized scopes.
   359  		c.DiscoveryDoc.ScopesSupported = scopes
   360  	}
   361  
   362  	if c.ClaimName == "" {
   363  		c.ClaimName = iampolicy.PolicyName
   364  	}
   365  
   366  	if jwksURL == "" {
   367  		// Fallback to discovery document jwksURL
   368  		jwksURL = c.DiscoveryDoc.JwksURI
   369  	}
   370  
   371  	if jwksURL == "" {
   372  		return c, nil
   373  	}
   374  
   375  	c.JWKS.URL, err = xnet.ParseHTTPURL(jwksURL)
   376  	if err != nil {
   377  		return c, err
   378  	}
   379  
   380  	if err = c.PopulatePublicKey(); err != nil {
   381  		return c, err
   382  	}
   383  
   384  	return c, nil
   385  }
   386  
   387  // NewJWT - initialize new jwt authenticator.
   388  func NewJWT(c Config) *JWT {
   389  	return &JWT{c}
   390  }