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  }