mirror of
https://github.com/minio/minio.git
synced 2025-05-22 18:11:50 -04:00
Reorganize OpenID config (#14871)
- Split into multiple files - Remove JSON unmarshaler for Config and providerCfg types (unused)
This commit is contained in:
parent
111745c564
commit
e55104a155
@ -1,4 +1,4 @@
|
|||||||
// Copyright (c) 2015-2021 MinIO, Inc.
|
// Copyright (c) 2015-2022 MinIO, Inc.
|
||||||
//
|
//
|
||||||
// This file is part of MinIO Object Storage stack
|
// This file is part of MinIO Object Storage stack
|
||||||
//
|
//
|
||||||
@ -19,34 +19,21 @@ package openid
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto"
|
"crypto"
|
||||||
"crypto/sha1"
|
|
||||||
"crypto/sha256"
|
|
||||||
"encoding/base64"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
jwtgo "github.com/golang-jwt/jwt/v4"
|
jwtgo "github.com/golang-jwt/jwt/v4"
|
||||||
"github.com/minio/madmin-go"
|
|
||||||
"github.com/minio/minio-go/v7/pkg/set"
|
|
||||||
"github.com/minio/minio/internal/arn"
|
"github.com/minio/minio/internal/arn"
|
||||||
"github.com/minio/minio/internal/auth"
|
"github.com/minio/minio/internal/auth"
|
||||||
"github.com/minio/minio/internal/config"
|
|
||||||
"github.com/minio/minio/internal/config/identity/openid/provider"
|
|
||||||
xhttp "github.com/minio/minio/internal/http"
|
|
||||||
"github.com/minio/pkg/env"
|
|
||||||
iampolicy "github.com/minio/pkg/iam/policy"
|
iampolicy "github.com/minio/pkg/iam/policy"
|
||||||
xnet "github.com/minio/pkg/net"
|
xnet "github.com/minio/pkg/net"
|
||||||
)
|
)
|
||||||
|
|
||||||
var errSingleProvider = config.Errorf("Only one OpenID provider can be configured if not using role policy mapping")
|
|
||||||
|
|
||||||
type publicKeys struct {
|
type publicKeys struct {
|
||||||
*sync.RWMutex
|
*sync.RWMutex
|
||||||
|
|
||||||
@ -79,185 +66,6 @@ func (pk *publicKeys) get(kid string) crypto.PublicKey {
|
|||||||
return pk.pkMap[kid]
|
return pk.pkMap[kid]
|
||||||
}
|
}
|
||||||
|
|
||||||
type providerCfg struct {
|
|
||||||
// Used for user interface like console
|
|
||||||
DisplayName string `json:"displayName,omitempty"`
|
|
||||||
|
|
||||||
JWKS struct {
|
|
||||||
URL *xnet.URL `json:"url"`
|
|
||||||
} `json:"jwks"`
|
|
||||||
URL *xnet.URL `json:"url,omitempty"`
|
|
||||||
ClaimPrefix string `json:"claimPrefix,omitempty"`
|
|
||||||
ClaimName string `json:"claimName,omitempty"`
|
|
||||||
ClaimUserinfo bool `json:"claimUserInfo,omitempty"`
|
|
||||||
RedirectURI string `json:"redirectURI,omitempty"`
|
|
||||||
RedirectURIDynamic bool `json:"redirectURIDynamic"`
|
|
||||||
DiscoveryDoc DiscoveryDoc
|
|
||||||
ClientID string
|
|
||||||
ClientSecret string
|
|
||||||
RolePolicy string
|
|
||||||
|
|
||||||
roleArn arn.ARN
|
|
||||||
provider provider.Provider
|
|
||||||
}
|
|
||||||
|
|
||||||
// initializeProvider initializes if any additional vendor specific information
|
|
||||||
// was provided, initialization will return an error initial login fails.
|
|
||||||
func (p *providerCfg) initializeProvider(cfgGet func(string, string) string, transport http.RoundTripper) error {
|
|
||||||
vendor := cfgGet(EnvIdentityOpenIDVendor, Vendor)
|
|
||||||
if vendor == "" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
var err error
|
|
||||||
switch vendor {
|
|
||||||
case keyCloakVendor:
|
|
||||||
adminURL := cfgGet(EnvIdentityOpenIDKeyCloakAdminURL, KeyCloakAdminURL)
|
|
||||||
realm := cfgGet(EnvIdentityOpenIDKeyCloakRealm, KeyCloakRealm)
|
|
||||||
p.provider, err = provider.KeyCloak(
|
|
||||||
provider.WithAdminURL(adminURL),
|
|
||||||
provider.WithOpenIDConfig(provider.DiscoveryDoc(p.DiscoveryDoc)),
|
|
||||||
provider.WithTransport(transport),
|
|
||||||
provider.WithRealm(realm),
|
|
||||||
)
|
|
||||||
return err
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("Unsupport vendor %s", keyCloakVendor)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// UserInfo returns claims for authenticated user from userInfo endpoint.
|
|
||||||
//
|
|
||||||
// Some OIDC implementations such as GitLab do not support
|
|
||||||
// claims as part of the normal oauth2 flow, instead rely
|
|
||||||
// on service providers making calls to IDP to fetch additional
|
|
||||||
// claims available from the UserInfo endpoint
|
|
||||||
func (p *providerCfg) UserInfo(accessToken string, transport http.RoundTripper) (map[string]interface{}, error) {
|
|
||||||
if p.JWKS.URL == nil || p.JWKS.URL.String() == "" {
|
|
||||||
return nil, errors.New("openid not configured")
|
|
||||||
}
|
|
||||||
client := &http.Client{
|
|
||||||
Transport: transport,
|
|
||||||
}
|
|
||||||
|
|
||||||
req, err := http.NewRequest(http.MethodPost, p.DiscoveryDoc.UserInfoEndpoint, nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if accessToken != "" {
|
|
||||||
req.Header.Set("Authorization", "Bearer "+accessToken)
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := client.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
defer xhttp.DrainBody(resp.Body)
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
|
||||||
// uncomment this for debugging when needed.
|
|
||||||
// reqBytes, _ := httputil.DumpRequest(req, false)
|
|
||||||
// fmt.Println(string(reqBytes))
|
|
||||||
// respBytes, _ := httputil.DumpResponse(resp, true)
|
|
||||||
// fmt.Println(string(respBytes))
|
|
||||||
return nil, errors.New(resp.Status)
|
|
||||||
}
|
|
||||||
|
|
||||||
dec := json.NewDecoder(resp.Body)
|
|
||||||
claims := map[string]interface{}{}
|
|
||||||
|
|
||||||
if err = dec.Decode(&claims); err != nil {
|
|
||||||
// uncomment this for debugging when needed.
|
|
||||||
// reqBytes, _ := httputil.DumpRequest(req, false)
|
|
||||||
// fmt.Println(string(reqBytes))
|
|
||||||
// respBytes, _ := httputil.DumpResponse(resp, true)
|
|
||||||
// fmt.Println(string(respBytes))
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return claims, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Config - OpenID Config
|
|
||||||
// RSA authentication target arguments
|
|
||||||
type Config struct {
|
|
||||||
Enabled bool `json:"enabled"`
|
|
||||||
|
|
||||||
// map of roleARN to providerCfg's
|
|
||||||
arnProviderCfgsMap map[arn.ARN]*providerCfg
|
|
||||||
|
|
||||||
// map of config names to providerCfg's
|
|
||||||
ProviderCfgs map[string]*providerCfg
|
|
||||||
|
|
||||||
pubKeys publicKeys
|
|
||||||
roleArnPolicyMap map[arn.ARN]string
|
|
||||||
|
|
||||||
transport http.RoundTripper
|
|
||||||
closeRespFn func(io.ReadCloser)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetIAMPolicyClaimName - returns the policy claim name for the (at most one)
|
|
||||||
// provider configured without a role policy.
|
|
||||||
func (r *Config) GetIAMPolicyClaimName() string {
|
|
||||||
pCfg, ok := r.arnProviderCfgsMap[DummyRoleARN]
|
|
||||||
if !ok {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return pCfg.ClaimPrefix + pCfg.ClaimName
|
|
||||||
}
|
|
||||||
|
|
||||||
// LookupUser lookup userid for the provider
|
|
||||||
func (r Config) LookupUser(roleArn, userid string) (provider.User, error) {
|
|
||||||
// Can safely ignore error here as empty or invalid ARNs will not be
|
|
||||||
// mapped.
|
|
||||||
arnVal, _ := arn.Parse(roleArn)
|
|
||||||
pCfg, ok := r.arnProviderCfgsMap[arnVal]
|
|
||||||
if ok {
|
|
||||||
user, err := pCfg.provider.LookupUser(userid)
|
|
||||||
if err != nil && err != provider.ErrAccessTokenExpired {
|
|
||||||
return user, err
|
|
||||||
}
|
|
||||||
if err == provider.ErrAccessTokenExpired {
|
|
||||||
if err = pCfg.provider.LoginWithClientID(pCfg.ClientID, pCfg.ClientSecret); err != nil {
|
|
||||||
return user, err
|
|
||||||
}
|
|
||||||
user, err = pCfg.provider.LookupUser(userid)
|
|
||||||
}
|
|
||||||
return user, err
|
|
||||||
}
|
|
||||||
// Without any specific logic for a provider, all accounts
|
|
||||||
// are always enabled.
|
|
||||||
return provider.User{ID: userid, Enabled: true}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
|
||||||
keyCloakVendor = "keycloak"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ProviderEnabled returns true if any vendor specific provider is enabled.
|
|
||||||
func (r Config) ProviderEnabled() bool {
|
|
||||||
if !r.Enabled {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
for _, v := range r.arnProviderCfgsMap {
|
|
||||||
if v.provider != nil {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetRoleInfo - returns ARN to policies map if a role policy based openID
|
|
||||||
// provider is configured. Otherwise returns nil.
|
|
||||||
func (r Config) GetRoleInfo() map[arn.ARN]string {
|
|
||||||
for _, p := range r.arnProviderCfgsMap {
|
|
||||||
if p.RolePolicy != "" {
|
|
||||||
return r.roleArnPolicyMap
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// PopulatePublicKey - populates a new publickey from the JWKS URL.
|
// PopulatePublicKey - populates a new publickey from the JWKS URL.
|
||||||
func (r *Config) PopulatePublicKey(arn arn.ARN) error {
|
func (r *Config) PopulatePublicKey(arn arn.ARN) error {
|
||||||
pCfg := r.arnProviderCfgsMap[arn]
|
pCfg := r.arnProviderCfgsMap[arn]
|
||||||
@ -280,42 +88,6 @@ func (r *Config) PopulatePublicKey(arn arn.ARN) error {
|
|||||||
return r.pubKeys.parseAndAdd(resp.Body)
|
return r.pubKeys.parseAndAdd(resp.Body)
|
||||||
}
|
}
|
||||||
|
|
||||||
// UnmarshalJSON - decodes JSON data.
|
|
||||||
func (r *Config) UnmarshalJSON(data []byte) error {
|
|
||||||
// subtype to avoid recursive call to UnmarshalJSON()
|
|
||||||
type subConfig Config
|
|
||||||
var sr subConfig
|
|
||||||
|
|
||||||
if err := json.Unmarshal(data, &sr); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
ar := Config(sr)
|
|
||||||
*r = ar
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetDefaultExpiration - returns the expiration seconds expected.
|
|
||||||
func GetDefaultExpiration(dsecs string) (time.Duration, error) {
|
|
||||||
defaultExpiryDuration := time.Duration(60) * time.Minute // Defaults to 1hr.
|
|
||||||
if dsecs != "" {
|
|
||||||
expirySecs, err := strconv.ParseInt(dsecs, 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
return 0, auth.ErrInvalidDuration
|
|
||||||
}
|
|
||||||
|
|
||||||
// The duration, in seconds, of the role session.
|
|
||||||
// The value can range from 900 seconds (15 minutes)
|
|
||||||
// up to 365 days.
|
|
||||||
if expirySecs < 900 || expirySecs > 31536000 {
|
|
||||||
return 0, auth.ErrInvalidDuration
|
|
||||||
}
|
|
||||||
|
|
||||||
defaultExpiryDuration = time.Duration(expirySecs) * time.Second
|
|
||||||
}
|
|
||||||
return defaultExpiryDuration, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ErrTokenExpired - error token expired
|
// ErrTokenExpired - error token expired
|
||||||
var (
|
var (
|
||||||
ErrTokenExpired = errors.New("token expired")
|
ErrTokenExpired = errors.New("token expired")
|
||||||
@ -461,86 +233,6 @@ func (r *Config) updateUserinfoClaims(arn arn.ARN, accessToken string, claims ma
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetSettings - fetches OIDC settings for site-replication related validation.
|
|
||||||
// NOTE that region must be populated by caller as this package does not know.
|
|
||||||
func (r *Config) GetSettings() madmin.OpenIDSettings {
|
|
||||||
res := madmin.OpenIDSettings{}
|
|
||||||
if !r.Enabled {
|
|
||||||
return res
|
|
||||||
}
|
|
||||||
|
|
||||||
for arn, provCfg := range r.arnProviderCfgsMap {
|
|
||||||
hashedSecret := ""
|
|
||||||
{
|
|
||||||
h := sha256.New()
|
|
||||||
h.Write([]byte(provCfg.ClientSecret))
|
|
||||||
bs := h.Sum(nil)
|
|
||||||
hashedSecret = base64.RawURLEncoding.EncodeToString(bs)
|
|
||||||
}
|
|
||||||
if arn != DummyRoleARN {
|
|
||||||
if res.Roles != nil {
|
|
||||||
res.Roles = make(map[string]madmin.OpenIDProviderSettings)
|
|
||||||
}
|
|
||||||
res.Roles[arn.String()] = madmin.OpenIDProviderSettings{
|
|
||||||
ClaimUserinfoEnabled: provCfg.ClaimUserinfo,
|
|
||||||
RolePolicy: provCfg.RolePolicy,
|
|
||||||
ClientID: provCfg.ClientID,
|
|
||||||
HashedClientSecret: hashedSecret,
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
res.ClaimProvider = madmin.OpenIDProviderSettings{
|
|
||||||
ClaimUserinfoEnabled: provCfg.ClaimUserinfo,
|
|
||||||
RolePolicy: provCfg.RolePolicy,
|
|
||||||
ClientID: provCfg.ClientID,
|
|
||||||
HashedClientSecret: hashedSecret,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
return res
|
|
||||||
}
|
|
||||||
|
|
||||||
// OpenID keys and envs.
|
|
||||||
const (
|
|
||||||
JwksURL = "jwks_url"
|
|
||||||
ConfigURL = "config_url"
|
|
||||||
ClaimName = "claim_name"
|
|
||||||
ClaimUserinfo = "claim_userinfo"
|
|
||||||
ClaimPrefix = "claim_prefix"
|
|
||||||
ClientID = "client_id"
|
|
||||||
ClientSecret = "client_secret"
|
|
||||||
RolePolicy = "role_policy"
|
|
||||||
DisplayName = "display_name"
|
|
||||||
|
|
||||||
Vendor = "vendor"
|
|
||||||
Scopes = "scopes"
|
|
||||||
RedirectURI = "redirect_uri"
|
|
||||||
RedirectURIDynamic = "redirect_uri_dynamic"
|
|
||||||
|
|
||||||
// Vendor specific ENV only enabled if the Vendor matches == "vendor"
|
|
||||||
KeyCloakRealm = "keycloak_realm"
|
|
||||||
KeyCloakAdminURL = "keycloak_admin_url"
|
|
||||||
|
|
||||||
EnvIdentityOpenIDEnable = "MINIO_IDENTITY_OPENID_ENABLE"
|
|
||||||
EnvIdentityOpenIDVendor = "MINIO_IDENTITY_OPENID_VENDOR"
|
|
||||||
EnvIdentityOpenIDClientID = "MINIO_IDENTITY_OPENID_CLIENT_ID"
|
|
||||||
EnvIdentityOpenIDClientSecret = "MINIO_IDENTITY_OPENID_CLIENT_SECRET"
|
|
||||||
EnvIdentityOpenIDURL = "MINIO_IDENTITY_OPENID_CONFIG_URL"
|
|
||||||
EnvIdentityOpenIDClaimName = "MINIO_IDENTITY_OPENID_CLAIM_NAME"
|
|
||||||
EnvIdentityOpenIDClaimUserInfo = "MINIO_IDENTITY_OPENID_CLAIM_USERINFO"
|
|
||||||
EnvIdentityOpenIDClaimPrefix = "MINIO_IDENTITY_OPENID_CLAIM_PREFIX"
|
|
||||||
EnvIdentityOpenIDRolePolicy = "MINIO_IDENTITY_OPENID_ROLE_POLICY"
|
|
||||||
EnvIdentityOpenIDRedirectURI = "MINIO_IDENTITY_OPENID_REDIRECT_URI"
|
|
||||||
EnvIdentityOpenIDRedirectURIDynamic = "MINIO_IDENTITY_OPENID_REDIRECT_URI_DYNAMIC"
|
|
||||||
EnvIdentityOpenIDScopes = "MINIO_IDENTITY_OPENID_SCOPES"
|
|
||||||
EnvIdentityOpenIDDisplayName = "MINIO_IDENTITY_OPENID_DISPLAY_NAME"
|
|
||||||
|
|
||||||
// Vendor specific ENVs only enabled if the Vendor matches == "vendor"
|
|
||||||
EnvIdentityOpenIDKeyCloakRealm = "MINIO_IDENTITY_OPENID_KEYCLOAK_REALM"
|
|
||||||
EnvIdentityOpenIDKeyCloakAdminURL = "MINIO_IDENTITY_OPENID_KEYCLOAK_ADMIN_URL"
|
|
||||||
)
|
|
||||||
|
|
||||||
// DiscoveryDoc - parses the output from openid-configuration
|
// DiscoveryDoc - parses the output from openid-configuration
|
||||||
// for example https://accounts.google.com/.well-known/openid-configuration
|
// for example https://accounts.google.com/.well-known/openid-configuration
|
||||||
type DiscoveryDoc struct {
|
type DiscoveryDoc struct {
|
||||||
@ -582,312 +274,3 @@ func parseDiscoveryDoc(u *xnet.URL, transport http.RoundTripper, closeRespFn fun
|
|||||||
}
|
}
|
||||||
return d, nil
|
return d, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// DefaultKVS - default config for OpenID config
|
|
||||||
var (
|
|
||||||
DefaultKVS = config.KVS{
|
|
||||||
config.KV{
|
|
||||||
Key: config.Enable,
|
|
||||||
Value: "",
|
|
||||||
},
|
|
||||||
config.KV{
|
|
||||||
Key: DisplayName,
|
|
||||||
Value: "",
|
|
||||||
},
|
|
||||||
config.KV{
|
|
||||||
Key: ConfigURL,
|
|
||||||
Value: "",
|
|
||||||
},
|
|
||||||
config.KV{
|
|
||||||
Key: ClientID,
|
|
||||||
Value: "",
|
|
||||||
},
|
|
||||||
config.KV{
|
|
||||||
Key: ClientSecret,
|
|
||||||
Value: "",
|
|
||||||
},
|
|
||||||
config.KV{
|
|
||||||
Key: ClaimName,
|
|
||||||
Value: iampolicy.PolicyName,
|
|
||||||
},
|
|
||||||
config.KV{
|
|
||||||
Key: ClaimUserinfo,
|
|
||||||
Value: "",
|
|
||||||
},
|
|
||||||
config.KV{
|
|
||||||
Key: RolePolicy,
|
|
||||||
Value: "",
|
|
||||||
},
|
|
||||||
config.KV{
|
|
||||||
Key: ClaimPrefix,
|
|
||||||
Value: "",
|
|
||||||
},
|
|
||||||
config.KV{
|
|
||||||
Key: RedirectURI,
|
|
||||||
Value: "",
|
|
||||||
},
|
|
||||||
config.KV{
|
|
||||||
Key: RedirectURIDynamic,
|
|
||||||
Value: "off",
|
|
||||||
},
|
|
||||||
config.KV{
|
|
||||||
Key: Scopes,
|
|
||||||
Value: "",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
// Enabled returns if configURL is enabled.
|
|
||||||
func Enabled(kvs config.KVS) bool {
|
|
||||||
return kvs.Get(ConfigURL) != ""
|
|
||||||
}
|
|
||||||
|
|
||||||
// DummyRoleARN is used to indicate that the user associated with it was
|
|
||||||
// authenticated via policy-claim based OpenID provider.
|
|
||||||
var DummyRoleARN = func() arn.ARN {
|
|
||||||
v, err := arn.NewIAMRoleARN("dummy-internal", "")
|
|
||||||
if err != nil {
|
|
||||||
panic("should not happen!")
|
|
||||||
}
|
|
||||||
return v
|
|
||||||
}()
|
|
||||||
|
|
||||||
// LookupConfig lookup jwks from config, override with any ENVs.
|
|
||||||
func LookupConfig(kvsMap map[string]config.KVS, transport http.RoundTripper, closeRespFn func(io.ReadCloser), serverRegion string) (c Config, err error) {
|
|
||||||
openIDClientTransport := http.DefaultTransport
|
|
||||||
if transport != nil {
|
|
||||||
openIDClientTransport = transport
|
|
||||||
}
|
|
||||||
c = Config{
|
|
||||||
Enabled: false,
|
|
||||||
arnProviderCfgsMap: map[arn.ARN]*providerCfg{},
|
|
||||||
ProviderCfgs: map[string]*providerCfg{},
|
|
||||||
pubKeys: publicKeys{
|
|
||||||
RWMutex: &sync.RWMutex{},
|
|
||||||
pkMap: map[string]crypto.PublicKey{},
|
|
||||||
},
|
|
||||||
roleArnPolicyMap: map[arn.ARN]string{},
|
|
||||||
transport: openIDClientTransport,
|
|
||||||
closeRespFn: closeRespFn,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Make a copy of the config we received so we can mutate it safely.
|
|
||||||
kvsMap2 := make(map[string]config.KVS, len(kvsMap))
|
|
||||||
for k, v := range kvsMap {
|
|
||||||
kvsMap2[k] = v
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add in each configuration name found from environment variables, i.e.
|
|
||||||
// if we see MINIO_IDENTITY_OPENID_CONFIG_URL_2, we add the key "2" to
|
|
||||||
// `kvsMap2` if it does not already exist.
|
|
||||||
envs := env.List(EnvIdentityOpenIDURL + config.Default)
|
|
||||||
for _, k := range envs {
|
|
||||||
cfgName := strings.TrimPrefix(k, EnvIdentityOpenIDURL+config.Default)
|
|
||||||
if cfgName == "" {
|
|
||||||
return c, config.Errorf("Environment variable must have a non-empty config name: %s", k)
|
|
||||||
}
|
|
||||||
|
|
||||||
// It is possible that some variables were specified via config
|
|
||||||
// commands and some variables are intended to be overridden
|
|
||||||
// from the environment, so we ensure that the key is not
|
|
||||||
// overwritten in `kvsMap2` as it may have existing config.
|
|
||||||
if _, ok := kvsMap2[cfgName]; !ok {
|
|
||||||
kvsMap2[cfgName] = DefaultKVS
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
hasLegacyPolicyMapping = false
|
|
||||||
seenClientIDs = set.NewStringSet()
|
|
||||||
)
|
|
||||||
for cfgName, kvs := range kvsMap2 {
|
|
||||||
// remove this since we have removed support for this already.
|
|
||||||
kvs.Delete(JwksURL)
|
|
||||||
|
|
||||||
if err = config.CheckValidKeys(config.IdentityOpenIDSubSys, kvs, DefaultKVS); err != nil {
|
|
||||||
return c, err
|
|
||||||
}
|
|
||||||
|
|
||||||
getCfgVal := func(envVar, cfgParam string) string {
|
|
||||||
if cfgName != config.Default {
|
|
||||||
envVar += config.Default + cfgName
|
|
||||||
}
|
|
||||||
return env.Get(envVar, kvs.Get(cfgParam))
|
|
||||||
}
|
|
||||||
|
|
||||||
// In the past, when only one openID provider was allowed, there
|
|
||||||
// was no `enable` parameter - the configuration is turned off
|
|
||||||
// by clearing the values. With multiple providers, we support
|
|
||||||
// individually enabling/disabling provider configurations. If
|
|
||||||
// the enable parameter's value is non-empty, we use that
|
|
||||||
// setting, otherwise we treat it as enabled if some important
|
|
||||||
// parameters are non-empty.
|
|
||||||
var (
|
|
||||||
cfgEnableVal = getCfgVal(EnvIdentityOpenIDEnable, config.Enable)
|
|
||||||
isExplicitlyEnabled = false
|
|
||||||
)
|
|
||||||
if cfgEnableVal != "" {
|
|
||||||
isExplicitlyEnabled = true
|
|
||||||
}
|
|
||||||
|
|
||||||
var enabled bool
|
|
||||||
if isExplicitlyEnabled {
|
|
||||||
enabled, err = config.ParseBool(cfgEnableVal)
|
|
||||||
if err != nil {
|
|
||||||
return c, err
|
|
||||||
}
|
|
||||||
// No need to continue loading if the config is not enabled.
|
|
||||||
if !enabled {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
p := providerCfg{
|
|
||||||
DisplayName: getCfgVal(EnvIdentityOpenIDDisplayName, DisplayName),
|
|
||||||
ClaimName: getCfgVal(EnvIdentityOpenIDClaimName, ClaimName),
|
|
||||||
ClaimUserinfo: getCfgVal(EnvIdentityOpenIDClaimUserInfo, ClaimUserinfo) == config.EnableOn,
|
|
||||||
ClaimPrefix: getCfgVal(EnvIdentityOpenIDClaimPrefix, ClaimPrefix),
|
|
||||||
RedirectURI: getCfgVal(EnvIdentityOpenIDRedirectURI, RedirectURI),
|
|
||||||
RedirectURIDynamic: getCfgVal(EnvIdentityOpenIDRedirectURIDynamic, RedirectURIDynamic) == config.EnableOn,
|
|
||||||
ClientID: getCfgVal(EnvIdentityOpenIDClientID, ClientID),
|
|
||||||
ClientSecret: getCfgVal(EnvIdentityOpenIDClientSecret, ClientSecret),
|
|
||||||
RolePolicy: getCfgVal(EnvIdentityOpenIDRolePolicy, RolePolicy),
|
|
||||||
}
|
|
||||||
|
|
||||||
configURL := getCfgVal(EnvIdentityOpenIDURL, ConfigURL)
|
|
||||||
|
|
||||||
if !isExplicitlyEnabled {
|
|
||||||
enabled = true
|
|
||||||
if p.ClientID == "" && p.ClientSecret == "" && configURL == "" {
|
|
||||||
enabled = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// No need to continue loading if the config is not enabled.
|
|
||||||
if !enabled {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate that client ID has not been duplicately specified.
|
|
||||||
if seenClientIDs.Contains(p.ClientID) {
|
|
||||||
return c, config.Errorf("Client ID %s is present with multiple OpenID configurations", p.ClientID)
|
|
||||||
}
|
|
||||||
seenClientIDs.Add(p.ClientID)
|
|
||||||
|
|
||||||
p.URL, err = xnet.ParseHTTPURL(configURL)
|
|
||||||
if err != nil {
|
|
||||||
return c, err
|
|
||||||
}
|
|
||||||
configURLDomain := p.URL.Hostname()
|
|
||||||
p.DiscoveryDoc, err = parseDiscoveryDoc(p.URL, transport, closeRespFn)
|
|
||||||
if err != nil {
|
|
||||||
return c, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if p.ClaimUserinfo && configURL == "" {
|
|
||||||
return c, errors.New("please specify config_url to enable fetching claims from UserInfo endpoint")
|
|
||||||
}
|
|
||||||
|
|
||||||
if scopeList := getCfgVal(EnvIdentityOpenIDScopes, Scopes); scopeList != "" {
|
|
||||||
var scopes []string
|
|
||||||
for _, scope := range strings.Split(scopeList, ",") {
|
|
||||||
scope = strings.TrimSpace(scope)
|
|
||||||
if scope == "" {
|
|
||||||
return c, config.Errorf("empty scope value is not allowed '%s', please refer to our documentation", scopeList)
|
|
||||||
}
|
|
||||||
scopes = append(scopes, scope)
|
|
||||||
}
|
|
||||||
// Replace the discovery document scopes by client customized scopes.
|
|
||||||
p.DiscoveryDoc.ScopesSupported = scopes
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if claim name is the non-default value and role policy is set.
|
|
||||||
if p.ClaimName != iampolicy.PolicyName && p.RolePolicy != "" {
|
|
||||||
// In the unlikely event that the user specifies
|
|
||||||
// `iampolicy.PolicyName` as the claim name explicitly and sets
|
|
||||||
// a role policy, this check is thwarted, but we will be using
|
|
||||||
// the role policy anyway.
|
|
||||||
return c, config.Errorf("Role Policy (=`%s`) and Claim Name (=`%s`) cannot both be set", p.RolePolicy, p.ClaimName)
|
|
||||||
}
|
|
||||||
|
|
||||||
jwksURL := p.DiscoveryDoc.JwksURI
|
|
||||||
if jwksURL == "" {
|
|
||||||
return c, config.Errorf("no JWKS URI found in your provider's discovery doc (config_url=%s)", configURL)
|
|
||||||
}
|
|
||||||
|
|
||||||
p.JWKS.URL, err = xnet.ParseHTTPURL(jwksURL)
|
|
||||||
if err != nil {
|
|
||||||
return c, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if p.RolePolicy != "" {
|
|
||||||
// RolePolicy is validated by IAM System during its
|
|
||||||
// initialization.
|
|
||||||
|
|
||||||
// Generate role ARN as combination of provider domain and
|
|
||||||
// prefix of client ID.
|
|
||||||
domain := configURLDomain
|
|
||||||
if domain == "" {
|
|
||||||
// Attempt to parse the JWKs URI.
|
|
||||||
domain = p.JWKS.URL.Hostname()
|
|
||||||
if domain == "" {
|
|
||||||
return c, config.Errorf("unable to generate a domain from the OpenID config")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if p.ClientID == "" {
|
|
||||||
return c, config.Errorf("client ID must not be empty")
|
|
||||||
}
|
|
||||||
|
|
||||||
// We set the resource ID of the role arn as a hash of client
|
|
||||||
// ID, so we can get a short roleARN that stays the same on
|
|
||||||
// restart.
|
|
||||||
var resourceID string
|
|
||||||
{
|
|
||||||
h := sha1.New()
|
|
||||||
h.Write([]byte(p.ClientID))
|
|
||||||
bs := h.Sum(nil)
|
|
||||||
resourceID = base64.RawURLEncoding.EncodeToString(bs)
|
|
||||||
}
|
|
||||||
p.roleArn, err = arn.NewIAMRoleARN(resourceID, serverRegion)
|
|
||||||
if err != nil {
|
|
||||||
return c, config.Errorf("unable to generate ARN from the OpenID config: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
c.roleArnPolicyMap[p.roleArn] = p.RolePolicy
|
|
||||||
} else if p.ClaimName == "" {
|
|
||||||
return c, config.Errorf("A role policy or claim name must be specified")
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = p.initializeProvider(getCfgVal, c.transport); err != nil {
|
|
||||||
return c, err
|
|
||||||
}
|
|
||||||
|
|
||||||
arnKey := p.roleArn
|
|
||||||
if p.RolePolicy == "" {
|
|
||||||
arnKey = DummyRoleARN
|
|
||||||
hasLegacyPolicyMapping = true
|
|
||||||
// Ensure that when a JWT policy claim based provider
|
|
||||||
// exists, it is the only one.
|
|
||||||
if _, ok := c.arnProviderCfgsMap[DummyRoleARN]; ok {
|
|
||||||
return c, errSingleProvider
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
c.arnProviderCfgsMap[arnKey] = &p
|
|
||||||
c.ProviderCfgs[cfgName] = &p
|
|
||||||
|
|
||||||
if err = c.PopulatePublicKey(arnKey); err != nil {
|
|
||||||
return c, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure that when a JWT policy claim based provider
|
|
||||||
// exists, it is the only one.
|
|
||||||
if hasLegacyPolicyMapping && len(c.ProviderCfgs) > 1 {
|
|
||||||
return c, errSingleProvider
|
|
||||||
}
|
|
||||||
|
|
||||||
c.Enabled = true
|
|
||||||
|
|
||||||
return c, nil
|
|
||||||
}
|
|
||||||
|
518
internal/config/identity/openid/openid.go
Normal file
518
internal/config/identity/openid/openid.go
Normal file
@ -0,0 +1,518 @@
|
|||||||
|
// Copyright (c) 2015-2022 MinIO, Inc.
|
||||||
|
//
|
||||||
|
// This file is part of MinIO Object Storage stack
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package openid
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto"
|
||||||
|
"crypto/sha1"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/base64"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/minio/madmin-go"
|
||||||
|
"github.com/minio/minio-go/v7/pkg/set"
|
||||||
|
"github.com/minio/minio/internal/arn"
|
||||||
|
"github.com/minio/minio/internal/auth"
|
||||||
|
"github.com/minio/minio/internal/config"
|
||||||
|
"github.com/minio/minio/internal/config/identity/openid/provider"
|
||||||
|
"github.com/minio/pkg/env"
|
||||||
|
iampolicy "github.com/minio/pkg/iam/policy"
|
||||||
|
xnet "github.com/minio/pkg/net"
|
||||||
|
)
|
||||||
|
|
||||||
|
// OpenID keys and envs.
|
||||||
|
const (
|
||||||
|
JwksURL = "jwks_url"
|
||||||
|
ConfigURL = "config_url"
|
||||||
|
ClaimName = "claim_name"
|
||||||
|
ClaimUserinfo = "claim_userinfo"
|
||||||
|
ClaimPrefix = "claim_prefix"
|
||||||
|
ClientID = "client_id"
|
||||||
|
ClientSecret = "client_secret"
|
||||||
|
RolePolicy = "role_policy"
|
||||||
|
DisplayName = "display_name"
|
||||||
|
|
||||||
|
Vendor = "vendor"
|
||||||
|
Scopes = "scopes"
|
||||||
|
RedirectURI = "redirect_uri"
|
||||||
|
RedirectURIDynamic = "redirect_uri_dynamic"
|
||||||
|
|
||||||
|
// Vendor specific ENV only enabled if the Vendor matches == "vendor"
|
||||||
|
KeyCloakRealm = "keycloak_realm"
|
||||||
|
KeyCloakAdminURL = "keycloak_admin_url"
|
||||||
|
|
||||||
|
EnvIdentityOpenIDEnable = "MINIO_IDENTITY_OPENID_ENABLE"
|
||||||
|
EnvIdentityOpenIDVendor = "MINIO_IDENTITY_OPENID_VENDOR"
|
||||||
|
EnvIdentityOpenIDClientID = "MINIO_IDENTITY_OPENID_CLIENT_ID"
|
||||||
|
EnvIdentityOpenIDClientSecret = "MINIO_IDENTITY_OPENID_CLIENT_SECRET"
|
||||||
|
EnvIdentityOpenIDURL = "MINIO_IDENTITY_OPENID_CONFIG_URL"
|
||||||
|
EnvIdentityOpenIDClaimName = "MINIO_IDENTITY_OPENID_CLAIM_NAME"
|
||||||
|
EnvIdentityOpenIDClaimUserInfo = "MINIO_IDENTITY_OPENID_CLAIM_USERINFO"
|
||||||
|
EnvIdentityOpenIDClaimPrefix = "MINIO_IDENTITY_OPENID_CLAIM_PREFIX"
|
||||||
|
EnvIdentityOpenIDRolePolicy = "MINIO_IDENTITY_OPENID_ROLE_POLICY"
|
||||||
|
EnvIdentityOpenIDRedirectURI = "MINIO_IDENTITY_OPENID_REDIRECT_URI"
|
||||||
|
EnvIdentityOpenIDRedirectURIDynamic = "MINIO_IDENTITY_OPENID_REDIRECT_URI_DYNAMIC"
|
||||||
|
EnvIdentityOpenIDScopes = "MINIO_IDENTITY_OPENID_SCOPES"
|
||||||
|
EnvIdentityOpenIDDisplayName = "MINIO_IDENTITY_OPENID_DISPLAY_NAME"
|
||||||
|
|
||||||
|
// Vendor specific ENVs only enabled if the Vendor matches == "vendor"
|
||||||
|
EnvIdentityOpenIDKeyCloakRealm = "MINIO_IDENTITY_OPENID_KEYCLOAK_REALM"
|
||||||
|
EnvIdentityOpenIDKeyCloakAdminURL = "MINIO_IDENTITY_OPENID_KEYCLOAK_ADMIN_URL"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DefaultKVS - default config for OpenID config
|
||||||
|
var (
|
||||||
|
DefaultKVS = config.KVS{
|
||||||
|
config.KV{
|
||||||
|
Key: config.Enable,
|
||||||
|
Value: "",
|
||||||
|
},
|
||||||
|
config.KV{
|
||||||
|
Key: DisplayName,
|
||||||
|
Value: "",
|
||||||
|
},
|
||||||
|
config.KV{
|
||||||
|
Key: ConfigURL,
|
||||||
|
Value: "",
|
||||||
|
},
|
||||||
|
config.KV{
|
||||||
|
Key: ClientID,
|
||||||
|
Value: "",
|
||||||
|
},
|
||||||
|
config.KV{
|
||||||
|
Key: ClientSecret,
|
||||||
|
Value: "",
|
||||||
|
},
|
||||||
|
config.KV{
|
||||||
|
Key: ClaimName,
|
||||||
|
Value: iampolicy.PolicyName,
|
||||||
|
},
|
||||||
|
config.KV{
|
||||||
|
Key: ClaimUserinfo,
|
||||||
|
Value: "",
|
||||||
|
},
|
||||||
|
config.KV{
|
||||||
|
Key: RolePolicy,
|
||||||
|
Value: "",
|
||||||
|
},
|
||||||
|
config.KV{
|
||||||
|
Key: ClaimPrefix,
|
||||||
|
Value: "",
|
||||||
|
},
|
||||||
|
config.KV{
|
||||||
|
Key: RedirectURI,
|
||||||
|
Value: "",
|
||||||
|
},
|
||||||
|
config.KV{
|
||||||
|
Key: RedirectURIDynamic,
|
||||||
|
Value: "off",
|
||||||
|
},
|
||||||
|
config.KV{
|
||||||
|
Key: Scopes,
|
||||||
|
Value: "",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
var errSingleProvider = config.Errorf("Only one OpenID provider can be configured if not using role policy mapping")
|
||||||
|
|
||||||
|
// DummyRoleARN is used to indicate that the user associated with it was
|
||||||
|
// authenticated via policy-claim based OpenID provider.
|
||||||
|
var DummyRoleARN = func() arn.ARN {
|
||||||
|
v, err := arn.NewIAMRoleARN("dummy-internal", "")
|
||||||
|
if err != nil {
|
||||||
|
panic("should not happen!")
|
||||||
|
}
|
||||||
|
return v
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Config - OpenID Config
|
||||||
|
type Config struct {
|
||||||
|
Enabled bool
|
||||||
|
|
||||||
|
// map of roleARN to providerCfg's
|
||||||
|
arnProviderCfgsMap map[arn.ARN]*providerCfg
|
||||||
|
|
||||||
|
// map of config names to providerCfg's
|
||||||
|
ProviderCfgs map[string]*providerCfg
|
||||||
|
|
||||||
|
pubKeys publicKeys
|
||||||
|
roleArnPolicyMap map[arn.ARN]string
|
||||||
|
|
||||||
|
transport http.RoundTripper
|
||||||
|
closeRespFn func(io.ReadCloser)
|
||||||
|
}
|
||||||
|
|
||||||
|
// LookupConfig lookup jwks from config, override with any ENVs.
|
||||||
|
func LookupConfig(kvsMap map[string]config.KVS, transport http.RoundTripper, closeRespFn func(io.ReadCloser), serverRegion string) (c Config, err error) {
|
||||||
|
openIDClientTransport := http.DefaultTransport
|
||||||
|
if transport != nil {
|
||||||
|
openIDClientTransport = transport
|
||||||
|
}
|
||||||
|
c = Config{
|
||||||
|
Enabled: false,
|
||||||
|
arnProviderCfgsMap: map[arn.ARN]*providerCfg{},
|
||||||
|
ProviderCfgs: map[string]*providerCfg{},
|
||||||
|
pubKeys: publicKeys{
|
||||||
|
RWMutex: &sync.RWMutex{},
|
||||||
|
pkMap: map[string]crypto.PublicKey{},
|
||||||
|
},
|
||||||
|
roleArnPolicyMap: map[arn.ARN]string{},
|
||||||
|
transport: openIDClientTransport,
|
||||||
|
closeRespFn: closeRespFn,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make a copy of the config we received so we can mutate it safely.
|
||||||
|
kvsMap2 := make(map[string]config.KVS, len(kvsMap))
|
||||||
|
for k, v := range kvsMap {
|
||||||
|
kvsMap2[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add in each configuration name found from environment variables, i.e.
|
||||||
|
// if we see MINIO_IDENTITY_OPENID_CONFIG_URL_2, we add the key "2" to
|
||||||
|
// `kvsMap2` if it does not already exist.
|
||||||
|
envs := env.List(EnvIdentityOpenIDURL + config.Default)
|
||||||
|
for _, k := range envs {
|
||||||
|
cfgName := strings.TrimPrefix(k, EnvIdentityOpenIDURL+config.Default)
|
||||||
|
if cfgName == "" {
|
||||||
|
return c, config.Errorf("Environment variable must have a non-empty config name: %s", k)
|
||||||
|
}
|
||||||
|
|
||||||
|
// It is possible that some variables were specified via config
|
||||||
|
// commands and some variables are intended to be overridden
|
||||||
|
// from the environment, so we ensure that the key is not
|
||||||
|
// overwritten in `kvsMap2` as it may have existing config.
|
||||||
|
if _, ok := kvsMap2[cfgName]; !ok {
|
||||||
|
kvsMap2[cfgName] = DefaultKVS
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
hasLegacyPolicyMapping = false
|
||||||
|
seenClientIDs = set.NewStringSet()
|
||||||
|
)
|
||||||
|
for cfgName, kvs := range kvsMap2 {
|
||||||
|
// remove this since we have removed support for this already.
|
||||||
|
kvs.Delete(JwksURL)
|
||||||
|
|
||||||
|
if err = config.CheckValidKeys(config.IdentityOpenIDSubSys, kvs, DefaultKVS); err != nil {
|
||||||
|
return c, err
|
||||||
|
}
|
||||||
|
|
||||||
|
getCfgVal := func(envVar, cfgParam string) string {
|
||||||
|
if cfgName != config.Default {
|
||||||
|
envVar += config.Default + cfgName
|
||||||
|
}
|
||||||
|
return env.Get(envVar, kvs.Get(cfgParam))
|
||||||
|
}
|
||||||
|
|
||||||
|
// In the past, when only one openID provider was allowed, there
|
||||||
|
// was no `enable` parameter - the configuration is turned off
|
||||||
|
// by clearing the values. With multiple providers, we support
|
||||||
|
// individually enabling/disabling provider configurations. If
|
||||||
|
// the enable parameter's value is non-empty, we use that
|
||||||
|
// setting, otherwise we treat it as enabled if some important
|
||||||
|
// parameters are non-empty.
|
||||||
|
var (
|
||||||
|
cfgEnableVal = getCfgVal(EnvIdentityOpenIDEnable, config.Enable)
|
||||||
|
isExplicitlyEnabled = false
|
||||||
|
)
|
||||||
|
if cfgEnableVal != "" {
|
||||||
|
isExplicitlyEnabled = true
|
||||||
|
}
|
||||||
|
|
||||||
|
var enabled bool
|
||||||
|
if isExplicitlyEnabled {
|
||||||
|
enabled, err = config.ParseBool(cfgEnableVal)
|
||||||
|
if err != nil {
|
||||||
|
return c, err
|
||||||
|
}
|
||||||
|
// No need to continue loading if the config is not enabled.
|
||||||
|
if !enabled {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
p := newProviderCfgFromConfig(getCfgVal)
|
||||||
|
configURL := getCfgVal(EnvIdentityOpenIDURL, ConfigURL)
|
||||||
|
|
||||||
|
if !isExplicitlyEnabled {
|
||||||
|
enabled = true
|
||||||
|
if p.ClientID == "" && p.ClientSecret == "" && configURL == "" {
|
||||||
|
enabled = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// No need to continue loading if the config is not enabled.
|
||||||
|
if !enabled {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate that client ID has not been duplicately specified.
|
||||||
|
if seenClientIDs.Contains(p.ClientID) {
|
||||||
|
return c, config.Errorf("Client ID %s is present with multiple OpenID configurations", p.ClientID)
|
||||||
|
}
|
||||||
|
seenClientIDs.Add(p.ClientID)
|
||||||
|
|
||||||
|
p.URL, err = xnet.ParseHTTPURL(configURL)
|
||||||
|
if err != nil {
|
||||||
|
return c, err
|
||||||
|
}
|
||||||
|
configURLDomain := p.URL.Hostname()
|
||||||
|
p.DiscoveryDoc, err = parseDiscoveryDoc(p.URL, transport, closeRespFn)
|
||||||
|
if err != nil {
|
||||||
|
return c, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.ClaimUserinfo && configURL == "" {
|
||||||
|
return c, errors.New("please specify config_url to enable fetching claims from UserInfo endpoint")
|
||||||
|
}
|
||||||
|
|
||||||
|
if scopeList := getCfgVal(EnvIdentityOpenIDScopes, Scopes); scopeList != "" {
|
||||||
|
var scopes []string
|
||||||
|
for _, scope := range strings.Split(scopeList, ",") {
|
||||||
|
scope = strings.TrimSpace(scope)
|
||||||
|
if scope == "" {
|
||||||
|
return c, config.Errorf("empty scope value is not allowed '%s', please refer to our documentation", scopeList)
|
||||||
|
}
|
||||||
|
scopes = append(scopes, scope)
|
||||||
|
}
|
||||||
|
// Replace the discovery document scopes by client customized scopes.
|
||||||
|
p.DiscoveryDoc.ScopesSupported = scopes
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if claim name is the non-default value and role policy is set.
|
||||||
|
if p.ClaimName != iampolicy.PolicyName && p.RolePolicy != "" {
|
||||||
|
// In the unlikely event that the user specifies
|
||||||
|
// `iampolicy.PolicyName` as the claim name explicitly and sets
|
||||||
|
// a role policy, this check is thwarted, but we will be using
|
||||||
|
// the role policy anyway.
|
||||||
|
return c, config.Errorf("Role Policy (=`%s`) and Claim Name (=`%s`) cannot both be set", p.RolePolicy, p.ClaimName)
|
||||||
|
}
|
||||||
|
|
||||||
|
jwksURL := p.DiscoveryDoc.JwksURI
|
||||||
|
if jwksURL == "" {
|
||||||
|
return c, config.Errorf("no JWKS URI found in your provider's discovery doc (config_url=%s)", configURL)
|
||||||
|
}
|
||||||
|
|
||||||
|
p.JWKS.URL, err = xnet.ParseHTTPURL(jwksURL)
|
||||||
|
if err != nil {
|
||||||
|
return c, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.RolePolicy != "" {
|
||||||
|
// RolePolicy is validated by IAM System during its
|
||||||
|
// initialization.
|
||||||
|
|
||||||
|
// Generate role ARN as combination of provider domain and
|
||||||
|
// prefix of client ID.
|
||||||
|
domain := configURLDomain
|
||||||
|
if domain == "" {
|
||||||
|
// Attempt to parse the JWKs URI.
|
||||||
|
domain = p.JWKS.URL.Hostname()
|
||||||
|
if domain == "" {
|
||||||
|
return c, config.Errorf("unable to parse a domain from the OpenID config")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if p.ClientID == "" {
|
||||||
|
return c, config.Errorf("client ID must not be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
// We set the resource ID of the role arn as a hash of client
|
||||||
|
// ID, so we can get a short roleARN that stays the same on
|
||||||
|
// restart.
|
||||||
|
var resourceID string
|
||||||
|
{
|
||||||
|
h := sha1.New()
|
||||||
|
h.Write([]byte(p.ClientID))
|
||||||
|
bs := h.Sum(nil)
|
||||||
|
resourceID = base64.RawURLEncoding.EncodeToString(bs)
|
||||||
|
}
|
||||||
|
p.roleArn, err = arn.NewIAMRoleARN(resourceID, serverRegion)
|
||||||
|
if err != nil {
|
||||||
|
return c, config.Errorf("unable to generate ARN from the OpenID config: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.roleArnPolicyMap[p.roleArn] = p.RolePolicy
|
||||||
|
} else if p.ClaimName == "" {
|
||||||
|
return c, config.Errorf("A role policy or claim name must be specified")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = p.initializeProvider(getCfgVal, c.transport); err != nil {
|
||||||
|
return c, err
|
||||||
|
}
|
||||||
|
|
||||||
|
arnKey := p.roleArn
|
||||||
|
if p.RolePolicy == "" {
|
||||||
|
arnKey = DummyRoleARN
|
||||||
|
hasLegacyPolicyMapping = true
|
||||||
|
// Ensure that when a JWT policy claim based provider
|
||||||
|
// exists, it is the only one.
|
||||||
|
if _, ok := c.arnProviderCfgsMap[DummyRoleARN]; ok {
|
||||||
|
return c, errSingleProvider
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
c.arnProviderCfgsMap[arnKey] = &p
|
||||||
|
c.ProviderCfgs[cfgName] = &p
|
||||||
|
|
||||||
|
if err = c.PopulatePublicKey(arnKey); err != nil {
|
||||||
|
return c, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure that when a JWT policy claim based provider
|
||||||
|
// exists, it is the only one.
|
||||||
|
if hasLegacyPolicyMapping && len(c.ProviderCfgs) > 1 {
|
||||||
|
return c, errSingleProvider
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Enabled = true
|
||||||
|
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enabled returns if configURL is enabled.
|
||||||
|
func Enabled(kvs config.KVS) bool {
|
||||||
|
return kvs.Get(ConfigURL) != ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSettings - fetches OIDC settings for site-replication related validation.
|
||||||
|
// NOTE that region must be populated by caller as this package does not know.
|
||||||
|
func (r *Config) GetSettings() madmin.OpenIDSettings {
|
||||||
|
res := madmin.OpenIDSettings{}
|
||||||
|
if !r.Enabled {
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
for arn, provCfg := range r.arnProviderCfgsMap {
|
||||||
|
hashedSecret := ""
|
||||||
|
{
|
||||||
|
h := sha256.New()
|
||||||
|
h.Write([]byte(provCfg.ClientSecret))
|
||||||
|
bs := h.Sum(nil)
|
||||||
|
hashedSecret = base64.RawURLEncoding.EncodeToString(bs)
|
||||||
|
}
|
||||||
|
if arn != DummyRoleARN {
|
||||||
|
if res.Roles != nil {
|
||||||
|
res.Roles = make(map[string]madmin.OpenIDProviderSettings)
|
||||||
|
}
|
||||||
|
res.Roles[arn.String()] = madmin.OpenIDProviderSettings{
|
||||||
|
ClaimUserinfoEnabled: provCfg.ClaimUserinfo,
|
||||||
|
RolePolicy: provCfg.RolePolicy,
|
||||||
|
ClientID: provCfg.ClientID,
|
||||||
|
HashedClientSecret: hashedSecret,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
res.ClaimProvider = madmin.OpenIDProviderSettings{
|
||||||
|
ClaimUserinfoEnabled: provCfg.ClaimUserinfo,
|
||||||
|
RolePolicy: provCfg.RolePolicy,
|
||||||
|
ClientID: provCfg.ClientID,
|
||||||
|
HashedClientSecret: hashedSecret,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetIAMPolicyClaimName - returns the policy claim name for the (at most one)
|
||||||
|
// provider configured without a role policy.
|
||||||
|
func (r *Config) GetIAMPolicyClaimName() string {
|
||||||
|
pCfg, ok := r.arnProviderCfgsMap[DummyRoleARN]
|
||||||
|
if !ok {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return pCfg.ClaimPrefix + pCfg.ClaimName
|
||||||
|
}
|
||||||
|
|
||||||
|
// LookupUser lookup userid for the provider
|
||||||
|
func (r Config) LookupUser(roleArn, userid string) (provider.User, error) {
|
||||||
|
// Can safely ignore error here as empty or invalid ARNs will not be
|
||||||
|
// mapped.
|
||||||
|
arnVal, _ := arn.Parse(roleArn)
|
||||||
|
pCfg, ok := r.arnProviderCfgsMap[arnVal]
|
||||||
|
if ok {
|
||||||
|
user, err := pCfg.provider.LookupUser(userid)
|
||||||
|
if err != nil && err != provider.ErrAccessTokenExpired {
|
||||||
|
return user, err
|
||||||
|
}
|
||||||
|
if err == provider.ErrAccessTokenExpired {
|
||||||
|
if err = pCfg.provider.LoginWithClientID(pCfg.ClientID, pCfg.ClientSecret); err != nil {
|
||||||
|
return user, err
|
||||||
|
}
|
||||||
|
user, err = pCfg.provider.LookupUser(userid)
|
||||||
|
}
|
||||||
|
return user, err
|
||||||
|
}
|
||||||
|
// Without any specific logic for a provider, all accounts
|
||||||
|
// are always enabled.
|
||||||
|
return provider.User{ID: userid, Enabled: true}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProviderEnabled returns true if any vendor specific provider is enabled.
|
||||||
|
func (r Config) ProviderEnabled() bool {
|
||||||
|
if !r.Enabled {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for _, v := range r.arnProviderCfgsMap {
|
||||||
|
if v.provider != nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetRoleInfo - returns ARN to policies map if a role policy based openID
|
||||||
|
// provider is configured. Otherwise returns nil.
|
||||||
|
func (r Config) GetRoleInfo() map[arn.ARN]string {
|
||||||
|
for _, p := range r.arnProviderCfgsMap {
|
||||||
|
if p.RolePolicy != "" {
|
||||||
|
return r.roleArnPolicyMap
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDefaultExpiration - returns the expiration seconds expected.
|
||||||
|
func GetDefaultExpiration(dsecs string) (time.Duration, error) {
|
||||||
|
defaultExpiryDuration := time.Duration(60) * time.Minute // Defaults to 1hr.
|
||||||
|
if dsecs != "" {
|
||||||
|
expirySecs, err := strconv.ParseInt(dsecs, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return 0, auth.ErrInvalidDuration
|
||||||
|
}
|
||||||
|
|
||||||
|
// The duration, in seconds, of the role session.
|
||||||
|
// The value can range from 900 seconds (15 minutes)
|
||||||
|
// up to 365 days.
|
||||||
|
if expirySecs < 900 || expirySecs > 31536000 {
|
||||||
|
return 0, auth.ErrInvalidDuration
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultExpiryDuration = time.Duration(expirySecs) * time.Second
|
||||||
|
}
|
||||||
|
return defaultExpiryDuration, nil
|
||||||
|
}
|
148
internal/config/identity/openid/providercfg.go
Normal file
148
internal/config/identity/openid/providercfg.go
Normal file
@ -0,0 +1,148 @@
|
|||||||
|
// Copyright (c) 2015-2022 MinIO, Inc.
|
||||||
|
//
|
||||||
|
// This file is part of MinIO Object Storage stack
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package openid
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/minio/minio/internal/arn"
|
||||||
|
"github.com/minio/minio/internal/config"
|
||||||
|
"github.com/minio/minio/internal/config/identity/openid/provider"
|
||||||
|
xhttp "github.com/minio/minio/internal/http"
|
||||||
|
xnet "github.com/minio/pkg/net"
|
||||||
|
)
|
||||||
|
|
||||||
|
type providerCfg struct {
|
||||||
|
// Used for user interface like console
|
||||||
|
DisplayName string
|
||||||
|
|
||||||
|
JWKS struct {
|
||||||
|
URL *xnet.URL
|
||||||
|
}
|
||||||
|
URL *xnet.URL
|
||||||
|
ClaimPrefix string
|
||||||
|
ClaimName string
|
||||||
|
ClaimUserinfo bool
|
||||||
|
RedirectURI string
|
||||||
|
RedirectURIDynamic bool
|
||||||
|
DiscoveryDoc DiscoveryDoc
|
||||||
|
ClientID string
|
||||||
|
ClientSecret string
|
||||||
|
RolePolicy string
|
||||||
|
|
||||||
|
roleArn arn.ARN
|
||||||
|
provider provider.Provider
|
||||||
|
}
|
||||||
|
|
||||||
|
func newProviderCfgFromConfig(getCfgVal func(env, cfgName string) string) providerCfg {
|
||||||
|
return providerCfg{
|
||||||
|
DisplayName: getCfgVal(EnvIdentityOpenIDDisplayName, DisplayName),
|
||||||
|
ClaimName: getCfgVal(EnvIdentityOpenIDClaimName, ClaimName),
|
||||||
|
ClaimUserinfo: getCfgVal(EnvIdentityOpenIDClaimUserInfo, ClaimUserinfo) == config.EnableOn,
|
||||||
|
ClaimPrefix: getCfgVal(EnvIdentityOpenIDClaimPrefix, ClaimPrefix),
|
||||||
|
RedirectURI: getCfgVal(EnvIdentityOpenIDRedirectURI, RedirectURI),
|
||||||
|
RedirectURIDynamic: getCfgVal(EnvIdentityOpenIDRedirectURIDynamic, RedirectURIDynamic) == config.EnableOn,
|
||||||
|
ClientID: getCfgVal(EnvIdentityOpenIDClientID, ClientID),
|
||||||
|
ClientSecret: getCfgVal(EnvIdentityOpenIDClientSecret, ClientSecret),
|
||||||
|
RolePolicy: getCfgVal(EnvIdentityOpenIDRolePolicy, RolePolicy),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
keyCloakVendor = "keycloak"
|
||||||
|
)
|
||||||
|
|
||||||
|
// initializeProvider initializes if any additional vendor specific information
|
||||||
|
// was provided, initialization will return an error initial login fails.
|
||||||
|
func (p *providerCfg) initializeProvider(cfgGet func(string, string) string, transport http.RoundTripper) error {
|
||||||
|
vendor := cfgGet(EnvIdentityOpenIDVendor, Vendor)
|
||||||
|
if vendor == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var err error
|
||||||
|
switch vendor {
|
||||||
|
case keyCloakVendor:
|
||||||
|
adminURL := cfgGet(EnvIdentityOpenIDKeyCloakAdminURL, KeyCloakAdminURL)
|
||||||
|
realm := cfgGet(EnvIdentityOpenIDKeyCloakRealm, KeyCloakRealm)
|
||||||
|
p.provider, err = provider.KeyCloak(
|
||||||
|
provider.WithAdminURL(adminURL),
|
||||||
|
provider.WithOpenIDConfig(provider.DiscoveryDoc(p.DiscoveryDoc)),
|
||||||
|
provider.WithTransport(transport),
|
||||||
|
provider.WithRealm(realm),
|
||||||
|
)
|
||||||
|
return err
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("Unsupport vendor %s", keyCloakVendor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserInfo returns claims for authenticated user from userInfo endpoint.
|
||||||
|
//
|
||||||
|
// Some OIDC implementations such as GitLab do not support
|
||||||
|
// claims as part of the normal oauth2 flow, instead rely
|
||||||
|
// on service providers making calls to IDP to fetch additional
|
||||||
|
// claims available from the UserInfo endpoint
|
||||||
|
func (p *providerCfg) UserInfo(accessToken string, transport http.RoundTripper) (map[string]interface{}, error) {
|
||||||
|
if p.JWKS.URL == nil || p.JWKS.URL.String() == "" {
|
||||||
|
return nil, errors.New("openid not configured")
|
||||||
|
}
|
||||||
|
client := &http.Client{
|
||||||
|
Transport: transport,
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := http.NewRequest(http.MethodPost, p.DiscoveryDoc.UserInfoEndpoint, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if accessToken != "" {
|
||||||
|
req.Header.Set("Authorization", "Bearer "+accessToken)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer xhttp.DrainBody(resp.Body)
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
// uncomment this for debugging when needed.
|
||||||
|
// reqBytes, _ := httputil.DumpRequest(req, false)
|
||||||
|
// fmt.Println(string(reqBytes))
|
||||||
|
// respBytes, _ := httputil.DumpResponse(resp, true)
|
||||||
|
// fmt.Println(string(respBytes))
|
||||||
|
return nil, errors.New(resp.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
dec := json.NewDecoder(resp.Body)
|
||||||
|
claims := map[string]interface{}{}
|
||||||
|
|
||||||
|
if err = dec.Decode(&claims); err != nil {
|
||||||
|
// uncomment this for debugging when needed.
|
||||||
|
// reqBytes, _ := httputil.DumpRequest(req, false)
|
||||||
|
// fmt.Println(string(reqBytes))
|
||||||
|
// respBytes, _ := httputil.DumpResponse(resp, true)
|
||||||
|
// fmt.Println(string(respBytes))
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return claims, nil
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user