|
|
|
|
@@ -47,13 +47,14 @@ type Config struct {
|
|
|
|
|
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"`
|
|
|
|
|
RedirectURI string `json:"redirectURI,omitempty"`
|
|
|
|
|
DiscoveryDoc DiscoveryDoc
|
|
|
|
|
ClientID string
|
|
|
|
|
ClientSecret string
|
|
|
|
|
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"`
|
|
|
|
|
DiscoveryDoc DiscoveryDoc
|
|
|
|
|
ClientID string
|
|
|
|
|
ClientSecret string
|
|
|
|
|
|
|
|
|
|
provider provider.Provider
|
|
|
|
|
publicKeys map[string]crypto.PublicKey
|
|
|
|
|
@@ -61,6 +62,63 @@ type Config struct {
|
|
|
|
|
closeRespFn func(io.ReadCloser)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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 (r *Config) UserInfo(accessToken string) (map[string]interface{}, error) {
|
|
|
|
|
if r.JWKS.URL == nil || r.JWKS.URL.String() == "" {
|
|
|
|
|
return nil, errors.New("openid not configured")
|
|
|
|
|
}
|
|
|
|
|
transport := http.DefaultTransport
|
|
|
|
|
if r.transport != nil {
|
|
|
|
|
transport = r.transport
|
|
|
|
|
}
|
|
|
|
|
client := &http.Client{
|
|
|
|
|
Transport: transport,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
req, err := http.NewRequest(http.MethodPost, r.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 r.closeRespFn(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)
|
|
|
|
|
var 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
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// LookupUser lookup userid for the provider
|
|
|
|
|
func (r Config) LookupUser(userid string) (provider.User, error) {
|
|
|
|
|
if r.provider != nil {
|
|
|
|
|
@@ -122,9 +180,6 @@ func (r *Config) InitializeKeycloakProvider(adminURL, realm string) error {
|
|
|
|
|
|
|
|
|
|
// PopulatePublicKey - populates a new publickey from the JWKS URL.
|
|
|
|
|
func (r *Config) PopulatePublicKey() error {
|
|
|
|
|
r.Lock()
|
|
|
|
|
defer r.Unlock()
|
|
|
|
|
|
|
|
|
|
if r.JWKS.URL == nil || r.JWKS.URL.String() == "" {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
@@ -135,6 +190,10 @@ func (r *Config) PopulatePublicKey() error {
|
|
|
|
|
client := &http.Client{
|
|
|
|
|
Transport: transport,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
r.Lock()
|
|
|
|
|
defer r.Unlock()
|
|
|
|
|
|
|
|
|
|
resp, err := client.Get(r.JWKS.URL.String())
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
@@ -234,7 +293,7 @@ func updateClaimsExpiry(dsecs string, claims map[string]interface{}) error {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Validate - validates the id_token.
|
|
|
|
|
func (r *Config) Validate(token, dsecs string) (map[string]interface{}, error) {
|
|
|
|
|
func (r *Config) Validate(token, accessToken, dsecs string) (map[string]interface{}, error) {
|
|
|
|
|
jp := new(jwtgo.Parser)
|
|
|
|
|
jp.ValidMethods = []string{
|
|
|
|
|
"RS256", "RS384", "RS512", "ES256", "ES384", "ES512",
|
|
|
|
|
@@ -273,6 +332,23 @@ func (r *Config) Validate(token, dsecs string) (map[string]interface{}, error) {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// If claim user info is enabled, get claims from userInfo
|
|
|
|
|
// and overwrite them with the claims from JWT.
|
|
|
|
|
if r.ClaimUserinfo {
|
|
|
|
|
if accessToken == "" {
|
|
|
|
|
return nil, errors.New("access_token is mandatory if user_info claim is enabled")
|
|
|
|
|
}
|
|
|
|
|
uclaims, err := r.UserInfo(accessToken)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
for k, v := range uclaims {
|
|
|
|
|
if _, ok := claims[k]; !ok { // only add to claims not update it.
|
|
|
|
|
claims[k] = v
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return claims, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@@ -283,29 +359,32 @@ func (Config) ID() ID {
|
|
|
|
|
|
|
|
|
|
// OpenID keys and envs.
|
|
|
|
|
const (
|
|
|
|
|
JwksURL = "jwks_url"
|
|
|
|
|
ConfigURL = "config_url"
|
|
|
|
|
ClaimName = "claim_name"
|
|
|
|
|
ClaimPrefix = "claim_prefix"
|
|
|
|
|
ClientID = "client_id"
|
|
|
|
|
ClientSecret = "client_secret"
|
|
|
|
|
Vendor = "vendor"
|
|
|
|
|
Scopes = "scopes"
|
|
|
|
|
RedirectURI = "redirect_uri"
|
|
|
|
|
JwksURL = "jwks_url"
|
|
|
|
|
ConfigURL = "config_url"
|
|
|
|
|
ClaimName = "claim_name"
|
|
|
|
|
ClaimUserinfo = "claim_userinfo"
|
|
|
|
|
ClaimPrefix = "claim_prefix"
|
|
|
|
|
ClientID = "client_id"
|
|
|
|
|
ClientSecret = "client_secret"
|
|
|
|
|
|
|
|
|
|
Vendor = "vendor"
|
|
|
|
|
Scopes = "scopes"
|
|
|
|
|
RedirectURI = "redirect_uri"
|
|
|
|
|
|
|
|
|
|
// Vendor specific ENV only enabled if the Vendor matches == "vendor"
|
|
|
|
|
KeyCloakRealm = "keycloak_realm"
|
|
|
|
|
KeyCloakAdminURL = "keycloak_admin_url"
|
|
|
|
|
|
|
|
|
|
EnvIdentityOpenIDVendor = "MINIO_IDENTITY_OPENID_VENDOR"
|
|
|
|
|
EnvIdentityOpenIDClientID = "MINIO_IDENTITY_OPENID_CLIENT_ID"
|
|
|
|
|
EnvIdentityOpenIDClientSecret = "MINIO_IDENTITY_OPENID_CLIENT_SECRET"
|
|
|
|
|
EnvIdentityOpenIDJWKSURL = "MINIO_IDENTITY_OPENID_JWKS_URL"
|
|
|
|
|
EnvIdentityOpenIDURL = "MINIO_IDENTITY_OPENID_CONFIG_URL"
|
|
|
|
|
EnvIdentityOpenIDClaimName = "MINIO_IDENTITY_OPENID_CLAIM_NAME"
|
|
|
|
|
EnvIdentityOpenIDClaimPrefix = "MINIO_IDENTITY_OPENID_CLAIM_PREFIX"
|
|
|
|
|
EnvIdentityOpenIDRedirectURI = "MINIO_IDENTITY_OPENID_REDIRECT_URI"
|
|
|
|
|
EnvIdentityOpenIDScopes = "MINIO_IDENTITY_OPENID_SCOPES"
|
|
|
|
|
EnvIdentityOpenIDVendor = "MINIO_IDENTITY_OPENID_VENDOR"
|
|
|
|
|
EnvIdentityOpenIDClientID = "MINIO_IDENTITY_OPENID_CLIENT_ID"
|
|
|
|
|
EnvIdentityOpenIDClientSecret = "MINIO_IDENTITY_OPENID_CLIENT_SECRET"
|
|
|
|
|
EnvIdentityOpenIDJWKSURL = "MINIO_IDENTITY_OPENID_JWKS_URL"
|
|
|
|
|
EnvIdentityOpenIDURL = "MINIO_IDENTITY_OPENID_CONFIG_URL"
|
|
|
|
|
EnvIdentityOpenIDClaimName = "MINIO_IDENTITY_OPENID_CLAIM_NAME"
|
|
|
|
|
EnvIdentityOpenIDClaimUserInfo = "MINIO_IDENTITY_OPENID_CLAIM_USERINFO"
|
|
|
|
|
EnvIdentityOpenIDClaimPrefix = "MINIO_IDENTITY_OPENID_CLAIM_PREFIX"
|
|
|
|
|
EnvIdentityOpenIDRedirectURI = "MINIO_IDENTITY_OPENID_REDIRECT_URI"
|
|
|
|
|
EnvIdentityOpenIDScopes = "MINIO_IDENTITY_OPENID_SCOPES"
|
|
|
|
|
|
|
|
|
|
// Vendor specific ENVs only enabled if the Vendor matches == "vendor"
|
|
|
|
|
EnvIdentityOpenIDKeyCloakRealm = "MINIO_IDENTITY_OPENID_KEYCLOAK_REALM"
|
|
|
|
|
@@ -374,6 +453,10 @@ var (
|
|
|
|
|
Key: ClaimName,
|
|
|
|
|
Value: iampolicy.PolicyName,
|
|
|
|
|
},
|
|
|
|
|
config.KV{
|
|
|
|
|
Key: ClaimUserinfo,
|
|
|
|
|
Value: "",
|
|
|
|
|
},
|
|
|
|
|
config.KV{
|
|
|
|
|
Key: ClaimPrefix,
|
|
|
|
|
Value: "",
|
|
|
|
|
@@ -410,15 +493,16 @@ func LookupConfig(kvs config.KVS, transport *http.Transport, closeRespFn func(io
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
c = Config{
|
|
|
|
|
RWMutex: &sync.RWMutex{},
|
|
|
|
|
ClaimName: env.Get(EnvIdentityOpenIDClaimName, kvs.Get(ClaimName)),
|
|
|
|
|
ClaimPrefix: env.Get(EnvIdentityOpenIDClaimPrefix, kvs.Get(ClaimPrefix)),
|
|
|
|
|
RedirectURI: env.Get(EnvIdentityOpenIDRedirectURI, kvs.Get(RedirectURI)),
|
|
|
|
|
publicKeys: make(map[string]crypto.PublicKey),
|
|
|
|
|
ClientID: env.Get(EnvIdentityOpenIDClientID, kvs.Get(ClientID)),
|
|
|
|
|
ClientSecret: env.Get(EnvIdentityOpenIDClientSecret, kvs.Get(ClientSecret)),
|
|
|
|
|
transport: transport,
|
|
|
|
|
closeRespFn: closeRespFn,
|
|
|
|
|
RWMutex: &sync.RWMutex{},
|
|
|
|
|
ClaimName: env.Get(EnvIdentityOpenIDClaimName, kvs.Get(ClaimName)),
|
|
|
|
|
ClaimUserinfo: env.Get(EnvIdentityOpenIDClaimUserInfo, kvs.Get(ClaimUserinfo)) == config.EnableOn,
|
|
|
|
|
ClaimPrefix: env.Get(EnvIdentityOpenIDClaimPrefix, kvs.Get(ClaimPrefix)),
|
|
|
|
|
RedirectURI: env.Get(EnvIdentityOpenIDRedirectURI, kvs.Get(RedirectURI)),
|
|
|
|
|
publicKeys: make(map[string]crypto.PublicKey),
|
|
|
|
|
ClientID: env.Get(EnvIdentityOpenIDClientID, kvs.Get(ClientID)),
|
|
|
|
|
ClientSecret: env.Get(EnvIdentityOpenIDClientSecret, kvs.Get(ClientSecret)),
|
|
|
|
|
transport: transport,
|
|
|
|
|
closeRespFn: closeRespFn,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
configURL := env.Get(EnvIdentityOpenIDURL, kvs.Get(ConfigURL))
|
|
|
|
|
@@ -433,6 +517,10 @@ func LookupConfig(kvs config.KVS, transport *http.Transport, closeRespFn func(io
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if c.ClaimUserinfo && configURL == "" {
|
|
|
|
|
return c, errors.New("please specify config_url to enable fetching claims from UserInfo endpoint")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if scopeList := env.Get(EnvIdentityOpenIDScopes, kvs.Get(Scopes)); scopeList != "" {
|
|
|
|
|
var scopes []string
|
|
|
|
|
for _, scope := range strings.Split(scopeList, ",") {
|
|
|
|
|
|