github.com/brioux/go-keycloak@v0.0.0-20240929191119-b54a3a01d90b/keycloak.go (about)

     1  package keycloak
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"encoding/json"
     7  	"fmt"
     8  	"io"
     9  	"io/ioutil"
    10  	"net/http"
    11  	"net/url"
    12  	"strings"
    13  
    14  	"github.com/google/go-querystring/query"
    15  )
    16  
    17  const (
    18  	defaultAdminBase = "admin/realms"
    19  	defaultBase      = "realms"
    20  
    21  	formEncoded   = "application/x-www-form-urlencoded"
    22  	passwordGrant = "password"
    23  	clientGrant   = "client_credentials"
    24  	offlineScope  = "offline_access"
    25  )
    26  
    27  // Response is the Keycloak response.
    28  type Response struct {
    29  	Response *http.Response
    30  }
    31  
    32  // ErrorResponse returns the error response from Keycloak
    33  type ErrorResponse struct {
    34  	Response *http.Response
    35  	Message  string `json:"error_description"`
    36  }
    37  
    38  func (r *ErrorResponse) Error() string {
    39  	return fmt.Sprintf("%v %v: %d %v",
    40  		r.Response.Request.Method, r.Response.Request.URL,
    41  		r.Response.StatusCode, r.Message)
    42  }
    43  
    44  // Client manages communication to Keycloak
    45  type Client struct {
    46  	common     service      // Reuse struct
    47  	httpClient *http.Client // HTTP client to communicate with keycloak
    48  
    49  	// Keycloak Client Configuration
    50  	baseURL *url.URL
    51  	realm   string
    52  
    53  	hasOfflineAccess bool
    54  	isServiceAccount bool
    55  	isConfidential   bool
    56  
    57  	clientID     string
    58  	clientSecret string
    59  
    60  	adminAccount string
    61  	adminPass    string
    62  
    63  	// Services
    64  	Authentication *AuthenticationService
    65  	AdminUser      *AdminUserService
    66  	UMA            *UMAService
    67  
    68  	adminOIDC *OIDCToken
    69  }
    70  
    71  type service struct {
    72  	client *Client
    73  }
    74  
    75  type headers struct {
    76  	authorization string
    77  	contentType   string
    78  }
    79  
    80  // NewServiceAccount is targeted at Service Accounts with elevated privileges
    81  func NewServiceAccount(
    82  	httpClient *http.Client,
    83  
    84  	baseURL string,
    85  	realm string,
    86  	hasOfflineAccess bool,
    87  
    88  	clientID string,
    89  	clientSecret string,
    90  ) *Client {
    91  	return newClient(httpClient, baseURL, realm, hasOfflineAccess, true, true, clientID, clientSecret, "", "")
    92  }
    93  
    94  // NewConfidentialAdmin is targeted at users with elevated privileges
    95  // who will be using a confidential client to authenticate against.
    96  func NewConfidentialAdmin(
    97  	httpClient *http.Client,
    98  
    99  	baseURL string,
   100  	realm string,
   101  	hasOfflineAccess bool,
   102  
   103  	clientID string,
   104  	clientSecret string,
   105  
   106  	adminAccount string,
   107  	adminPass string,
   108  ) *Client {
   109  	return newClient(httpClient, baseURL, realm, hasOfflineAccess, false, true, clientID, clientSecret, adminAccount, adminPass)
   110  }
   111  
   112  // NewPublicAdmin is targeted at users with elevated privileges who will
   113  // be using a public client to authenticate against.
   114  func NewPublicAdmin(
   115  	httpClient *http.Client,
   116  
   117  	baseURL string,
   118  	realm string,
   119  	hasOfflineAccess bool,
   120  
   121  	clientID string,
   122  
   123  	adminAccount string,
   124  	adminPass string,
   125  ) *Client {
   126  	return newClient(httpClient, baseURL, realm, hasOfflineAccess, false, false, clientID, "", adminAccount, adminPass)
   127  }
   128  
   129  // newClient returns a new Keycloak consumer. If no httpClient is provided
   130  // the default httpClient will be used.
   131  func newClient(
   132  	httpClient *http.Client,
   133  
   134  	baseURL string,
   135  	realm string,
   136  
   137  	// Requires offline_access role
   138  	hasOfflineAccess bool,
   139  	// Requires confidential access type and service accounts enabled
   140  	isServiceAccount bool,
   141  	// Requires client secret when making protected requests
   142  	isConfidential bool,
   143  
   144  	// If using service accounts
   145  	clientID string,
   146  	clientSecret string,
   147  
   148  	// If using an admin account
   149  	adminAccount string,
   150  	adminPass string,
   151  ) *Client {
   152  
   153  	if httpClient == nil {
   154  		httpClient = http.DefaultClient
   155  	}
   156  
   157  	base, _ := url.Parse(baseURL)
   158  
   159  	c := &Client{
   160  		httpClient: httpClient,
   161  		baseURL:    base,
   162  		realm:      realm,
   163  
   164  		hasOfflineAccess: hasOfflineAccess,
   165  		isServiceAccount: isServiceAccount,
   166  		isConfidential:   isConfidential,
   167  
   168  		clientID:     clientID,
   169  		clientSecret: clientSecret,
   170  
   171  		adminAccount: adminAccount,
   172  		adminPass:    adminPass,
   173  		adminOIDC:    &OIDCToken{},
   174  	}
   175  
   176  	c.common.client = c
   177  	c.Authentication = (*AuthenticationService)(&c.common)
   178  	c.AdminUser = (*AdminUserService)(&c.common)
   179  	c.UMA = (*UMAService)(&c.common)
   180  
   181  	return c
   182  }
   183  
   184  // BaseURL returns the baseURL value
   185  func (c Client) BaseURL() string { return c.baseURL.String() }
   186  
   187  // Realm returns the realm value
   188  func (c Client) Realm() string { return c.realm }
   189  
   190  // ClientID returns the clientID value
   191  func (c Client) ClientID() string { return c.clientID }
   192  
   193  // ClientSecret returns the clientSecret value
   194  func (c Client) ClientSecret() string { return c.clientSecret }
   195  
   196  // AdminAccount returns the adminAccount value
   197  func (c Client) AdminAccount() string { return c.adminAccount }
   198  
   199  // AdminPass returns the adminPass value
   200  func (c Client) AdminPass() string { return c.adminPass }
   201  
   202  // AdminOIDC returns the admin access token
   203  func (c Client) AdminOIDC() *OIDCToken { return c.adminOIDC }
   204  
   205  // newRequest creates the keycloak request with a relative URL provided.
   206  func (c *Client) newRequest(
   207  	method,
   208  	path string,
   209  	body interface{},
   210  	h headers,
   211  	isAdminRequest bool,
   212  ) (*http.Request, error) {
   213  	u, err := c.baseURL.Parse(path)
   214  	if err != nil {
   215  		return nil, err
   216  	}
   217  
   218  	var req *http.Request
   219  	if h.contentType == formEncoded && body != nil {
   220  		formEnc, err := query.Values(body)
   221  		if err != nil {
   222  			return nil, err
   223  		}
   224  		form := strings.NewReader(formEnc.Encode())
   225  		req, err = http.NewRequest(method, u.String(), form)
   226  	} else if body != nil {
   227  		buf := new(bytes.Buffer)
   228  		enc := json.NewEncoder(buf)
   229  		enc.SetEscapeHTML(false)
   230  		err := enc.Encode(body)
   231  		if err != nil {
   232  			return nil, err
   233  		}
   234  
   235  		req, err = http.NewRequest(method, u.String(), buf)
   236  	} else {
   237  		req, err = http.NewRequest(method, u.String(), nil)
   238  	}
   239  	if err != nil {
   240  		return nil, err
   241  	}
   242  
   243  	if h.contentType != "" {
   244  		req.Header.Set("Content-Type", h.contentType)
   245  	}
   246  	if body != nil && h.contentType == "" {
   247  		req.Header.Set("Content-Type", "application/json")
   248  	}
   249  	if h.authorization != "" {
   250  		req.Header.Set("Authorization", h.authorization)
   251  	}
   252  	if isAdminRequest {
   253  		var token *OIDCToken
   254  		var err error
   255  
   256  		adminGrant := &AccessGrantRequest{}
   257  
   258  		if c.hasOfflineAccess {
   259  			adminGrant.Scope = offlineScope
   260  		}
   261  
   262  		if c.isConfidential && c.isServiceAccount {
   263  			adminGrant.GrantType = clientGrant
   264  
   265  			token, _, err = c.Authentication.GetOIDCToken(
   266  				context.Background(),
   267  				adminGrant,
   268  			)
   269  		} else {
   270  			adminGrant.GrantType = passwordGrant
   271  			adminGrant.Username = c.adminAccount
   272  			adminGrant.Password = c.adminPass
   273  
   274  			token, _, err = c.Authentication.GetOIDCToken(
   275  				context.Background(),
   276  				adminGrant,
   277  			)
   278  		}
   279  
   280  		if err != nil {
   281  			return nil, err
   282  		}
   283  		req.Header.Set("Authorization", "Bearer "+token.AccessToken)
   284  	}
   285  
   286  	return req, nil
   287  }
   288  
   289  // do sends a keycloak request and returns the repsonse.
   290  func (c *Client) do(
   291  	ctx context.Context,
   292  	req *http.Request,
   293  	v interface{},
   294  ) (*Response, error) {
   295  	req = req.WithContext(ctx)
   296  
   297  	resp, err := c.httpClient.Do(req)
   298  	if err != nil {
   299  		select {
   300  		case <-ctx.Done():
   301  			return nil, ctx.Err()
   302  		default:
   303  		}
   304  	}
   305  	defer resp.Body.Close()
   306  
   307  	response := &Response{Response: resp}
   308  
   309  	if c := resp.StatusCode; c >= 300 {
   310  		errorResponse := &ErrorResponse{Response: resp}
   311  
   312  		data, err := ioutil.ReadAll(resp.Body)
   313  		if err == nil && data != nil {
   314  			json.Unmarshal(data, errorResponse)
   315  		}
   316  
   317  		return nil, errorResponse
   318  	}
   319  
   320  	if v != nil {
   321  		if w, ok := v.(io.Writer); ok {
   322  			io.Copy(w, resp.Body)
   323  		} else {
   324  			decErr := json.NewDecoder(resp.Body).Decode(v)
   325  			if decErr == io.EOF {
   326  				decErr = nil // ignore empty response errors
   327  			}
   328  			if decErr != nil {
   329  				err = decErr
   330  			}
   331  		}
   332  	}
   333  
   334  	return response, err
   335  }