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.