go-keycloak-admin-sdk
A professional, typed Go SDK for the Keycloak Admin REST API, generated
from Keycloak's official OpenAPI specification and wrapped with a hand-written
OAuth2 auth/transport layer and ergonomic, typed augmentation.
Module path: github.com/qcodr/go-keycloak-admin-sdk
Target Keycloak: 26.7
Why generated + augmented
Keycloak publishes an official OpenAPI document (~180 operations, including the
Organizations API that hand-written clients like gocloak do not cover
first-class). Generating from that spec yields the full, typed admin surface in
one place, pinned to a Keycloak version. The trade-off is that the spec is
loosely typed in places (type: object, string payloads, form-encoded bodies),
so this SDK combines generation with a thin hand-written layer for
authentication, error handling, and the endpoints consumers use most.
The three layers
| Layer |
Files |
What it provides |
| 1. Generated |
kcadmin/zz_generated.go |
Typed models + a low-level net/http client (RawClient), produced by oapi-codegen from the vendored spec. Never hand-edited. |
| 2. Auth/transport |
kcadmin/auth.go |
OAuth2 token sources (ClientCredentialsTokenSource, PasswordTokenSource), TokenURL, and NewAuthedHTTPClient. Injects and refreshes bearer tokens. The generated client knows nothing about auth. |
| 3. Augmentation |
kcadmin/client.go, errors.go, organizations.go, augment.go |
The ergonomic Client facade, structured error mapping to typed sentinels, and typed Organization/member/user wrappers. |
Install
go get github.com/qcodr/go-keycloak-admin-sdk@latest
Requires Go 1.25+ (the go directive in go.mod, pulled up by the
testcontainers-go integration-test dependency; the CI uses Go 1.26).
Quickstart
package main
import (
"context"
"fmt"
"log"
"github.com/qcodr/go-keycloak-admin-sdk/kcadmin"
)
func main() {
ctx := context.Background()
// Service-account (client_credentials) login. Lazy and self-refreshing.
ts := kcadmin.ClientCredentialsTokenSource(
ctx,
"https://auth.example.com", // Keycloak server root (no /admin)
"myrealm", // realm hosting the service-account client
"my-service-client", // client id
"my-client-secret", // client secret (from env in real code)
)
client, err := kcadmin.NewWithTokenSource(ctx, "https://auth.example.com", ts)
if err != nil {
log.Fatal(err)
}
users, err := client.ListUsers(ctx, "myrealm", &kcadmin.ListUsersOptions{Max: 20})
if err != nil {
log.Fatal(err)
}
for _, u := range users {
fmt.Printf("%s <%s>\n", u.Username, u.Email)
}
// Organizations: invite a user by email.
if err := client.InviteUser(ctx, "myrealm", "org-id",
kcadmin.InviteUserRequest{Email: "new@example.com"}); err != nil {
log.Fatal(err)
}
}
Admin password grant
For admin-CLI style logins (username/password), use PasswordTokenSource. It is
lazy: no network call happens until the first request.
ts := kcadmin.PasswordTokenSource(ctx, baseURL, "master", "admin-cli", "admin", os.Getenv("KC_ADMIN_PASSWORD"))
client, _ := kcadmin.NewWithTokenSource(ctx, baseURL, ts)
Base URL
baseURL is the Keycloak server root (e.g. https://auth.example.com).
The generated operation paths already include the /admin prefix, so do not
append /admin yourself. Any context path in the base URL is preserved.
Error handling
Every facade method maps non-2xx responses to a typed *APIError that matches
sentinel errors with errors.Is:
err := client.AddMember(ctx, realm, orgID, userID)
switch {
case errors.Is(err, kcadmin.ErrConflict): // 409 — already a member
case errors.Is(err, kcadmin.ErrNotFound): // 404
case errors.Is(err, kcadmin.ErrForbidden): // 403
case err != nil:
var apiErr *kcadmin.APIError
errors.As(err, &apiErr) // apiErr.StatusCode, apiErr.Message, apiErr.URL
}
Sentinels: ErrBadRequest (400), ErrUnauthorized (401), ErrForbidden
(403), ErrNotFound (404), ErrConflict (409), ErrServer (5xx).
Escape hatch
For endpoints the facade does not wrap, reach the full generated client:
raw := client.Raw() // *kcadmin.RawClient — every generated operation
Example
examples/list logs in with client credentials and lists users and
organizations. Configure via environment variables (no hardcoded secrets):
KC_BASE_URL=https://auth.example.com \
KC_REALM=myrealm \
KC_CLIENT_ID=my-service-client \
KC_CLIENT_SECRET=... \
go run ./examples/list
Make targets
| Target |
Description |
make spec KC=26.7 |
Download and vendor the Keycloak OpenAPI spec into openapi/keycloak-26.7.yaml. |
make generate |
Regenerate kcadmin/zz_generated.go from the vendored spec (oapi-codegen, pinned). |
make tidy |
go mod tidy. |
make test |
Run unit tests (httptest-mocked; no live network). |
make lint |
gofmt check + go vet. |
make integration-build |
Compile the -tags=integration build without running it. |
Testing
-
Unit tests (kcadmin/*_test.go) use net/http/httptest — no live calls.
They cover token-source behavior, bearer injection, error mapping, and the
Organizations/user facade.
-
Integration test (integration_test.go, build tag integration) boots a
real Keycloak 26.7 container with
testcontainers-go and
exercises login + list users + organizations. Run it opt-in:
go test -tags=integration ./...
It requires Docker and network access to pull the image.
Generation details
- Generator:
github.com/oapi-codegen/oapi-codegen/v2 (pinned to v2.7.2
in the Makefile).
- Config:
oapi-codegen.yaml generates models + client into package
kcadmin. The generated client type is renamed to RawClient
(output-options.client-type-name) so the hand-written facade can own the
Client name. skip-prune keeps the full shared schema graph.
- Scope: the full client is generated — no operations or tags were
excluded. The Keycloak spec declares no
operationIds, so operation method
names are derived from HTTP method + path (e.g.
GetAdminRealmsRealmOrganizationsOrgIDMembers). The typed facade exists
precisely to hide those long names for common operations.
Versioning policy
SDK releases track the Keycloak minor version. A tag vMAJOR.MINOR.PATCH
(for example v26.7.x) corresponds to openapi/keycloak-26.7.yaml. To move to
a new Keycloak minor: make spec KC=<new> → make generate → review the
augmentation diff → tag the new minor.
Note on the vendored spec: Keycloak currently serves the OpenAPI document
only from its latest path; the version-pinned URL 404s. make spec falls
back to latest, and the vendored file is treated as the 26.7 line. See
openapi/README.md.
License
Apache-2.0 — see LICENSE. The vendored OpenAPI spec is © the Keycloak
authors under Apache-2.0; see openapi/README.md.