github.com/minio/minio@v0.0.0-20240328213742-3f72439b8a27/internal/config/identity/openid/jwt.go (about)

     1  // Copyright (c) 2015-2022 MinIO, Inc.
     2  //
     3  // This file is part of MinIO Object Storage stack
     4  //
     5  // This program is free software: you can redistribute it and/or modify
     6  // it under the terms of the GNU Affero General Public License as published by
     7  // the Free Software Foundation, either version 3 of the License, or
     8  // (at your option) any later version.
     9  //
    10  // This program is distributed in the hope that it will be useful
    11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    13  // GNU Affero General Public License for more details.
    14  //
    15  // You should have received a copy of the GNU Affero General Public License
    16  // along with this program.  If not, see <http://www.gnu.org/licenses/>.
    17  
    18  package openid
    19  
    20  import (
    21  	"context"
    22  	"encoding/json"
    23  	"errors"
    24  	"fmt"
    25  	"io"
    26  	"net/http"
    27  	"sync"
    28  	"time"
    29  
    30  	jwtgo "github.com/golang-jwt/jwt/v4"
    31  	"github.com/minio/minio/internal/arn"
    32  	"github.com/minio/minio/internal/auth"
    33  	xnet "github.com/minio/pkg/v2/net"
    34  	"github.com/minio/pkg/v2/policy"
    35  )
    36  
    37  type publicKeys struct {
    38  	*sync.RWMutex
    39  
    40  	// map of kid to public key
    41  	pkMap map[string]interface{}
    42  }
    43  
    44  func (pk *publicKeys) parseAndAdd(b io.Reader) error {
    45  	var jwk JWKS
    46  	err := json.NewDecoder(b).Decode(&jwk)
    47  	if err != nil {
    48  		return err
    49  	}
    50  
    51  	for _, key := range jwk.Keys {
    52  		pkey, err := key.DecodePublicKey()
    53  		if err != nil {
    54  			return err
    55  		}
    56  		pk.add(key.Kid, pkey)
    57  	}
    58  
    59  	return nil
    60  }
    61  
    62  func (pk *publicKeys) add(keyID string, key interface{}) {
    63  	pk.Lock()
    64  	defer pk.Unlock()
    65  
    66  	pk.pkMap[keyID] = key
    67  }
    68  
    69  func (pk *publicKeys) get(kid string) interface{} {
    70  	pk.RLock()
    71  	defer pk.RUnlock()
    72  	return pk.pkMap[kid]
    73  }
    74  
    75  // PopulatePublicKey - populates a new publickey from the JWKS URL.
    76  func (r *Config) PopulatePublicKey(arn arn.ARN) error {
    77  	pCfg := r.arnProviderCfgsMap[arn]
    78  	if pCfg.JWKS.URL == nil || pCfg.JWKS.URL.String() == "" {
    79  		return nil
    80  	}
    81  
    82  	// Add client secret for the client ID for HMAC based signature.
    83  	r.pubKeys.add(pCfg.ClientID, []byte(pCfg.ClientSecret))
    84  
    85  	client := &http.Client{
    86  		Transport: r.transport,
    87  	}
    88  
    89  	resp, err := client.Get(pCfg.JWKS.URL.String())
    90  	if err != nil {
    91  		return err
    92  	}
    93  	defer r.closeRespFn(resp.Body)
    94  	if resp.StatusCode != http.StatusOK {
    95  		return errors.New(resp.Status)
    96  	}
    97  
    98  	return r.pubKeys.parseAndAdd(resp.Body)
    99  }
   100  
   101  // ErrTokenExpired - error token expired
   102  var (
   103  	ErrTokenExpired = errors.New("token expired")
   104  )
   105  
   106  func updateClaimsExpiry(dsecs string, claims map[string]interface{}) error {
   107  	expStr := claims["exp"]
   108  	if expStr == "" {
   109  		return ErrTokenExpired
   110  	}
   111  
   112  	// No custom duration requested, the claims can be used as is.
   113  	if dsecs == "" {
   114  		return nil
   115  	}
   116  
   117  	if _, err := auth.ExpToInt64(expStr); err != nil {
   118  		return err
   119  	}
   120  
   121  	defaultExpiryDuration, err := GetDefaultExpiration(dsecs)
   122  	if err != nil {
   123  		return err
   124  	}
   125  
   126  	claims["exp"] = time.Now().UTC().Add(defaultExpiryDuration).Unix() // update with new expiry.
   127  	return nil
   128  }
   129  
   130  const (
   131  	audClaim = "aud"
   132  	azpClaim = "azp"
   133  )
   134  
   135  // Validate - validates the id_token.
   136  func (r *Config) Validate(ctx context.Context, arn arn.ARN, token, accessToken, dsecs string, claims jwtgo.MapClaims) error {
   137  	jp := new(jwtgo.Parser)
   138  	jp.ValidMethods = []string{
   139  		"RS256", "RS384", "RS512",
   140  		"ES256", "ES384", "ES512",
   141  		"HS256", "HS384", "HS512",
   142  		"RS3256", "RS3384", "RS3512",
   143  		"ES3256", "ES3384", "ES3512",
   144  	}
   145  
   146  	keyFuncCallback := func(jwtToken *jwtgo.Token) (interface{}, error) {
   147  		kid, ok := jwtToken.Header["kid"].(string)
   148  		if !ok {
   149  			return nil, fmt.Errorf("Invalid kid value %v", jwtToken.Header["kid"])
   150  		}
   151  		return r.pubKeys.get(kid), nil
   152  	}
   153  
   154  	pCfg, ok := r.arnProviderCfgsMap[arn]
   155  	if !ok {
   156  		return fmt.Errorf("Role %s does not exist", arn)
   157  	}
   158  
   159  	jwtToken, err := jp.ParseWithClaims(token, &claims, keyFuncCallback)
   160  	if err != nil {
   161  		// Re-populate the public key in-case the JWKS
   162  		// pubkeys are refreshed
   163  		if err = r.PopulatePublicKey(arn); err != nil {
   164  			return err
   165  		}
   166  		jwtToken, err = jwtgo.ParseWithClaims(token, &claims, keyFuncCallback)
   167  		if err != nil {
   168  			return err
   169  		}
   170  	}
   171  
   172  	if !jwtToken.Valid {
   173  		return ErrTokenExpired
   174  	}
   175  
   176  	if err = updateClaimsExpiry(dsecs, claims); err != nil {
   177  		return err
   178  	}
   179  
   180  	if err = r.updateUserinfoClaims(ctx, arn, accessToken, claims); err != nil {
   181  		return err
   182  	}
   183  
   184  	// Validate that matching clientID appears in the aud or azp claims.
   185  
   186  	// REQUIRED. Audience(s) that this ID Token is intended for.
   187  	// It MUST contain the OAuth 2.0 client_id of the Relying Party
   188  	// as an audience value. It MAY also contain identifiers for
   189  	// other audiences. In the general case, the aud value is an
   190  	// array of case sensitive strings. In the common special case
   191  	// when there is one audience, the aud value MAY be a single
   192  	// case sensitive
   193  	audValues, ok := policy.GetValuesFromClaims(claims, audClaim)
   194  	if !ok {
   195  		return errors.New("STS JWT Token has `aud` claim invalid, `aud` must match configured OpenID Client ID")
   196  	}
   197  	if !audValues.Contains(pCfg.ClientID) {
   198  		// if audience claims is missing, look for "azp" claims.
   199  		// OPTIONAL. Authorized party - the party to which the ID
   200  		// Token was issued. If present, it MUST contain the OAuth
   201  		// 2.0 Client ID of this party. This Claim is only needed
   202  		// when the ID Token has a single audience value and that
   203  		// audience is different than the authorized party. It MAY
   204  		// be included even when the authorized party is the same
   205  		// as the sole audience. The azp value is a case sensitive
   206  		// string containing a StringOrURI value
   207  		azpValues, ok := policy.GetValuesFromClaims(claims, azpClaim)
   208  		if !ok {
   209  			return errors.New("STS JWT Token has `azp` claim invalid, `azp` must match configured OpenID Client ID")
   210  		}
   211  		if !azpValues.Contains(pCfg.ClientID) {
   212  			return errors.New("STS JWT Token has `azp` claim invalid, `azp` must match configured OpenID Client ID")
   213  		}
   214  	}
   215  
   216  	return nil
   217  }
   218  
   219  func (r *Config) updateUserinfoClaims(ctx context.Context, arn arn.ARN, accessToken string, claims map[string]interface{}) error {
   220  	pCfg, ok := r.arnProviderCfgsMap[arn]
   221  	// If claim user info is enabled, get claims from userInfo
   222  	// and overwrite them with the claims from JWT.
   223  	if ok && pCfg.ClaimUserinfo {
   224  		if accessToken == "" {
   225  			return errors.New("access_token is mandatory if user_info claim is enabled")
   226  		}
   227  		uclaims, err := pCfg.UserInfo(ctx, accessToken, r.transport)
   228  		if err != nil {
   229  			return err
   230  		}
   231  		for k, v := range uclaims {
   232  			if _, ok := claims[k]; !ok { // only add to claims not update it.
   233  				claims[k] = v
   234  			}
   235  		}
   236  	}
   237  	return nil
   238  }
   239  
   240  // DiscoveryDoc - parses the output from openid-configuration
   241  // for example https://accounts.google.com/.well-known/openid-configuration
   242  type DiscoveryDoc struct {
   243  	Issuer                           string   `json:"issuer,omitempty"`
   244  	AuthEndpoint                     string   `json:"authorization_endpoint,omitempty"`
   245  	TokenEndpoint                    string   `json:"token_endpoint,omitempty"`
   246  	EndSessionEndpoint               string   `json:"end_session_endpoint,omitempty"`
   247  	UserInfoEndpoint                 string   `json:"userinfo_endpoint,omitempty"`
   248  	RevocationEndpoint               string   `json:"revocation_endpoint,omitempty"`
   249  	JwksURI                          string   `json:"jwks_uri,omitempty"`
   250  	ResponseTypesSupported           []string `json:"response_types_supported,omitempty"`
   251  	SubjectTypesSupported            []string `json:"subject_types_supported,omitempty"`
   252  	IDTokenSigningAlgValuesSupported []string `json:"id_token_signing_alg_values_supported,omitempty"`
   253  	ScopesSupported                  []string `json:"scopes_supported,omitempty"`
   254  	TokenEndpointAuthMethods         []string `json:"token_endpoint_auth_methods_supported,omitempty"`
   255  	ClaimsSupported                  []string `json:"claims_supported,omitempty"`
   256  	CodeChallengeMethodsSupported    []string `json:"code_challenge_methods_supported,omitempty"`
   257  }
   258  
   259  func parseDiscoveryDoc(u *xnet.URL, transport http.RoundTripper, closeRespFn func(io.ReadCloser)) (DiscoveryDoc, error) {
   260  	d := DiscoveryDoc{}
   261  	req, err := http.NewRequest(http.MethodGet, u.String(), nil)
   262  	if err != nil {
   263  		return d, err
   264  	}
   265  	clnt := http.Client{
   266  		Transport: transport,
   267  	}
   268  	resp, err := clnt.Do(req)
   269  	if err != nil {
   270  		return d, err
   271  	}
   272  	defer closeRespFn(resp.Body)
   273  	if resp.StatusCode != http.StatusOK {
   274  		return d, fmt.Errorf("unexpected error returned by %s : status(%s)", u, resp.Status)
   275  	}
   276  	dec := json.NewDecoder(resp.Body)
   277  	if err = dec.Decode(&d); err != nil {
   278  		return d, err
   279  	}
   280  	return d, nil
   281  }