github.com/minio/minio@v0.0.0-20240328213742-3f72439b8a27/internal/config/identity/openid/providercfg.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  	"net/http"
    26  
    27  	"github.com/minio/minio/internal/arn"
    28  	"github.com/minio/minio/internal/config"
    29  	"github.com/minio/minio/internal/config/identity/openid/provider"
    30  	xhttp "github.com/minio/minio/internal/http"
    31  	xnet "github.com/minio/pkg/v2/net"
    32  )
    33  
    34  type providerCfg struct {
    35  	// Used for user interface like console
    36  	DisplayName string
    37  
    38  	JWKS struct {
    39  		URL *xnet.URL
    40  	}
    41  	URL                *xnet.URL
    42  	ClaimPrefix        string
    43  	ClaimName          string
    44  	ClaimUserinfo      bool
    45  	RedirectURI        string
    46  	RedirectURIDynamic bool
    47  	DiscoveryDoc       DiscoveryDoc
    48  	ClientID           string
    49  	ClientSecret       string
    50  	RolePolicy         string
    51  
    52  	roleArn  arn.ARN
    53  	provider provider.Provider
    54  }
    55  
    56  func newProviderCfgFromConfig(getCfgVal func(cfgName string) string) providerCfg {
    57  	return providerCfg{
    58  		DisplayName:        getCfgVal(DisplayName),
    59  		ClaimName:          getCfgVal(ClaimName),
    60  		ClaimUserinfo:      getCfgVal(ClaimUserinfo) == config.EnableOn,
    61  		ClaimPrefix:        getCfgVal(ClaimPrefix),
    62  		RedirectURI:        getCfgVal(RedirectURI),
    63  		RedirectURIDynamic: getCfgVal(RedirectURIDynamic) == config.EnableOn,
    64  		ClientID:           getCfgVal(ClientID),
    65  		ClientSecret:       getCfgVal(ClientSecret),
    66  		RolePolicy:         getCfgVal(RolePolicy),
    67  	}
    68  }
    69  
    70  const (
    71  	keyCloakVendor = "keycloak"
    72  )
    73  
    74  // initializeProvider initializes if any additional vendor specific information
    75  // was provided, initialization will return an error initial login fails.
    76  func (p *providerCfg) initializeProvider(cfgGet func(string) string, transport http.RoundTripper) error {
    77  	vendor := cfgGet(Vendor)
    78  	if vendor == "" {
    79  		return nil
    80  	}
    81  	var err error
    82  	switch vendor {
    83  	case keyCloakVendor:
    84  		adminURL := cfgGet(KeyCloakAdminURL)
    85  		realm := cfgGet(KeyCloakRealm)
    86  		p.provider, err = provider.KeyCloak(
    87  			provider.WithAdminURL(adminURL),
    88  			provider.WithOpenIDConfig(provider.DiscoveryDoc(p.DiscoveryDoc)),
    89  			provider.WithTransport(transport),
    90  			provider.WithRealm(realm),
    91  		)
    92  		return err
    93  	default:
    94  		return fmt.Errorf("Unsupported vendor %s", keyCloakVendor)
    95  	}
    96  }
    97  
    98  // GetRoleArn returns the role ARN.
    99  func (p *providerCfg) GetRoleArn() string {
   100  	if p.RolePolicy == "" {
   101  		return ""
   102  	}
   103  	return p.roleArn.String()
   104  }
   105  
   106  // UserInfo returns claims for authenticated user from userInfo endpoint.
   107  //
   108  // Some OIDC implementations such as GitLab do not support
   109  // claims as part of the normal oauth2 flow, instead rely
   110  // on service providers making calls to IDP to fetch additional
   111  // claims available from the UserInfo endpoint
   112  func (p *providerCfg) UserInfo(ctx context.Context, accessToken string, transport http.RoundTripper) (map[string]interface{}, error) {
   113  	if p.JWKS.URL == nil || p.JWKS.URL.String() == "" {
   114  		return nil, errors.New("openid not configured")
   115  	}
   116  
   117  	req, err := http.NewRequestWithContext(ctx, http.MethodPost, p.DiscoveryDoc.UserInfoEndpoint, nil)
   118  	if err != nil {
   119  		return nil, err
   120  	}
   121  
   122  	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
   123  	if accessToken != "" {
   124  		req.Header.Set("Authorization", "Bearer "+accessToken)
   125  	}
   126  
   127  	client := &http.Client{
   128  		Transport: transport,
   129  	}
   130  
   131  	resp, err := client.Do(req)
   132  	if err != nil {
   133  		return nil, err
   134  	}
   135  
   136  	defer xhttp.DrainBody(resp.Body)
   137  	if resp.StatusCode != http.StatusOK {
   138  		// uncomment this for debugging when needed.
   139  		// reqBytes, _ := httputil.DumpRequest(req, false)
   140  		// fmt.Println(string(reqBytes))
   141  		// respBytes, _ := httputil.DumpResponse(resp, true)
   142  		// fmt.Println(string(respBytes))
   143  		return nil, errors.New(resp.Status)
   144  	}
   145  
   146  	claims := map[string]interface{}{}
   147  	if err = json.NewDecoder(resp.Body).Decode(&claims); err != nil {
   148  		// uncomment this for debugging when needed.
   149  		// reqBytes, _ := httputil.DumpRequest(req, false)
   150  		// fmt.Println(string(reqBytes))
   151  		// respBytes, _ := httputil.DumpResponse(resp, true)
   152  		// fmt.Println(string(respBytes))
   153  		return nil, err
   154  	}
   155  
   156  	return claims, nil
   157  }