github.com/minio/minio@v0.0.0-20240328213742-3f72439b8a27/internal/config/identity/openid/provider/keycloak.go (about) 1 // Copyright (c) 2015-2021 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 provider 19 20 import ( 21 "encoding/json" 22 "errors" 23 "fmt" 24 "net/http" 25 "net/url" 26 "path" 27 "strings" 28 "sync" 29 ) 30 31 // Token - parses the output from IDP id_token. 32 type Token struct { 33 AccessToken string `json:"access_token"` 34 Expiry int `json:"expires_in"` 35 } 36 37 // KeycloakProvider implements Provider interface for KeyCloak Identity Provider. 38 type KeycloakProvider struct { 39 sync.Mutex 40 41 oeConfig DiscoveryDoc 42 client http.Client 43 adminURL string 44 realm string 45 46 // internal value refreshed 47 accessToken Token 48 } 49 50 // LoginWithUser authenticates username/password, not needed for Keycloak 51 func (k *KeycloakProvider) LoginWithUser(username, password string) error { 52 return ErrNotImplemented 53 } 54 55 // LoginWithClientID is implemented by Keycloak service account support 56 func (k *KeycloakProvider) LoginWithClientID(clientID, clientSecret string) error { 57 values := url.Values{} 58 values.Set("client_id", clientID) 59 values.Set("client_secret", clientSecret) 60 values.Set("grant_type", "client_credentials") 61 62 req, err := http.NewRequest(http.MethodPost, k.oeConfig.TokenEndpoint, strings.NewReader(values.Encode())) 63 if err != nil { 64 return err 65 } 66 req.Header.Set("Content-Type", "application/x-www-form-urlencoded") 67 68 resp, err := k.client.Do(req) 69 if err != nil { 70 return err 71 } 72 defer resp.Body.Close() 73 74 var accessToken Token 75 if err = json.NewDecoder(resp.Body).Decode(&accessToken); err != nil { 76 return err 77 } 78 79 k.Lock() 80 k.accessToken = accessToken 81 k.Unlock() 82 return nil 83 } 84 85 // LookupUser lookup user by their userid. 86 func (k *KeycloakProvider) LookupUser(userid string) (User, error) { 87 req, err := http.NewRequest(http.MethodGet, k.adminURL, nil) 88 if err != nil { 89 return User{}, err 90 } 91 req.URL.Path = path.Join(req.URL.Path, "realms", k.realm, "users", userid) 92 93 k.Lock() 94 accessToken := k.accessToken 95 k.Unlock() 96 if accessToken.AccessToken == "" { 97 return User{}, ErrAccessTokenExpired 98 } 99 req.Header.Set("Authorization", "Bearer "+accessToken.AccessToken) 100 resp, err := k.client.Do(req) 101 if err != nil { 102 return User{}, err 103 } 104 defer resp.Body.Close() 105 switch resp.StatusCode { 106 case http.StatusOK, http.StatusPartialContent: 107 var u User 108 if err = json.NewDecoder(resp.Body).Decode(&u); err != nil { 109 return User{}, err 110 } 111 return u, nil 112 case http.StatusNotFound: 113 return User{ 114 ID: userid, 115 Enabled: false, 116 }, nil 117 case http.StatusUnauthorized: 118 return User{}, ErrAccessTokenExpired 119 } 120 return User{}, fmt.Errorf("Unable to lookup %s - keycloak user lookup returned %v", userid, resp.Status) 121 } 122 123 // Option is a function type that accepts a pointer Target 124 type Option func(*KeycloakProvider) 125 126 // WithTransport provide custom transport 127 func WithTransport(transport http.RoundTripper) Option { 128 return func(p *KeycloakProvider) { 129 p.client = http.Client{ 130 Transport: transport, 131 } 132 } 133 } 134 135 // WithOpenIDConfig provide OpenID Endpoint configuration discovery document 136 func WithOpenIDConfig(oeConfig DiscoveryDoc) Option { 137 return func(p *KeycloakProvider) { 138 p.oeConfig = oeConfig 139 } 140 } 141 142 // WithAdminURL provide admin URL configuration for Keycloak 143 func WithAdminURL(url string) Option { 144 return func(p *KeycloakProvider) { 145 p.adminURL = url 146 } 147 } 148 149 // WithRealm provide realm configuration for Keycloak 150 func WithRealm(realm string) Option { 151 return func(p *KeycloakProvider) { 152 p.realm = realm 153 } 154 } 155 156 // KeyCloak initializes a new keycloak provider 157 func KeyCloak(opts ...Option) (Provider, error) { 158 p := &KeycloakProvider{} 159 160 for _, opt := range opts { 161 opt(p) 162 } 163 164 if p.adminURL == "" { 165 return nil, errors.New("Admin URL cannot be empty") 166 } 167 168 _, err := url.Parse(p.adminURL) 169 if err != nil { 170 return nil, fmt.Errorf("Unable to parse the adminURL %s: %w", p.adminURL, err) 171 } 172 173 if p.client.Transport == nil { 174 p.client.Transport = http.DefaultTransport 175 } 176 177 if p.oeConfig.TokenEndpoint == "" { 178 return nil, errors.New("missing OpenID token endpoint") 179 } 180 181 if p.realm == "" { 182 p.realm = "master" // default realm 183 } 184 185 return p, nil 186 }