Keycloak Onboarding LabChapter V — Admin REST API (AdminRoot)0/5Contents
Chapter V

Admin REST API (AdminRoot)

AdminRoot (@Path("/admin")) is the single JAX-RS root that every admin console and account console request passes through: both the admin-ui and account-ui SPAs talk to it exclusively via the generated KeycloakAdminClient, never by hand-rolled fetches. It resolves the bearer token's issuer to a realm, authenticates the caller against that realm, applies CORS, and then delegates into RealmsAdminResourceRealmAdminResource, all wired through the request-scoped KeycloakSession that reaches every backend provider. Because it is the sole seam between the whole frontend surface and the provider ecosystem, its auth/CORS/realm-resolution logic here explains how every admin feature ultimately gets to Keycloak's data.


@Provider@Path("/admin")public class AdminRoot {    protected static final Logger logger = Logger.getLogger(AdminRoot.class);    protected TokenManager tokenManager;    @Context    protected KeycloakSession session;    public AdminRoot() {        this.tokenManager = new TokenManager();    }
services/src/main/java/org/keycloak/services/resources/admin/AdminRoot.java · lines 6779
Figure I · AdminRoot is registered as a JAX-RS @Provider at /admin and every request-scoped KeycloakSession is injected via @Context. Every sub-resource it returns (realms, serverinfo, console redirects) is constructed by passing this same session along, so the whole admin API shares one session per request.
walkthrough

How a bearer token resolves to a realm and an authenticated admin

services/src/main/java/org/keycloak/services/resources/admin/AdminRoot.java · lines 182216
lines 185–192

The Authorization header's bearer token is pulled out and parsed as a JWS; if it's missing or malformed the request is rejected before any realm lookup happens.

lines 194–199

The realm isn't a path segment here — it's derived from the token's iss (issuer) claim, then looked up and set on the session context. This is how one root path serves every realm's admin traffic.

lines 201–206

Actual authentication is delegated to BearerTokenAuthenticator, which validates the token against the resolved realm and connection — AdminRoot itself never inspects roles or scopes.

lines 211–212

On success, an AdminAuth wraps the realm, token, user and client — this bundle is what every downstream admin resource (RealmsAdminResource, RealmAdminResource, ...) receives instead of re-authenticating.

public static AdminAuth authenticateRealmAdminRequest(KeycloakSession session) {    HttpHeaders headers = session.getContext().getRequestHeaders();    String tokenString = AppAuthManager.extractAuthorizationHeaderToken(headers);    if (tokenString == null) throw new NotAuthorizedException("Bearer");    AccessToken token;    try {        JWSInput input = new JWSInput(tokenString);        token = input.readJsonContent(AccessToken.class);    } catch (JWSInputException e) {        throw new NotAuthorizedException("Bearer token format error");    }    String realmName = Encode.decodePath(token.getIssuer().substring(token.getIssuer().lastIndexOf('/') + 1));    RealmManager realmManager = new RealmManager(session);    RealmModel realm = realmManager.getRealmByName(realmName);    if (realm == null) {        throw new NotAuthorizedException("Unknown realm in token");    }    session.getContext().setRealm(realm);    AuthenticationManager.AuthResult authResult = new AppAuthManager.BearerTokenAuthenticator(session)            .setRealm(realm)            .setConnection(session.getContext().getConnection())            .setHeaders(headers)            .authenticate();    if (authResult == null) {        logger.debug("Token not valid");        throw new NotAuthorizedException("Bearer");    }    session.getContext().setBearerToken(authResult.token());    return new AdminAuth(realm, authResult.token(), authResult.user(), authResult.client());}
step 1 of 4
walkthrough

Dispatch into the realm-scoped admin API

services/src/main/java/org/keycloak/services/resources/admin/AdminRoot.java · lines 232254
lines 232–237

Every path under /admin/realms/... funnels through this sub-resource locator; a feature-flag check (ADMIN_API) can make the whole admin REST surface disappear (404) if disabled.

lines 239–241

CORS preflight (OPTIONS) requests are short-circuited to a dedicated preflight resource before any authentication is attempted, since browsers send OPTIONS without credentials.

lines 243–250

Real requests are authenticated, then a Cors builder validates the request's Origin against the client's registered allowed origins from the token before any realm resource logic runs — this is what lets admin-ui and account-ui, served from a different origin than the backend, call the API from the browser at all.

line 252

Finally a fresh RealmsAdminResource is handed the session, the resolved AdminAuth, and the tokenManager — the JAX-RS sub-resource-locator pattern means AdminRoot never implements admin business logic itself, it only sets up auth/CORS and routes deeper.

@Path("realms")public RealmsAdminResource getRealmsAdmin() {    HttpRequest request = getHttpRequest();    if (!isAdminApiEnabled()) {        throw new NotFoundException();    }    if (request.getHttpMethod().equals(HttpMethod.OPTIONS)) {        return new RealmsAdminResourcePreflight(session, null, tokenManager, request);    }    AdminAuth auth = authenticateRealmAdminRequest(session);    if (auth != null) {        if (logger.isDebugEnabled()) {            logger.debugf("authenticated admin access for: %s", auth.getUser().getUsername());        }    }    Cors.builder().checkAllowedOrigins(auth.getToken()).allowedMethods("GET", "PUT", "POST", "DELETE").exposedHeaders("Location").auth().add();    return new RealmsAdminResource(session, auth, tokenManager);}
step 1 of 4
export async function initAdminClient(  keycloak: Keycloak,  environment: Environment,) {  const adminClient = new KeycloakAdminClient();  adminClient.setConfig({ realmName: environment.realm });  adminClient.baseUrl = environment.adminBaseUrl;  adminClient.registerTokenProvider({    async getAccessToken() {      try {        await keycloak.updateToken(5);      } catch {        await keycloak.login();      }      return keycloak.token;    },  });  return adminClient;}
js/apps/admin-ui/src/admin-client.tsx · lines 2344
Figure II · The token provider always tries to refresh via keycloak.updateToken(5) (refresh if it expires within 5 seconds) before returning a token, and falls back to a full login redirect on failure. This is exactly the bearer token that AdminRoot's authenticateRealmAdminRequest later parses to resolve the realm from the token issuer.
checkpoint

Check your understanding

Answered in place — nothing is graded, everything is explained. 0 / 1 passed

In authenticateRealmAdminRequest, how does AdminRoot know which realm a request belongs to?