1. Background

Apache Knox Gateway has traditionally acted as a federation client, delegating authentication to external Identity Providers (IdPs) via pac4j. Through pac4j, Knox can integrate with a wide range of authentication systems including CAS, OAuth 2.0, SAML, and OpenID Connect (OIDC). In this model, Knox is strictly a relying party (client) and never an identity provider itself.

Many modern architectures, however, expect a centralized OIDC Provider (OP) that issues tokens to downstream services. Products like Okta, Azure AD, Keycloak are commonly used for this purpose, but introducing and operating a separate IdP is not always desirable, especially in Hadoop-centric or Knox-centric deployments.

This proposal addresses this gap by enabling Apache Knox to act as an OIDC Provider while still retaining its federation capabilities. With KnoxIDF:

  • Knox can issue OAuth 2.0 / OIDC tokens directly to clients.

  • Knox can optionally federate identities and tokens from external, well-known OIDC Providers (e.g., Keycloak).

  • Downstream services can integrate with Knox exactly as they would with Keycloak or any standard OIDC provider.

This makes Knox both:

  • an OIDC Provider, and

  • an OIDC federation bridge, allowing gradual migration or hybrid identity architectures

2. Functional Requirements

  1. OIDC Provider Capabilities

    • Knox must expose standard OIDC-compatible endpoints.

    • Knox must be able to issue JWT-based access tokens and ID tokens.

  2. Supported OAuth 2.0 / OIDC Flows

    • Client Credentials flow

    • Authorization Code flow

  3. Federation Support

    • Knox must be able to authenticate users via external OIDC Providers.

    • Tokens received from federated providers must be mapped into Knox-issued tokens.

    • Federation must be optional and configurable per deployment.

  4. Topology Integration

    • KnoxIDF must be deployable as a Knox service and attachable to any topology.

  5. Persistence

    • Federated identity data must be persisted for traceability and attribute reuse.

    • Only ID token–related data should be stored (no access tokens or refresh tokens).


As part of preparing this document I already implemented a POC version of this new feature, but this is still a POC. Going forward I share what/why I implemented and conclude with some possible improvement ideas.

3. Design

3.1 New Knox Service: gateway-service-knox-idf

A new Knox service named gateway-service-knox-idf shall be introduced.

Key characteristics:

  • Can be added to any Knox topology.

  • Operates independently from pac4j-based inbound authentication.

  • Exposes OIDC/OAuth-specific REST endpoints.

  • Can be configured to either:

    • act as a standalone OP, or

    • federate authentication to an external OP.

This design keeps KnoxIDF modular and avoids changes to existing gateway authentication flows.



3.2 Login page changes

In the case of Authorization Code flow, the login page will display any pre-configured federated OPs, if the feature is enabled. To do that, the existing SSOCookieProvider needed to be updated to fetch this data from the KNOXIDF service configuration and pass it to Knox's login.html, where new JS code takes care of showing the alternative login path like this:

This way, end-users can decide if they want to use Knox's own authentication providers (PAM, LDAP, SAML, Kerberos, etc...) or if they want to identify themselves using the external OIDC Provider (as part of the POC, only Keycloak is supported/tested, but this should be easily extendable to other OIDC providers such as Okta, AzureAD, PingFederate,...).



3.3 REST API Endpoints

KnoxIDF exposes a set of REST endpoints aligned with standard OIDC expectations. These endpoints are compatible with clients configured for Keycloak-like providers.

Key categories include:

  • Authorization endpoint

    • Used for Authorization Code flow

    • Extends existing PasscodeTokenResourceBase

  • Token endpoint

    • Issues access tokens and ID tokens

    • Supports:

      • Client Credentials

      • Authorization Code

    • Extends existing PasscodeTokenResourceBase
  • Metadata / configuration endpoints

    • Expose provider metadata needed by OIDC clients

  • UserInfo Endpoint

    • Allows clients to retrieve authenticated user claims using an access token

    • Enables downstream services to obtain identity and attribute information without parsing the ID token directly.

    • Characteristics:

      • Requires a valid Knox-issued access token.

      • Returns claims derived from:

        • Knox-issued identity data, or

        • Federated identity attributes when federation is enabled.

      • When federation is used, the response is based on the persisted ID token attributes stored during authentication.

  • Registration Endpoint
    • Allows clients to register their client ID/clientSecret pair
    • Extends existing ClientCredentialsResource



3.4 Federation Architecture

When federation is enabled:

  • The authorization process delegates authentication to the external OP.

  • Knox processes and validates the received ID token.

  • Knox issues its own tokens to the client.

Federation is implemented as a token brokering mechanism:

  1. Client initiates an OAuth/OIDC flow against Knox.

  2. Knox delegates authentication to a configured external OP (e.g., Keycloak).

  3. External OP authenticates the user and returns an ID token.

  4. Knox:

    • Validates the ID token.

    • Extracts identity and attribute claims.

    • Persists federated identity data.

    • Issues Knox-signed tokens to the client.

This ensures downstream services only need to trust Knox, regardless of how authentication was performed upstream.



3.4.1 Database Design

federated_identity_db_er


Federated identity data is persisted using new database tables:

federated_identity

Stores the core identity mapping between Knox and the external OP.

Columns:

      • id: a generated unique ID used as a PK
      • user_id: a generated unique user ID, used as an identifier within Knox
      • provider: the name of the federated OP (e.g. KEYCLOAK)
      • external_subject: the external subject (user ID)

      • external_issuer: the external issuer

      • created_at: creation timestamp

federated_identity_attr

Stores individual attributes extracted from the federated ID token.

Columns:

      • identity_id: foreign key to federated_identity

      • attr_key: the attribute name

      • attr_value: the attribute value

Design considerations:

  • Only ID token data is persisted.

  • No access tokens, refresh tokens, or secrets are stored.

  • Enables future attribute reuse, auditing, and enrichment.

3.4.2 JdbcFederatedIdentityService configuration


By default, the storage and the corresponding FederatedIdentityService points to an empty implementation, which returns default values (empty lists, blank String instances, etc...). It's the end-user's responsibility to explicitly configure this feature (when federation is enabled with at least one federated OP) by setting the relevant federated identity service implementation in the gateway-site.xml  as follows:

    <property>
        <name>gateway.service.knoxidf.federatedidentity.impl</name>
        <value>org.apache.knox.gateway.services.knoxidf.federation.JdbcFederatedIdentityService</value>
    </property>

Failing to do this, the token generation and user info fetch actions will return an error indicating that the federated identity could not be found.



3.5 Advanced User Parameter Mappings

KnoxIDF supports advanced mechanisms for enriching ID tokens with additional user-related claims. These mechanisms allow administrators to inject static claims or dynamically resolve user attributes from external systems, enabling flexible identity and attribute modeling comparable to established OIDC providers such as Keycloak.

Two approaches are currently supported: hard-coded ID token claims and pluggable user parameter providers.



3.5.1 Hard-coded ID Token Claims

Similar to other, well-known OIDPs, KnoxIDF allows administrators to define static, hard-coded claims that are automatically included in generated ID tokens. These claims are independent of the authenticated user and are useful for injecting fixed metadata such as roles, scopes, or system-level identifiers.

Hard-coded claim mappings are configured using the knox.token.hardcoded.claim.mappings property in the KNOXTOKEN service definition.

Example configuration:


      <param>
        <name>knox.token.hardcoded.claim.mappings</name>
        <value>principal_roles=admin;scope=openid;principal_id=0;principal_name=root</value>
      </param>


Each key-value pair represents an ID token claim name and its corresponding static value. All configured claims are added to the issued ID token during token generation.


3.5.2 User Parameters Provider

To support dynamic user-specific claim resolution, KnoxIDF introduces a pluggable user parameter resolution mechanism via the UserParamsProvider interface. This abstraction allows KnoxIDF to fetch OIDC-related user attributes from external systems at token issuance time.

The interface definition is as follows:

public interface UserParamsProvider {

    /**
     * Fetches OIDC parameters for the given subject name.
     *
     * @param subjectName The user login/ID (e.g., "sam").
     * @return a map of OIDC parameters (e.g., email, name, roles)
     */
    Map<String, Object> getParamsFor(String subjectName, String scope);
}

As part of this effort, a first concrete implementation has been added: LdapUserParamsProvider. This implementation resolves user attributes from an LDAP directory and maps them into ID token claims.

The LDAP provider currently uses a fixed configuration model (subject to future enhancement) and retrieves a predefined set of attributes (e.g., cn, sn, givenName, mail) for the authenticated user.

The LDAP connection endpoint is configured via the following topology parameter, which must be defined in the KNOXIDF service:

       <param>
          <name>user.params.provider.ldap.url</name>
          <value>ldap://host.docker.internal:33389</value>
       </param>


At token issuance time, KnoxIDF invokes the configured UserParamsProvider to enrich the ID token with dynamically resolved user claims, optionally filtered by the requested OIDC scope.



3.6 Consent Page

As part of the Authorization Code flow, KnoxIDF introduces a consent page to ensure that end-users explicitly approve the scopes requested by a client application.

Key characteristics:

  • One-time consent per user and client
    Once a user grants consent for a given set of scopes to a client, subsequent authorization requests for the same scopes do not require additional confirmation. This ensures a smooth user experience while maintaining explicit consent semantics.

  • Topology-specific servlet
    The consent page is implemented as a lightweight servlet that is automatically registered in a topology only if the KNOXIDF service is included. This avoids unnecessary overhead for topologies that do not use KnoxIDF.

  • Integration with authorization flow
    During an Authorization Code request, KnoxIDF checks whether the user has already consented to the requested scopes. If consent is missing, the user is redirected to the consent page before the code is issued.

This design aligns with standard OIDC provider behavior and provides a simple but essential layer of user control over granted permissions.


Sequence:



4. Testing

4.0 Knox setup

At the time of this document being written, the Knox IDF code is sitting on a branch called knox_idf (keeping it in sync with master) in the Apache Knox repo. The following steps will help you to start and configure Knox to handle KnoxIDF-related API calls.

4.0.1 - Build Knox

git clone https://github.com/apache/knox.git

git checkout knox_idf

mvn -DskipTests -Dcheckstyle.skip=true -Dfindbugs.skip=true -Dpmd.skip=true -Drat.skip -Dspotbugs.skip=true -Dforbiddenapis.skip=true -Ppackage clean install

4.0.2 - Install Knox

Whenever I test Knox functionalities, I never use the built-in ant targets (such as ant install-test-home), instead I unzip the generated Knox deliverables into my test folder and use a Terminal window to create the required secrets and (re-)start Knox. I use the following bash script that does the job for me:

#!/bin/bash

KNOX_SOURCE=/Users/smolnar/projects/knox
KNOX_VERSION=3.0.0-SNAPSHOT
KNOX_HOME=/Users/smolnar/test/knoxGateway

function redeployKnoxLocal() {
    echo "Redeploying Knox $KNOX_VERSION ..."
    cd $KNOX_HOME
    echo "Stopping Knox (if needed)..."
    bin/gateway.sh stop
    echo "Removing old Knox deployment files..."
    rm -rf $KNOX_HOME/*
    echo "Unpacking Knox artifacts into $KNOX_HOME ..."
    unzip -q $KNOX_SOURCE/target/$KNOX_VERSION/knox-$KNOX_VERSION.zip -d $KNOX_HOME
    mv knox-$KNOX_VERSION/* .
    rm -rf mv knox-$KNOX_VERSION
    echo "Creating master secret..."
    bin/knoxcli.sh create-master --master gateway
    echo "Creating knox.token.hash.key alias..."
    bin/knoxcli.sh create-alias knox.token.hash.key --value B5oxlx9M4h4MhaGakj8k7Q2fbmPzo6h9te8dWTHs5Mg
    echo "Creating DB aliases..."
    bin/knoxcli.sh create-alias gateway_database_user --value knox
    bin/knoxcli.sh create-alias gateway_database_password --value knox
    echo "Starting Knox..."
    bin/gateway.sh start
}

Once this is done, you'll need to edit the gateway-site.xml config file in your $KNOX_HOME/conf  folder and add the following params:

    <property>
        <name>ssl.enabled</name>
        <value>false</value>
    </property>
     <property>
        <name>gateway.service.knoxidffederatedidentity.impl</name>
        <value>org.apache.knox.gateway.services.knoxidf.federation.JdbcFederatedIdentityService</value>
    </property>

    <!-- Optionally, setup your PostgreSQL DB here -->
    <property>
        <name>gateway.service.tokenstate.impl</name>
        <value>org.apache.knox.gateway.services.token.impl.JDBCTokenStateService</value>
    </property>
     <property>
        <name>gateway.database.type</name>
        <value>postgresql</value>
    </property>
    <property>
        <name>gateway.database.connection.url</name>
        <value>jdbc:postgresql://localhost:5432/postgres</value>
    </property>

When all good, you need to restart Knox:


cd $KNOX_HOME; bin/gateway.sh restart 


4.0.3 - Install KnoxIDF topologies

You'll see that testing the client credentials and authorization code flows, we'll need 2 dedicated Knox topologies:
  • knoxidf-sso (uses Knox's SSOCookieFilter for authentication)
  • knoxidf-token (uses Knox's JWTProvider for authentication)

You have to copy them into $KNOX_HOME/conf/topologies and Knox will deploy them for you in a couple seconds.

knoxidf-sso.xml
<?xml version="1.0" encoding="utf-8"?>
<topology>
    <gateway>
      <provider>
         <role>federation</role>
         <name>SSOCookieProvider</name>
         <enabled>true</enabled>
         <param>
            <name>knox.token.exp.server-managed</name>
            <value>false</value>
         </param>
         <param>
            <name>sso.unauthenticated.path.list</name>
            <value>/api/v1/websso/federated/op,/knoxidf/api/v1/.well-known/openid-configuration,/knoxidf/api/v1/client/register,/knoxidf/api/v1/authorize/callback,/knoxidf/api/v1/jwks</value>
         </param>
        <param>
            <name>jwt.expected.issuer</name>
            <value>http://www.local.com:8443/gateway/knoxidf-sso/knoxidf</value>
        </param>
      </provider>
      <provider>
            <role>identity-assertion</role>
            <name>Default</name>
            <enabled>true</enabled>
      </provider>
    </gateway>

    <service>
        <role>KNOXIDF</role>
       <param>
          <name>knox.token.ttl</name>
          <value>60000</value>  <!-- 1 min -->
       </param>
       <param>
          <name>token.exchange.topology.name</name>
          <value>knoxidf-token</value>
       </param>

       <!-- Federated OPs -->
       <param>
          <name>federated.op.names</name>
          <value>KeyCloak,Auth0</value>
       </param>

       <!-- KeyCloak -->
       <param>
          <name>federated.op.KeyCloak.enabled</name>
          <value>true</value>
       </param>
       <param>
          <name>federated.op.KeyCloak.clientId</name>
          <value>knox-proxy</value>
       </param>
       <param>
          <name>federated.op.KeyCloak.clientSecret</name>
          <value>eGUrfiYShueXAyGC3ZZAMNScGGWBuyLb</value>
       </param>
       <param>
          <name>federated.op.KeyCloak.authorize.endpoint</name>
          <value>http://www.local.com:8081/realms/demo/protocol/openid-connect/auth</value>
       </param>
       <param>
          <name>federated.op.KeyCloak.authorize.callback</name>
          <value>http://www.local.com:8443/gateway/knoxidf-sso/knoxidf/api/v1/authorize/callback</value>
       </param>
       <param>
          <name>federated.op.KeyCloak.token.endpoint</name>
          <value>http://www.local.com:8081/realms/demo/protocol/openid-connect/token</value>
       </param>
       <param>
          <name>federated.op.KeyCloak.userinfo.endpoint</name>
          <value>http://www.local.com:8081/realms/demo/protocol/openid-connect/userinfo</value>
       </param>
       <param>
          <name>federated.op.KeyCloak.discovery.endpoint</name>
          <value>http://www.local.com:8081/realms/demo/.well-known/openid-configuration</value>
       </param>
       <!-- Auth0 -->
       <param>
          <name>federated.op.Auth0.enabled</name>
          <value>true</value>
       </param>
       <param>
          <name>federated.op.Auth0.clientId</name>
          <value>$YOUR_CLIENT_ID</value>
       </param>
       <param>
          <name>federated.op.Auth0.clientSecret</name>
          <value>$YOUR_CLIENT_SECRET</value>
       </param>
       <param>
          <name>federated.op.Auth0.authorize.endpoint</name>
          <value>https://dev-4nx2z7mc7rc6vonp.us.auth0.com/authorize</value>
       </param>
       <param>
          <name>federated.op.Auth0.authorize.callback</name>
          <value>http://www.local.com:8443/gateway/knoxidf-sso/knoxidf/api/v1/authorize/callback</value>
       </param>
       <param>
          <name>federated.op.Auth0.token.endpoint</name>
          <value>https://dev-4nx2z7mc7rc6vonp.us.auth0.com/oauth/token</value>
       </param>
       <param>
          <name>federated.op.Auth0.userinfo.endpoint</name>
          <value>https://dev-4nx2z7mc7rc6vonp.us.auth0.com/userinfo</value>
       </param>
       <param>
          <name>federated.op.Auth0.discovery.endpoint</name>
          <value>https://dev-4nx2z7mc7rc6vonp.us.auth0.com/.well-known/openid-configuration</value>
       </param>
       
       <param>
          <name>user.params.provider.ldap.url</name>
          <value>ldap://www.local.com:33389</value>
       </param>
    </service>
</topology>
knoxidf-token.xml
<?xml version="1.0" encoding="utf-8"?>
<topology>
    <gateway>
      <provider>
         <role>federation</role>
         <name>JWTProvider</name>
         <enabled>true</enabled>
         <param>
            <name>knox.token.exp.server-managed</name>
            <value>true</value>
         </param>
      </provider>
    </gateway>

    <service>
        <role>KNOXIDF</role>
       <param>
          <name>knox.token.ttl</name>
          <value>120000</value> <!-- 2 mins -->
       </param>
       <param>
          <name>knox.token.issuer</name>
          <value>http://www.local.com:8443/gateway/knoxidf-sso/knoxidf</value>
       </param>
       <param>
          <name>user.params.provider.ldap.url</name>
          <value>ldap://www.local.com:33389</value>
       </param>
    </service>
</topology>

4.0.4 Client registration

After all the above steps, we now arrived to the point where you need to register a client to Knox IDF. It's as simple as issuing the following curl request:

$ curl -ik -X POST http://localhost:8443/gateway/knoxidf-sso/knoxidf/api/v1/client/register \
> -H "Content-Type: application/x-www-form-urlencoded" \
> -H "X-XSRF-Header: valid" \
> -d "redirect_uris=http://localhost:8443/gateway/*,http://host.docker.internal:8443/*,http://host.docker.internal:9080/*&allowed_scopes=openid,profile"
HTTP/1.1 200 OK
Date: Mon, 27 Apr 2026 14:28:29 GMT
Content-Type: text/plain
Content-Length: 357

{"client_secret":"WXpBNU5tUTFPVGN0WXpkaVppMDBaVEF3TFdKaE0yUXRabUl6T0RJd01ERTNaRE0zOjpORGc0WkRkaFlqTXRObUl4TmkwME5tWTVMVGt4WmpVdE1HUTJPV00xWVdOallqWTQ=","redirect_uris":"http://localhost:8443/gateway/*,http://host.docker.internal:8443/*,http://host.docker.internal:9080/*","client_id":"c096d597-c7bf-4e00-ba3d-fb3820017d37","allowed_scopes":"openid,profile"}

You'll have to use the acquired client credentials (clientId/clientSecret pair) going forward.

4.1 Testing – Polaris

To verify KnoxIDF’s compatibility with the Client Credentials flow, we leveraged Apache Polaris using its existing Keycloak integration as a reference. The official Polaris documentation for Keycloak is available here: Polaris Keycloak IdP Guide.

Pre-requisites

Before running the tests, the following tools and setup are required:

  • Docker: for containerized deployment of Polaris and KnoxIDF.

  • Polaris source code: clone the repository locally: git clone https://github.com/apache/polaris.git (in my DEV environment, this was cloned into ~/projects/polaris)

  • Gradle: for building and running Polaris test scripts.


Test Setup

  1. Register a client  - already done in 4.04 (my client id is c096d597-c7bf-4e00-ba3d-fb3820017d37, it's a never-expiring token).
  2. Create a KnoxIDF-specific Docker environment

    • In the projects/polaris/getting-started folder, create a new subfolder named polaris_knoxidf.

    • Copy the Keycloak subfolder as a template: cp -r keycloak polaris_knoxidf 

    • Edit docker-compose.yml in polaris_knoxidf to point to KnoxIDF instead of Keycloak. The following key changes were made:

      • quarkus.oidc.auth-server-url has to point to your Knox instance: http://host.docker.internal:8443/gateway/knoxidf-sso/knoxidf/api/v1

      • quarkus.oidc.client-id shoud be set to your generated client ID: c096d597-c7bf-4e00-ba3d-fb3820017d37

      • change the token declaration in the polaris-setup service to:

        token=$$(curl -s -X POST -H "Content-Type: application/x-www-form-urlencoded" 'http://host.docker.internal:8443/gateway/knoxidf-token/knoxtoken/api/v1/token' -d 'client_id=c096d597-c7bf-4e00-ba3d-fb3820017d37' -d 'client_secret=W...0=' -d 'grant_type=client_credentials' | jq -r .access_token) &&
      • remove the keycloak service (we don't need that here)


  3. Run the KnoxIDF environment

    cd ~/projects/polaris docker compose -f getting-started/polaris_knoxidf/docker-compose.yml up



Running the Tests

  • Once the containers are up, execute the Polaris KnoxIDF test script: ./polaris_knoxidf_test.sh
    This script runs the same flow as described in the Polaris Keycloak documentation, but against KnoxIDF, verifying that Client Credentials tokens are issued and accepted correctly.

    polaris_knoxidf_test.sh
    #!/usr/bin/env bash
    set -euo pipefail
    
    ###############################################################################
    # CONFIG
    ###############################################################################
    
    POLARIS_URL="http://localhost:8181"
    KNOX_TOKEN_URL="http://localhost:8443/gateway/knox-awc-token/knoxtoken/api/v1/token"
    
    CLIENT_ID="c096d597-c7bf-4e00-ba3d-fb3820017d37"
    CLIENT_SECRET="WXpBNU5tUTFPVGN0WXpkaVppMDBaVEF3TFdKaE0yUXRabUl6T0RJd01ERTNaRE0zOjpORGc0WkRkaFlqTXRObUl4TmkwME5tWTVMVGt4WmpVdE1HUTJPV00xWVdOallqWTQ="
    
    ###############################################################################
    # 1️⃣ OBTAIN KNOXIDF TOKEN
    ###############################################################################
    
    echo ""
    echo "=================================================================="
    echo "  OBTAINING TOKEN FROM KNOXIDF"
    echo "=================================================================="
    
    KNOX_TOKEN=$(curl -sk \
      -X POST "$KNOX_TOKEN_URL" \
      -H "Content-Type: application/x-www-form-urlencoded" \
      -d "client_id=$CLIENT_ID" \
      -d "client_secret=$CLIENT_SECRET" \
      -d "grant_type=client_credentials" \
      | jq -r '.access_token')
    
    echo "KnoxIDF token: $KNOX_TOKEN"
    echo ""
    
    ###############################################################################
    # 2️⃣ OBTAIN POLARIS-TOKENS (internal + mixed realms)
    ###############################################################################
    
    echo ""
    echo "=================================================================="
    echo "  OBTAINING POLARIS TOKENS (Internal + Mixed)"
    echo "=================================================================="
    
    POLARIS_TOKEN_REALM_INTERNAL=$(curl -s "$POLARIS_URL/api/catalog/v1/oauth/tokens" \
      --user root:s3cr3t \
      -H 'Polaris-Realm: realm-internal' \
      -d 'grant_type=client_credentials' \
      -d 'scope=PRINCIPAL_ROLE:ALL' | jq -r .access_token)
    
    POLARIS_TOKEN_REALM_MIXED=$(curl -s "$POLARIS_URL/api/catalog/v1/oauth/tokens" \
      --user root:s3cr3t \
      -H 'Polaris-Realm: realm-mixed' \
      -d 'grant_type=client_credentials' \
      -d 'scope=PRINCIPAL_ROLE:ALL' | jq -r .access_token)
    
    echo "Polaris token (realm-internal): $POLARIS_TOKEN_REALM_INTERNAL"
    echo ""
    echo "Polaris token (realm-mixed)  : $POLARIS_TOKEN_REALM_MIXED"
    echo ""
    
    ###############################################################################
    # 3️⃣ TEST CASES
    ###############################################################################
    
    function test_curl() {
        local token="$1"
        local realm="$2"
        local description="$3"
        local expected="$4"   # Expected outcome: "SUCCEED" or "FAIL"
    
        echo ""
        echo "=================================================================="
        echo " $description"
        echo "=================================================================="
    
        local response status
        response=$(curl -sk -w "%{http_code}" \
            -H "Authorization: Bearer $token" \
            -H "Polaris-Realm: $realm" \
            -H "Accept: application/json" \
            "$POLARIS_URL/api/management/v1/catalogs")
        
        status="${response: -3}"      # last 3 characters = HTTP code
        body="${response:0:${#response}-3}"
    
        # Determine expected HTTP code
        local expected_code
        if [[ "$expected" == "SUCCEED" ]]; then
            expected_code=200
        else
            expected_code=401
        fi
    
        # Assert
        if [ "$status" -eq "$expected_code" ]; then
            echo "✅ PASS: Got HTTP $status as expected"
            if [ "$status" -eq 200 ]; then
                echo "Response JSON:"
                echo "$body" | jq .
            fi
        else
            echo "❌ FAIL: Got HTTP $status, expected $expected_code"
        fi
    }
    
    # -------------------------------------------------------------
    # External Knox token
    # -------------------------------------------------------------
    test_curl "$KNOX_TOKEN" "realm-internal" "TEST: Knox token → realm-internal (SHOULD FAIL)" FAIL
    test_curl "$KNOX_TOKEN" "realm-external" "TEST: Knox token → realm-external (SHOULD SUCCEED)" SUCCEED
    test_curl "$KNOX_TOKEN" "realm-mixed" "TEST: Knox token → realm-mixed (SHOULD SUCCEED)" SUCCEED
    
    # -------------------------------------------------------------
    # Polaris tokens
    # -------------------------------------------------------------
    test_curl "$POLARIS_TOKEN_REALM_INTERNAL" "realm-internal" "TEST: Polaris token (internal) → realm-internal (SHOULD SUCCEED)" SUCCEED
    test_curl "$POLARIS_TOKEN_REALM_MIXED" "realm-mixed" "TEST: Polaris token (mixed) → realm-mixed (SHOULD SUCCEED)" SUCCEED
    
    # -------------------------------------------------------------
    # Cross-realm failures
    # -------------------------------------------------------------
    test_curl "$POLARIS_TOKEN_REALM_INTERNAL" "realm-mixed" "TEST: Polaris token (internal) → realm-mixed (SHOULD FAIL)" FAIL
    
    echo ""
    echo "=================================================================="
    echo "ALL TESTS COMPLETE"
    echo "=================================================================="


Sample result:

Sample result
$ clear; ./polaris_knoxidf_test.sh 

==================================================================
  OBTAINING TOKEN FROM KNOXIDF
==================================================================
KnoxIDF token: eyJqa3UiOiJodHRwOi8vbG9jYWxob3N0Ojg0NDMvZ2F0ZXdheS9rbm94LWF3Yy10b2tlbi9rbm94dG9rZW4vYXBpL3YxL2p3a3MuanNvbiIsImtpZCI6ImxOelF5WnAtREhaLWlKVVF1M0c0MExucWFFNWNvaWRQaWZkRGRHTDlRWXciLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJkMzg4NGZmNS1hZTA3LTQ1YzgtOTM4NS0yN2UzZDliYTE4NjMiLCJraWQiOiJsTnpReVpwLURIWi1pSlVRdTNHNDBMbnFhRTVjb2lkUGlmZERkR0w5UVl3IiwiaXNzIjoiaHR0cDovL2hvc3QuZG9ja2VyLmludGVybmFsOjg0NDMvZ2F0ZXdheS9rbm94LWF3Yy10b2tlbi9rbm94Y2xvYWsiLCJwcmluY2lwYWxfbmFtZSI6InJvb3QiLCJwcmluY2lwYWxfcm9sZXMiOiJhZG1pbiIsInByaW5jaXBhbF9pZCI6IjAiLCJqa3UiOiJodHRwOi8vbG9jYWxob3N0Ojg0NDMvZ2F0ZXdheS9rbm94LWF3Yy10b2tlbi9rbm94dG9rZW4vYXBpL3YxL2p3a3MuanNvbiIsInNjb3BlIjoib3BlbmlkIiwiZXhwIjoxNzY3NjIzNzQxLCJtYW5hZ2VkLnRva2VuIjoidHJ1ZSIsImlhdCI6MTc2NzYyMDE0MSwia25veC5pZCI6IjkzMTk1ZjY2LTM5ZjktNDg2NC05Y2E1LWU2OTY1YzFmYmQ4NyJ9.Z5TRCLj89CpFN47-gua5SAzJMPzVwkyZmcCehwzbNM8biotuVhPA97MSI5ZrqhxMm03wLOcgit8kct2rnmTSSuZMgpUWGcs3_SeCPq0ZMQF0ETX5TqnR1wXdVJnD044RuM_Jam-OE60S1RNlZNupZJrbk5LkOH999tN_KYTdRZS94m2ZaYNoH4KckA50uRKTIBYN9N-kRqAdhGTTTfgKgNqs9py-nOratZSHFDDUjosF00w7E14VCEsL7QGUqyU-N8RuadT5BJ6H2rKOHfDdFNNszl3sExZ52rOWO030HdO0gyTsfDnY2n8rg4zUopHCRIOoUOqkFMWbsMHbmhx3XA


==================================================================
  OBTAINING POLARIS TOKENS (Internal + Mixed)
==================================================================
Polaris token (realm-internal): eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJwb2xhcmlzIiwic3ViIjoicm9vdCIsImlhdCI6MTc2NzYyMDE0MSwiZXhwIjoxNzY3NjIzNzQxLCJqdGkiOiJhYTc2NDJiOC03OGYzLTQ2MTctODdiYy1mYzJlYTg5NWU2MzAiLCJhY3RpdmUiOnRydWUsImNsaWVudF9pZCI6InJvb3QiLCJwcmluY2lwYWxJZCI6MSwic2NvcGUiOiJQUklOQ0lQQUxfUk9MRTpBTEwifQ.IRndQQZ1iGQyfjA6OnQ5JoX7Cz84wHTgvwTMCqBPIaa1HvwPJlWy_0KGeMKuTg2YbhuU_Tbp8mxk88WymN3ObQNNAu6gA4pa_pOdk2Vs2PgIcSkevGIEWWt61UN6DJ5oiH5Cv6qnSkI0S3SVD7kDFxg3bacO8gZtE1P8I41sClmnsHBQWO7JuC4jQDwf06DEzhoR1SGQvKrmSYlCs2IOvBLZNviFQjey4YqC5hQsq853OBzO0k_8AGYLJf8xycrMpUW3zVGJYPb1Q5dbkx1ONiRjM3XPWInJ8Knt3T7gTv9Tw0BrOuFAMEXEHHVrxvpRr3PDfxAbx9IJWLuMTmLErw

Polaris token (realm-mixed)  : eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJwb2xhcmlzIiwic3ViIjoicm9vdCIsImlhdCI6MTc2NzYyMDE0MSwiZXhwIjoxNzY3NjIzNzQxLCJqdGkiOiJjODIwN2IyZS03ZjVjLTQyNDMtODM5MS01ZDE3OWJmYWM5MmMiLCJhY3RpdmUiOnRydWUsImNsaWVudF9pZCI6InJvb3QiLCJwcmluY2lwYWxJZCI6MSwic2NvcGUiOiJQUklOQ0lQQUxfUk9MRTpBTEwifQ.hXBMe2D22liicaR02Ee6HWfTnaqV8IQY5qHC0uFc9gEQXgN9bSyhPp_-QEFF9Z4cKyfZe4JRogocBhR9TtXxdr2t-qoecBisbJXaEm9amrm0Q84BSoCQONdo8RNFPwi_5No9V7F8H_wVcRsPNuF_hKdrx4_hOv3kSRakwmKtuD0aAmDU2S4xq_pRSTyHytG91D99tiTrZCrfjrrG8DwKHRGpKW2ZkxYtZcjrZ1XUQXV7P__x7rE_IOxknaq6XNVYh2FyTiTMuJJ1iAmtF0yB-0cI9yo7F0XOuoKoGxzhywU5FoAqxkcAht0ItX0KcC5zEgzNiEoHnwgZijpmmJzudw


==================================================================
 TEST: Knox token → realm-internal (SHOULD FAIL)
==================================================================
✅ PASS: Got HTTP 401 as expected

==================================================================
 TEST: Knox token → realm-external (SHOULD SUCCEED)
==================================================================
✅ PASS: Got HTTP 200 as expected
Response JSON:
{
  "catalogs": [
    {
      "type": "INTERNAL",
      "name": "quickstart_catalog",
      "properties": {
        "default-base-location": "file:///var/tmp/quickstart_catalog/"
      },
      "createTimestamp": 1767620134745,
      "lastUpdateTimestamp": 1767620134745,
      "entityVersion": 1,
      "storageConfigInfo": {
        "storageType": "FILE",
        "allowedLocations": [
          "file:///var/tmp/quickstart_catalog/"
        ]
      }
    }
  ]
}

==================================================================
 TEST: Knox token → realm-mixed (SHOULD SUCCEED)
==================================================================
✅ PASS: Got HTTP 200 as expected
Response JSON:
{
  "catalogs": [
    {
      "type": "INTERNAL",
      "name": "quickstart_catalog",
      "properties": {
        "default-base-location": "file:///var/tmp/quickstart_catalog/"
      },
      "createTimestamp": 1767620136082,
      "lastUpdateTimestamp": 1767620136082,
      "entityVersion": 1,
      "storageConfigInfo": {
        "storageType": "FILE",
        "allowedLocations": [
          "file:///var/tmp/quickstart_catalog/"
        ]
      }
    }
  ]
}

==================================================================
 TEST: Polaris token (internal) → realm-internal (SHOULD SUCCEED)
==================================================================
✅ PASS: Got HTTP 200 as expected
Response JSON:
{
  "catalogs": [
    {
      "type": "INTERNAL",
      "name": "quickstart_catalog",
      "properties": {
        "default-base-location": "file:///var/tmp/quickstart_catalog/"
      },
      "createTimestamp": 1767620133351,
      "lastUpdateTimestamp": 1767620133351,
      "entityVersion": 1,
      "storageConfigInfo": {
        "storageType": "FILE",
        "allowedLocations": [
          "file:///var/tmp/quickstart_catalog/"
        ]
      }
    }
  ]
}

==================================================================
 TEST: Polaris token (mixed) → realm-mixed (SHOULD SUCCEED)
==================================================================
✅ PASS: Got HTTP 200 as expected
Response JSON:
{
  "catalogs": [
    {
      "type": "INTERNAL",
      "name": "quickstart_catalog",
      "properties": {
        "default-base-location": "file:///var/tmp/quickstart_catalog/"
      },
      "createTimestamp": 1767620136082,
      "lastUpdateTimestamp": 1767620136082,
      "entityVersion": 1,
      "storageConfigInfo": {
        "storageType": "FILE",
        "allowedLocations": [
          "file:///var/tmp/quickstart_catalog/"
        ]
      }
    }
  ]
}

==================================================================
 TEST: Polaris token (internal) → realm-mixed (SHOULD FAIL)
==================================================================
✅ PASS: Got HTTP 401 as expected

==================================================================
ALL TESTS COMPLETE
==================================================================



Result

Following these steps demonstrates that:

  • Polaris can successfully authenticate against KnoxIDF using the Client Credentials flow.

  • KnoxIDF tokens are compatible with existing Keycloak-based integrations.



4.2 Testing – Authorization Code Flow with APISIX

To validate KnoxIDF as an Authorization Code provider, I used APISIX as a reverse proxy, replicating the standard Keycloak integration. This test confirms both standalone and federated operation modes.

Documentation Reference

The APISIX OIDC plugin documentation can be found in the official APISIX guides. These tests use the same configuration patterns outlined there, adapted for KnoxIDF as the OIDC provider. Some of the relevant docs are:



Pre-requisites

Before testing, the following components must be available and running:

  1. APISIX

    • Download the Docker image and start APISIX (within ~/projects/apisix-docker/example )

  2. Keycloak (testing federation)

    • Download and start Keycloak.

    • Configure the apisix_test_realm as per the documentation.

    • Ensure at least one test user exists.

  3. Auth0 (testing federation)
    1. Register yourself in ww.auth0.com and create a test application. Mine is called KnoxProxy and looks like this:
    2. Create a test user. Mine is samauth0@sam.com

  4. Backend service

    • Download and run the kennethreitz/httpbin:latest Docker image.

    • This provides a simple HTTP backend that displays a test image for validating authentication flow.

  5. KnoxIDF

    • Run locally with the same setup used for Polaris testing.

    • SSL is disabled, topologies deployed.


Test Setup

1. Keycloak Route Verification

  • Create a Keycloak-protected route in APISIX to confirm the baseline behavior:

    • Configure APISIX to use Keycloak as the OIDC provider with a route pointing to the httpbin backend.

    • Open an incognito browser session and navigate to the backend endpoint (e.g., http://host.docker.internal:9080/image/png).

    • Successful authentication through Keycloak should redirect to the expected image.

  • Clean up by removing the Keycloak-protected route from APISIX.

Route creation command:

curl  -X PUT 127.0.0.1:9180/apisix/admin/routes -H "X-Api-Key: edd1c9f034335f136f87ad84b625c8f1" -d '{
    "uri":"/*",
    "id": "apisix-keycloak-protected",
    "plugins":{
        "openid-connect":{
            "client_id":"apisix",
            "client_secret":"NixXgcMKEFwP2JGVAh3mzFOP2bZAhhyp",
            "discovery":"http://host.docker.internal:8081/realms/apisix_test_realm/.well-known/openid-configuration",
            "scope":"openid profile",
            "bearer_only":false,
            "realm":"apisix_test_realm",
            "introspection_endpoint_auth_method":"client_secret_post",
            "redirect_uri":"http://host.docker.internal:9080/",
            "session": {
               "secret": "this-should-be-a-long-random-secret-1234567890",
               "cookie": {
                "domain": "host.docker.internal",
                "path": "/",
                "samesite": "Lax",
                "secure": false
               }
            }
        }
    },
    "upstream":{
        "type":"roundrobin",
        "nodes":{
            "host.docker.internal:80":1
        }
    }
}'


This step ensures the APISIX + OIDC integration works as expected before switching to KnoxIDF. Once this is ready, you may want to clean the route:

curl -X DELETE http://127.0.0.1:9180/apisix/admin/routes/apisix-keycloak-protected



2. KnoxIDF Route Verification

  • Create a KnoxIDF-protected route in APISIX pointing to the same backend (httpbin) using KnoxIDF as the OIDC provider.

  • Test both federation modes:

  1. Federation Disabled

    • Set federated.op.enabled to false in all KnoxIDF topologies.

    • Navigate to http://host.docker.internal:9080/image/png in an incognito window.

    • The Knox login page should display without any federated alternatives.

    • Successful login should redirect to the backend image.

  2. Federation Enabled

    • Enable federated.op.[KeyCloak|Auth0].enabled in the KnoxIDF topologies, and configure the details of the enabled federated OIDP(s). After saving your changes, Knox will redeploy the topologies

    • Navigate to the same endpoint in an incognito window.

    • Assuming you enabled both, the Knox login page should now offer KeyCloak and Auth0 as a federated login option.

    • Logging in via any of the authentication methods should still redirect to the backend image.

    • Confirm there are no errors in the APISIX logs.

Route creation command:

curl -X PUT 127.0.0.1:9180/apisix/admin/routes -H "X-Api-Key: edd1c9f034335f136f87ad84b625c8f1" -d '
{
    "uri":"/*",
    "id": "apisix-knoxidf-protected",
    "plugins":{
        "openid-connect":{
            "client_id":"c096d597-c7bf-4e00-ba3d-fb3820017d37",
            "client_secret":"WXpBNU5tUTFPVGN0WXpkaVppMDBaVEF3TFdKaE0yUXRabUl6T0RJd01ERTNaRE0zOjpORGc0WkRkaFlqTXRObUl4TmkwME5tWTVMVGt4WmpVdE1HUTJPV00xWVdOallqWTQ=",
            "discovery":"http://host.docker.internal:8443/gateway/knoxidf-sso/knoxidf/api/v1/.well-known/openid-configuration",
            "scope":"openid profile",
            "bearer_only":false,
            "introspection_endpoint_auth_method":"client_secret_post",
            "redirect_uri":"http://host.docker.internal:9080/",
            "token_endpoint_auth_method": "client_secret_post",
            "session": {
               "secret": "this-should-be-a-long-random-secret-1234567890",
               "cookie": {
                "domain": "host.docker.internal",
                "path": "/",
                "samesite": "Lax",
                "secure": false
               }
            }
        }
    },
    "upstream":{
        "type":"roundrobin",
        "nodes":{
            "host.docker.internal:80":1
        }
    }
}'



Key Notes

  • These tests demonstrate that KnoxIDF can act as a fully compliant OIDC Authorization Code provider, supporting:

    • Standalone login

    • Federated login through external OIDC providers (e.g., Keycloak)

  • APISIX behaves consistently whether KnoxIDF is used standalone or federated.

  • This test validates end-to-end OIDC flows, including login, consent (if required), token issuance, and redirection.



5. Future Improvements

Several advanced features commonly provided by other OIDPs are good candidates for future KnoxIDF enhancements:

Refresh Token Support check mark button

  • Support long-lived sessions without reauthentication.

  • Particularly useful for browser-based clients.

User & Client Management APIs

  • CRUD APIs for OAuth clients.

  • Attribute mapping and claim customization.

Role & Scope Management

  • Fine-grained authorization via roles and scopes.

  • Mapping federated roles into Knox-issued tokens.

Multi-OP Federation check mark button 

  • Support federation from multiple external OPs simultaneously.

  • Provider selection based on realm, client, or request context.

Additional User Parameter Provider Implementations

While the UserParamsProvider abstraction enables dynamic user claim resolution, the current implementation is limited to LDAP-based attribute lookup. Future enhancements may introduce additional provider implementations to support a wider range of enterprise identity sources.

Potential implementations include:

  • Database-backed user parameter providers

  • REST-based providers for external identity or profile services

  • Integration with existing Knox or Hadoop user/group resolution mechanisms

Supporting multiple provider types would allow KnoxIDF to serve as a more flexible identity broker across heterogeneous environments.

Automatic Federated Identity Service Enablement check mark button 

Currently, the gateway.service.knoxidf.federatedidentity.impl configuration must be explicitly set to enable federated identity persistence. This manual step introduces the possibility of misconfiguration when KnoxIDF is used across multiple topologies.

As a future improvement, Knox could automatically enable the federated identity service implementation whenever at least one topology includes the KNOXIDF service. This would simplify deployment, reduce configuration overhead, and ensure consistent federation behavior without requiring additional administrative intervention.

Automated Docker-based tests check mark button 

Knox supports automated Docker-based integration tests for a while now. It's essential that we add KnoxIDF-related tests into that framework.


  • No labels