Seamlessly transfer APEX application users to Keycloak with Keycloak user migration plugin

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.

  1. It allows Keycloak to connect to a legacy authentication system via REST endpoints.

  2. When a user attempts to log in for the first time, the plugin checks the legacy system for user information.

  3. 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

  4. The plugin verifies the user's password against the legacy system.

  5. 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

  1. 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
    
  2. Use the Compatibility history table to find the version compatible with your Keycloak and click the Version/Commit link.

  3. Right click the keycloak-rest-provider-XX.X.X.jar and copy the link to clipboard

  4. Wget the file

     wget <<paste URL from clipboard>>
    
  5. 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
    
  6. Restart the container so that it loads the plugin.

     docker restart opc-keycloak-1
    
  7. REST enable your schema on the APEX side.

  8. 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;
  1. This will then create this structure

  2. Change the auth GET handler to correctly select the user details & change the groups & attributes accordingly

  3. 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.

  4. OPTIONAL/RECOMMENDED: Secure this new REST Module.

  5. Log in to Keycloak console

  6. Ensure your Realm is selected (i.e not Master)

  7. Click User federation > Add User migration using a REST client providers

  8. 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.

  9. OPTIONAL/RECOMMENDED: Set the Rest client basic auth settings

  10. 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
    
  11. Click Sign In

  12. Sign in as an Application User e.g Admin with the correct password e.g. LeedsLeedsLeeds

  13. You’ll then be prompted to change your password (as per the required_actions)

  14. Then you need to update Account Information as per the required_actions

  15. 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.