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 }