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 }