Seamlessly transfer APEX application users to Keycloak with Keycloak user migration plugin
Whats this about? Say for example, you are using APEX with a current Authentication, could be APEX Authentication, could be Table Based, etc. You want to implement Keycloak (using this amazing guide), however you want a way to transfer your users to Keycloak and you are looking at options.
Whats the solution? For this we are going to use the Keycloak user migration plugin.
Whilst you’re reading what this plugin does, in your head, substitute the words legacy system for current APEX application.
It allows Keycloak to connect to a legacy authentication system via REST endpoints.
When a user attempts to log in for the first time, the plugin checks the legacy system for user information.
If the user exists in the legacy system, the plugin creates a corresponding user account in Keycloak, copying over details such as username, email, roles, and even TOTP (Time-based One-Time Password) devices
The plugin verifies the user's password against the legacy system.
After the first successful login, the user is fully migrated to Keycloak, and subsequent logins are handled entirely by Keycloak.
So rather than a mass import, its going to trickle the users in to Keycloak as and when they log in - brilliant.
Steps
Find your current version of Keycloak
docker exec -it opc-keycloak-1 /opt/keycloak/bin/kc.sh --version
Keycloak 22.0.4 JVM: 17.0.8 (Red Hat, Inc. OpenJDK 64-Bit Server VM 17.0.8+7-LTS) OS: Linux 5.15.0-301.163.5.2.el8uek.x86_64 amd64
Use the Compatibility history table to find the version compatible with your Keycloak and click the Version/Commit link.
Right click the
keycloak-rest-provider-XX.X.X.jar
and copy the link to clipboardWget the file
wget <<paste URL from clipboard>>
Copy it to the providers folder (change filename - I’m using the 2.0.0 version)
docker cp keycloak-rest-provider-2.0.0.jar opc-keycloak-1:/opt/keycloak/providers
Restart the container so that it loads the plugin.
docker restart opc-keycloak-1
REST enable your schema on the APEX side.
Import these Restful Services (totps has been intentionally commented out)
-- Generated by ORDS REST Data Services 24.3.2.r3121009
-- Schema: Marching_on_Together Date: Thu Dec 12 03:35:48 2024
--
BEGIN
ORDS.DEFINE_MODULE(
p_module_name => 'keycloak-user-migration',
p_base_path => '/kum/',
p_items_per_page => 25,
p_status => 'PUBLISHED',
p_comments => NULL);
ORDS.DEFINE_TEMPLATE(
p_module_name => 'keycloak-user-migration',
p_pattern => 'auth/:username',
p_priority => 0,
p_etag_type => 'HASH',
p_etag_query => NULL,
p_comments => NULL);
ORDS.DEFINE_HANDLER(
p_module_name => 'keycloak-user-migration',
p_pattern => 'auth/:username',
p_method => 'GET',
p_source_type => 'plsql/block',
p_mimes_allowed => NULL,
p_comments => NULL,
p_source =>
'DECLARE
l_return CLOB;
BEGIN
WITH my_users AS (
SELECT
null AS id,
:username AS username,
:username || ''@example.com'' AS email,
:username AS first_name,
''Smith'' AS last_name,
''true'' AS enabled,
''true'' AS email_verified,
JSON_OBJECT(''position'' VALUE JSON_ARRAY(''rockstar-developer''), ''likes'' VALUE JSON_ARRAY(''cats'', ''dogs'', ''cookies'')) AS attributes,
JSON_ARRAY(''admin'') AS roles,
JSON_ARRAY(''migrated_users'') AS groups,
JSON_ARRAY(''UPDATE_PASSWORD'',''UPDATE_PROFILE'',''update_user_locale'') AS required_actions
-- ,JSON_ARRAY(
-- JSON_OBJECT(
-- ''name'' VALUE ''Totp Device 1'',
-- ''secret'' VALUE ''secret'',
-- ''digits'' VALUE 6,
-- ''period'' VALUE 30,
-- ''algorithm'' VALUE ''HmacSHA1'',
-- ''encoding'' VALUE ''BASE32''
-- )
-- ) AS totps
FROM dual
)
SELECT ' || 'JSON_OBJECT(
''id'' VALUE id,
''username'' VALUE username,
''email'' VALUE email,
''firstName'' VALUE first_name,
''lastName'' VALUE last_name,
''enabled'' VALUE enabled,
''emailVerified'' VALUE email_verified,
''attributes'' VALUE attributes,
''roles'' VALUE roles,
''groups'' VALUE groups,
''requiredActions'' VALUE required_actions
-- ,''totps'' VALUE totps
) AS user_json
INTO l_return
FROM my_users;
-- Set the content type to JSON
owa_util.mime_header(''application/json'', false);
htp.p(''Content-Length: '' || dbms_lob.getlength(l_return));
owa_util.http_header_close;
-- Output the JSON content
htp.p(l_return);
END;
');
ORDS.DEFINE_HANDLER(
p_module_name => 'keycloak-user-migration',
p_pattern => 'auth/:username',
p_method => 'POST',
p_source_type => 'plsql/block',
p_mimes_allowed => 'application/json',
p_comments => NULL,
p_source =>
'declare
l_app_c constant number default 248;
e_incorrect_credentials exception;
begin
-- Set the content type to JSON
owa_util.mime_header(''text/plain'', false);
htp.p(''Content-Length: '' || 0);
owa_util.http_header_close;
apex_session.create_session (p_app_id => l_app_c, p_page_id => 1, p_username => ''nobody'');
if not apex_util.is_login_password_valid( UPPER(:username) , :password ) then
raise e_incorrect_credentials;
end if;
:status := 200;
exception
when e_incorrect_credentials then
:status := 401; -- Unauthorized
when others then
:status := 400;
end;
');
ORDS.DEFINE_PARAMETER(
p_module_name => 'keycloak-user-migration',
p_pattern => 'auth/:username',
p_method => 'POST',
p_name => 'USERNAME',
p_bind_variable_name => 'username',
p_source_type => 'HEADER',
p_param_type => 'STRING',
p_access_method => 'IN',
p_comments => NULL);
ORDS.DEFINE_PARAMETER(
p_module_name => 'keycloak-user-migration',
p_pattern => 'auth/:username',
p_method => 'POST',
p_name => 'X-APEX-STATUS-CODE',
p_bind_variable_name => 'status',
p_source_type => 'HEADER',
p_param_type => 'INT',
p_access_method => 'OUT',
p_comments => NULL);
COMMIT;
END;
This will then create this structure
Change the auth GET handler to correctly select the user details & change the groups & attributes accordingly
Optional: Edit the auth POST handler to validate the user/password combination as appropriate for your application & change the 248 App ID to your application.
OPTIONAL/RECOMMENDED: Secure this new REST Module.
Log in to Keycloak console
Ensure your Realm is selected (i.e not Master)
Click User federation > Add User migration using a REST client providers
Use the settings as below ensuring you set your URL. In the case of on-premise, you container must be able to view your REST service. Bridge-host or ngrok might help here.
OPTIONAL/RECOMMENDED: Set the Rest client basic auth settings
Sign out of Keycloak and try to sign in as a new Realm User / or use a new Private Tab e.g.
https://identity.example.com/realms/YOUR-REALM-NAME/account
Click Sign In
Sign in as an Application User e.g Admin with the correct password e.g. LeedsLeedsLeeds
You’ll then be prompted to change your password (as per the required_actions)
Then you need to update Account Information as per the required_actions
The user is now signed in and the user is now created in Keycloak & REST will not be used again for this user.
ENJOY
What’s the picture? Right here. There’s some great restaurants / bars at the bottom of this ginnel.