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 }