A Go client for the Keycloak Admin REST API and OIDC endpoints.
go get github.com/dsheehan167/go-keycloakRequires Go 1.25 or newer.
package main
import (
"context"
"errors"
"log"
"net/http"
"time"
"github.com/dsheehan167/go-keycloak"
)
func main() {
ctx := context.Background()
c, err := keycloak.NewClient("https://kc.example.com", "my-realm",
keycloak.WithTimeout(15*time.Second),
)
if err != nil {
log.Fatal(err)
}
tok, err := c.GetToken(ctx, keycloak.TokenRequest{
GrantType: keycloak.GrantClientCredentials,
ClientID: "admin-cli",
ClientSecret: "<secret>",
})
if err != nil {
log.Fatal(err)
}
c.SetAccessToken(tok.AccessToken)
user, err := c.Users.Get(ctx, "<user-uuid>")
if err != nil {
var kc *keycloak.Error
if errors.As(err, &kc) && kc.StatusCode == http.StatusNotFound {
log.Printf("no such user")
return
}
log.Fatal(err)
}
log.Printf("found %s", user.Username)
}NewClient(baseURL, realm string, opts ...Option):
| Option | Purpose |
|---|---|
WithLogger(*zap.Logger) |
Install a logger. Default zap.NewNop(). |
WithHTTPClient(*http.Client) |
Supply a configured client. Overrides WithTimeout and WithInsecureSkipVerify. |
WithTimeout(time.Duration) |
HTTP timeout. Default 30s. |
WithInsecureSkipVerify(bool) |
Disable TLS verification. Development only. |
WithAccessToken(string) |
Seed the bearer token at construction. |
baseURL is the scheme + host (and optional path prefix), e.g. https://kc.example.com or https://kc.example.com/auth. Trailing slashes are trimmed.
Tokens can be rotated with c.SetAccessToken(token) at any time; the field is mutex-guarded and safe for concurrent reads.
| Method | Endpoint |
|---|---|
GetToken(ctx, TokenRequest) |
POST /realms/{realm}/protocol/openid-connect/token |
Logout(ctx, LogoutRequest) |
POST /realms/{realm}/protocol/openid-connect/logout |
ListCerts(ctx) |
GET /realms/{realm}/protocol/openid-connect/certs |
ListPermissions(ctx, audience) |
UMA permission introspection |
| Method | Purpose |
|---|---|
Get(ctx, userID) |
Fetch a user |
List(ctx, ListUsersOptions) |
Paginated user search |
ListClientRoles(ctx, clientUUID, userID) |
Client roles mapped to the user |
AddClientRoles(ctx, clientUUID, userID, roleIDs) |
Assign client roles |
RemoveClientRoles(ctx, clientUUID, userID, roleIDs) |
Unassign client roles |
ListGroups(ctx, userID) |
Groups the user belongs to |
AddToGroup(ctx, userID, groupID) |
Add to group |
RemoveFromGroup(ctx, userID, groupID) |
Remove from group |
| Method | Purpose |
|---|---|
Get(ctx, groupID) |
Fetch a group |
List(ctx, ListGroupsOptions) |
Paginated group search |
Create(ctx, CreateGroupOptions) |
Create a group |
Update(ctx, groupID, UpdateGroupOptions) |
Update fields |
Delete(ctx, groupID) |
Delete |
ListMembers(ctx, groupID, ListGroupMembersOptions) |
Users in the group |
ListClientRoles(ctx, clientUUID, groupID) |
Client roles mapped to the group |
AddClientRoles(ctx, clientUUID, groupID, roleIDs) |
Assign client roles |
RemoveClientRoles(ctx, clientUUID, groupID, roleIDs) |
Unassign client roles |
| Method | Purpose |
|---|---|
Get(ctx, roleID) |
Fetch a realm role by id |
GetClientRole(ctx, clientUUID, roleName) |
Fetch a client role by name |
ListClientRoles(ctx, clientUUID, ListClientRolesOptions) |
Roles under a client |
CreateClientRole(ctx, clientUUID, CreateClientRoleOptions) |
Create |
UpdateClientRole(ctx, clientUUID, roleName, UpdateClientRoleOptions) |
Update |
DeleteClientRole(ctx, clientUUID, roleName) |
Delete |
ListClientRoleUsers(ctx, clientUUID, roleName, opts) |
Users carrying a role |
ListClientRoleGroups(ctx, clientUUID, roleName, opts) |
Groups carrying a role |
| Method | Purpose |
|---|---|
Get(ctx, clientUUID, policyID) |
Fetch a policy |
List(ctx, clientUUID, ListPoliciesOptions) |
List client policies |
Update(ctx, clientUUID, policyID, UpdatePolicyOptions) |
Update fields (not membership) |
AddRoles(ctx, clientUUID, policyID, []*PolicyRole) |
Add roles to a role policy |
RemoveRoles(ctx, clientUUID, policyID, roleIDs) |
Drop roles from a role policy |
Every non-2xx response is wrapped in *keycloak.Error carrying the HTTP status, Keycloak's error code, a message, and the raw body. Match with errors.As:
var kc *keycloak.Error
if errors.As(err, &kc) {
switch kc.StatusCode {
case http.StatusNotFound:
// ...
case http.StatusUnauthorized:
// token rejected
}
}keycloak.ErrNoAccessToken is returned when an admin method is called without a bearer token set. Match with errors.Is to drive a refresh flow:
if errors.Is(err, keycloak.ErrNoAccessToken) {
tok, _ := c.GetToken(ctx, req)
c.SetAccessToken(tok.AccessToken)
// retry
}Validation errors (missing required fields) return plain error — they signal programmer error, not a recoverable condition.
UpdateClientRoleOptions and UpdatePolicyOptions use pointer fields so callers can distinguish "leave alone" from "set". Use go-utils/utils for literal pointer helpers:
import "github.com/dsheehan167/go-utils/utils"
opts := keycloak.UpdateClientRoleOptions{
Description: utils.String("new description"),
// Name is nil → preserved
}Every List*Options embeds Pagination:
roles, err := c.Roles.ListClientRoles(ctx, clientUUID, keycloak.ListClientRolesOptions{
Pagination: keycloak.Pagination{First: 0, Max: 100},
Search: "admin",
})