NAME
WWW::Keycloak::Admin - Keycloak Admin REST API for one realm, with idempotent ensure methods
VERSION
version 0.001
SYNOPSIS
my $admin = WWW::Keycloak->new( base_url => $url, realm => 'main', username => 'admin', password => $pw )->admin;
# one call, one endpoint
my $client = $admin->find_client('my-cli');
my $id = $admin->create_user( { username => 'alice', enabled => \1 } );
# wanted state, as often as you like
my $r = $admin->ensure_client( clientId => 'my-cli', publicClient => \1 );
print $r->{changed}; # 'created', 'updated' or ''
DESCRIPTION
The Admin REST API of the realm the WWW::Keycloak facade was made for.
The basic methods are one endpoint each. get_* and find_* return the representation as a hash (find_* returns nothing when there is no match), list_* an array reference, create_* the id of the new object, which Keycloak sends in the Location header, and update_* and delete_* true. Every failure is a WWW::Keycloak::Error::API; is_not_found and is_conflict tell the common cases apart.
The ensure_* methods are what makes a setup repeatable. Each looks the object up by its readable key, creates it when it is missing, otherwise writes only what differs, and returns { id => ..., changed => 'created' | 'updated' | '' }. Only the keys given are compared; nothing is ever deleted.
When Keycloak refuses the token (HTTP 401), the request is repeated once with a fresh one.
base_url
Required. The Keycloak URL without /realms/....
realm
Required. The realm every method works on.
ua
Required. The LWP::UserAgent to use.
auth
The WWW::Keycloak::Auth that supplies the admin token. Without it every call throws a validation error.
call
my $result = $admin->call( GET => '/clients?clientId=x' );
my $result = $admin->call( POST => '/admin/realms', { realm => 'new' } );
One request against the Admin API. A path starting with /admin/ is taken from the server root, anything else from the realm. Returns what "send_request" in WWW::Keycloak::Role::HTTP returns. The way to reach an endpoint this class has no method for.
server_info
get_realm
create_realm
$admin->create_realm( { enabled => \1 } ); # the realm of this object
update_realm
$admin->update_realm( { accessTokenLifespan => 600 } );
Keycloak takes a partial representation here and leaves the rest alone.
delete_realm
export_realm
my $rep = $admin->export_realm( clients => 1, groups_and_roles => 0 );
Keycloak masks secrets and authenticator settings in the export.
partial_import
my $summary = $admin->partial_import( { users => [ ... ] }, if_exists => 'SKIP' );
if_exists is FAIL (default), SKIP or OVERWRITE.
list_clients
my $clients = $admin->list_clients( first => 0, max => 50 );
find_client
my $client = $admin->find_client('my-cli') or die 'no such client';
By clientId, the readable key. Everything else takes the internal id.
get_client
create_client
update_client
$admin->update_client( $id, { %$client, description => 'new' } );
Send the whole representation; "ensure_client" does that for you.
delete_client
get_client_secret
regenerate_client_secret
get_service_account_user
list_client_scopes
find_client_scope
my $scope = $admin->find_client_scope('amr');
By name.
get_client_scope
create_client_scope
update_client_scope
delete_client_scope
add_default_client_scope
$admin->add_default_client_scope( $client_id, $scope_id ); # both internal ids
add_realm_default_client_scope
$admin->add_realm_default_client_scope($scope_id);
New clients of the realm get this scope.
list_protocol_mappers
my $mappers = $admin->list_protocol_mappers( client => $client_id );
my $mappers = $admin->list_protocol_mappers( client_scope => $scope_id );
create_protocol_mapper
my $id = $admin->create_protocol_mapper( client => $client_id, { name => 'amr', protocol => 'openid-connect', protocolMapper => 'oidc-amr-mapper', config => {...} } );
update_protocol_mapper
$admin->update_protocol_mapper( client => $client_id, $mapper_id, \%rep );
delete_protocol_mapper
$admin->delete_protocol_mapper( client_scope => $scope_id, $mapper_id );
list_users
my $users = $admin->list_users( search => 'ali', max => 20 );
find_user
my $user = $admin->find_user('alice');
By username, exactly; Keycloak stores user names in lower case.
get_user
create_user
my $id = $admin->create_user( { username => 'alice', enabled => \1, credentials => [ { type => 'password', value => $pw, temporary => \0 } ] } );
update_user
delete_user
set_password
$admin->set_password( $id, $password, temporary => 0 );
list_credentials
delete_credential
list_sessions
logout_user
list_flows
list_executions
my $steps = $admin->list_executions('browser');
All steps of a flow and its sub-flows, flat, each with level, providerId and authenticationConfig.
copy_flow
get_execution_config
create_execution_config
my $config_id = $admin->create_execution_config( $execution_id, { alias => 'x', config => {...} } );
Works on the built-in flows too.
update_execution_config
$admin->update_execution_config( $config_id, { alias => 'x', config => {...} } );
describe_authenticator
ensure_realm
$admin->ensure_realm( enabled => \1, accessTokenLifespan => 600 );
ensure_client
my $r = $admin->ensure_client( clientId => 'my-cli', publicClient => \1, attributes => { ... } );
defaultClientScopes, optionalClientScopes and protocolMappers are refused: Keycloak takes them when a client is created but ignores them when it is updated, so they could not be kept in the wanted state. Use "add_default_client_scope" and "ensure_protocol_mapper".
ensure_client_scope
my $r = $admin->ensure_client_scope( name => 'amr', attributes => { 'include.in.token.scope' => 'false' } );
The protocol defaults to openid-connect.
ensure_protocol_mapper
my $r = $admin->ensure_protocol_mapper( client => 'my-cli', name => 'amr', protocolMapper => 'oidc-amr-mapper', config => { 'id.token.claim' => 'true' } );
my $r = $admin->ensure_protocol_mapper( client_scope => 'amr', name => 'amr', ... );
The owner is named by clientId or by scope name. The protocol defaults to openid-connect.
ensure_user
my $r = $admin->ensure_user( username => 'alice', enabled => \1, email => 'alice@example.org',
credentials => [ { type => 'password', value => $pw, temporary => \0 } ] );
credentials are used when the user is created and ignored afterwards: a password is not reset on every run. Call "set_password" for that. username and email are compared in lower case, the way Keycloak keeps them, and attribute values as lists of strings (a single value may be given as a string). Attributes the realm's user profile does not declare are dropped by Keycloak unless the profile allows unmanaged attributes; such an attribute never arrives and is reported as updated on every run.
ensure_execution_config
my $r = $admin->ensure_execution_config(
flow => 'browser',
authenticator => 'auth-otp-form',
config => { 'default.reference.value' => 'otp', 'default.reference.maxAge' => 3600 },
);
Settings of one step of an authentication flow, found by its authenticator. alias names a new configuration; default "<flow> <authenticator>".
Give the complete configuration. Keycloak hides the values of these settings when they are read (they come back as **********), so they can neither be compared nor merged: an existing configuration is replaced by config and reported as updated on every run.
SUPPORT
Issues
Please report bugs and feature requests on GitHub at https://github.com/Getty/p5-www-keycloak/issues.
IRC
Join #kubernetes on irc.perl.org or message Getty directly.
CONTRIBUTING
Contributions are welcome! Please fork the repository and submit a pull request.
AUTHOR
Torsten Raudssus <getty@cpan.org>
COPYRIGHT AND LICENSE
This software is copyright (c) 2026 by Torsten Raudssus <torsten@raudssus.de> https://raudssus.de/.
This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself.