You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@knox.apache.org by sm...@apache.org on 2026/07/05 12:01:46 UTC

(knox) 01/01: Knox as OIDC Provider (#1215)

This is an automated email from the ASF dual-hosted git repository.

smolnar82 pushed a commit to branch knox_idf
in repository https://gitbox.apache.org/repos/asf/knox.git

commit 3a3b8043d5605ab9bcb7bddbb3edfc1d4bee0653
Author: Sandor Molnar <sm...@apache.org>
AuthorDate: Wed May 20 22:31:52 2026 +0200

    Knox as OIDC Provider (#1215)
    
    * KnoxIDF - Initial commit
    * KnoxIDF - multi OP support
    * KnoxIDF - make token endpoint configurable during discovery
    * KnoxIDF - Code cleanup and bug fixes
    * KnoxIDF - Multi OP enablement improvements and code adoption to Larry's recent changes
    * KnoxIDF - Add REFRESH_TOKEN support
    * KnoxIDF - Automatically enable JdbcFederatedIdentityService when KnoxIDF is present in any topology
    * KnoxIDF - Added Docker-based integration tests
    * KnoxIDF: configurable user params provider (only LDAP for now)
    * KnoxIDF: add support for auth code flow with PKCE
    * KnoxIDF: fix an issue with the empty user params provider implementation
    * KnoxIDF: Refactor Docker build to use local Maven artifacts and unify CI/Dev workflows
---
 .github/workflows/build/Dockerfile                 |   5 +-
 .../build/conf/topologies/knoxidf-ldap.xml         |  67 +++
 .../build/conf/topologies/knoxidf-token.xml        |  42 ++
 .github/workflows/publish-test-results.yml         |   2 +-
 .github/workflows/tests.yml                        |  30 ++
 .github/workflows/tests/common_utils.py            |  31 ++
 .github/workflows/tests/test_knoxidf.py            | 344 ++++++++++++++++
 .../main/resources/build-tools/spotbugs-filter.xml |   5 +
 .../applications/knoxauth/app/js/knoxauth.js       |  37 +-
 .../resources/applications/knoxauth/app/login.html |   8 +-
 .../applications/knoxauth/app/styles/knox.css      |  67 +++
 .../src/main/resources/docker/Dockerfile           |   1 -
 .../federation/jwt/filter/JWTFederationFilter.java |  20 +-
 .../jwt/filter/SSOCookieFederationFilter.java      |  25 ++
 .../provider/federation/SSOCookieProviderTest.java |  36 +-
 .../knox/gateway/filter/RedirectToUrlFilter.java   |   2 +-
 gateway-release/home/conf/topologies/knoxsso.xml   |   4 +
 gateway-release/home/conf/users.ldif               |  20 +-
 gateway-release/pom.xml                            |   4 +
 .../apache/knox/gateway/UrlEncodedFormRequest.java |  14 +-
 .../database/AbstractDataSourceFactory.java        |   8 +
 .../apache/knox/gateway/database/DatabaseType.java |  39 +-
 .../apache/knox/gateway/database/KnoxDatabase.java |  23 +-
 .../knox/gateway/deploy/DeploymentFactory.java     |   7 +
 .../gateway/services/DefaultGatewayServices.java   |   2 +
 .../factory/FederatedIdentityServiceFactory.java   |  97 +++++
 .../EmptyFederatedIdentitityService.java           |  51 +++
 .../federation/FederatedIdentityDatabase.java      | 132 ++++++
 .../FederatedIdentityServiceMessages.java          |  35 ++
 .../federation/JdbcFederatedIdentityService.java   | 108 +++++
 .../services/token/impl/TokenStateDatabase.java    |  14 +-
 ...org.apache.knox.gateway.services.ServiceFactory |   9 +-
 ...eateKnoxIDFFederatedIdentityAttributesTable.sql |  21 +
 ...noxIDFFederatedIdentityAttributesTableDerby.sql |  22 +
 ...oxIDFFederatedIdentityAttributesTableOracle.sql |  22 +
 .../createKnoxIDFFederatedIdentityTable.sql        |  25 ++
 .../createKnoxIDFFederatedIdentityTableDerby.sql   |  25 ++
 .../createKnoxIDFFederatedIdentityTableOracle.sql  |  25 ++
 .../services/AbstractGatewayServicesTest.java      |   3 +-
 .../pom.xml                                        |  95 ++---
 .../service/knoxidf/AuthConsentServlet.java        | 132 ++++++
 .../gateway/service/knoxidf/AuthorizeResource.java | 408 +++++++++++++++++++
 .../gateway/service/knoxidf/DiscoveryResource.java |  76 ++++
 .../knox/gateway/service/knoxidf/JwksResource.java |  26 +-
 .../knox/gateway/service/knoxidf/OIDCScope.java    |  66 +++
 .../service/knoxidf/RegistrationResource.java      | 153 +++++++
 .../gateway/service/knoxidf/TokenResource.java     | 452 +++++++++++++++++++++
 .../gateway/service/knoxidf/UserInfoResource.java  | 141 +++++++
 .../KnoxIDFServiceDeploymentContributor.java       |  29 +-
 .../userparams/EmptyUserParamsProvider.java        |  15 +-
 .../knoxidf/userparams/LdapUserParamsProvider.java | 201 +++++++++
 .../knoxidf/userparams/UserParamsProvider.java     |  17 +-
 .../userparams/UserParamsProviderFactory.java      |  15 +-
 ...nox.gateway.deploy.ServiceDeploymentContributor |  18 +
 .../gateway/service/knoxsso/WebSSOResource.java    |  84 ++--
 .../knoxtoken/ClientCredentialsResource.java       |   6 +
 .../gateway/service/knoxtoken/TokenResource.java   |  83 ++--
 .../gateway/security/CommonTokenConstants.java     |   2 +
 .../apache/knox/gateway/services/ServiceType.java  |   3 +-
 .../knoxidf/federation/FederatedIdentity.java      |  87 ++++
 .../federation/FederatedIdentityService.java}      |  21 +-
 .../FederatedIdentityServiceException.java}        |  15 +-
 .../services/security/token/JWTokenAttributes.java | 130 +++---
 .../security/token/JWTokenAttributesBuilder.java   |  16 +-
 .../services/security/token/TokenMetadata.java     |  10 +-
 .../services/security/token/TokenMetadataType.java |   2 +-
 .../services/security/token/TokenUtils.java        |  17 +
 .../gateway/services/security/token/impl/JWT.java  |   3 +
 .../services/security/token/impl/JWTToken.java     |  17 +
 gateway-util-common/pom.xml                        |  17 +
 .../org/apache/knox/gateway/util/JsonUtils.java    |   9 +-
 .../util/knoxidf/AuthorizeRequestMetadata.java     | 124 ++++++
 .../knoxidf/AuthorizeRequestMetadataStore.java     |  20 +-
 .../util/knoxidf/FederatedOpConfiguration.java     |  81 ++++
 .../knoxidf/FederatedOpConfigurationFactory.java   |  42 ++
 .../knoxidf/FederatedOpConfigurationStore.java     |  22 +-
 .../gateway/util/knoxidf/KnoxIDFArtifactStore.java |  26 +-
 .../gateway/util/knoxidf/KnoxIDFConstants.java     |  60 +++
 .../knox/gateway/util/knoxidf/KnoxIDFUtils.java    | 107 +++++
 pom.xml                                            |  17 +
 80 files changed, 3994 insertions(+), 303 deletions(-)

diff --git a/.github/workflows/build/Dockerfile b/.github/workflows/build/Dockerfile
index 1781ead94..5c0407661 100644
--- a/.github/workflows/build/Dockerfile
+++ b/.github/workflows/build/Dockerfile
@@ -17,7 +17,8 @@ FROM eclipse-temurin:17-jre
 
 MAINTAINER moresandeep
 
-RUN useradd -ms /bin/bash gateway
+# Install dependencies
+RUN apt-get update && apt-get install -y git && useradd -ms /bin/bash gateway
 
 # Create temporary directories for extraction
 RUN mkdir -p /tmp/knox-artifacts /tmp/knoxshell-artifacts /knox-runtime /knoxshell /knox-runtime/knoxshell
@@ -43,6 +44,8 @@ ADD .github/workflows/build/conf/topologies/health.xml /knox-runtime/conf/topolo
 ADD .github/workflows/build/conf/topologies/knoxldap.xml /knox-runtime/conf/topologies/knoxldap.xml
 ADD .github/workflows/build/conf/topologies/remoteauth.xml /knox-runtime/conf/topologies/remoteauth.xml
 ADD .github/workflows/build/conf/topologies/k8sauth.xml /knox-runtime/conf/topologies/k8sauth.xml
+ADD .github/workflows/build/conf/topologies/knoxidf-ldap.xml /knox-runtime/conf/topologies/knoxidf-ldap.xml
+ADD .github/workflows/build/conf/topologies/knoxidf-token.xml /knox-runtime/conf/topologies/knoxidf-token.xml
 
 RUN chown -R gateway /knox-runtime/
 
diff --git a/.github/workflows/build/conf/topologies/knoxidf-ldap.xml b/.github/workflows/build/conf/topologies/knoxidf-ldap.xml
new file mode 100644
index 000000000..a82920bdb
--- /dev/null
+++ b/.github/workflows/build/conf/topologies/knoxidf-ldap.xml
@@ -0,0 +1,67 @@
+<?xml version="1.0" encoding="utf-8"?>
+<topology>
+    <gateway>
+      <provider>
+         <role>authentication</role>
+         <name>ShiroProvider</name>
+         <enabled>true</enabled>
+         <param>
+            <name>main.ldapRealm</name>
+            <value>org.apache.knox.gateway.shirorealm.KnoxLdapRealm</value>
+         </param>
+         <param>
+            <name>main.ldapRealm.userDnTemplate</name>
+            <value>uid={0},ou=people,dc=hadoop,dc=apache,dc=org</value>
+         </param>
+         <param>
+            <name>main.ldapRealm.contextFactory.url</name>
+            <value>ldap://ldap:33389</value>
+         </param>
+         <param>
+            <name>main.ldapRealm.contextFactory.authenticationMechanism</name>
+            <value>simple</value>
+         </param>
+         <param>
+            <name>urls./knoxidf/api/v1/.well-known/openid-configuration</name>
+            <value>anon</value>
+         </param>
+         <param>
+            <name>urls./knoxidf/api/v1/client/register</name>
+            <value>anon</value>
+         </param>
+         <param>
+            <name>urls./knoxidf/api/v1/authorize/callback</name>
+            <value>anon</value>
+         </param>
+         <param>
+            <name>urls./knoxidf/api/v1/jwks</name>
+            <value>anon</value>
+          </param>
+         <param>
+            <name>urls./**</name>
+            <value>authcBasic</value>
+         </param>
+      </provider>
+      <provider>
+            <role>identity-assertion</role>
+            <name>Default</name>
+            <enabled>true</enabled>
+      </provider>
+    </gateway>
+
+    <service>
+        <role>KNOXIDF</role>
+        <param>
+            <name>knoxidf.knox.token.ttl</name>
+            <value>60000</value>
+        </param>
+        <param>
+            <name>knoxidf.knox.token.limit.per.user</name>
+            <value>-1</value>
+        </param>
+        <param>
+            <name>token.exchange.topology.name</name>
+            <value>knoxidf-token</value>
+        </param>
+    </service>
+</topology>
diff --git a/.github/workflows/build/conf/topologies/knoxidf-token.xml b/.github/workflows/build/conf/topologies/knoxidf-token.xml
new file mode 100644
index 000000000..fdc11d62b
--- /dev/null
+++ b/.github/workflows/build/conf/topologies/knoxidf-token.xml
@@ -0,0 +1,42 @@
+<?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>
+      <provider>
+            <role>identity-assertion</role>
+            <name>Default</name>
+            <enabled>true</enabled>
+      </provider>
+    </gateway>
+
+    <service>
+        <role>KNOXIDF</role>
+        <param>
+           <name>knoxidf.knox.token.ttl</name>
+           <value>86400000</value>
+        </param>
+        <param>
+            <name>knoxidf.knox.token.limit.per.user</name>
+            <value>-1</value>
+        </param>
+    </service>
+    <service>
+        <role>KNOXTOKEN</role>
+        <param>
+            <name>knox.token.ttl</name>
+            <value>60000</value>
+        </param>
+        <param>
+            <name>knox.token.limit.per.user</name>
+            <value>-1</value>
+        </param>
+    </service>
+</topology>
diff --git a/.github/workflows/publish-test-results.yml b/.github/workflows/publish-test-results.yml
index 0f584f599..621830115 100644
--- a/.github/workflows/publish-test-results.yml
+++ b/.github/workflows/publish-test-results.yml
@@ -46,5 +46,5 @@ jobs:
           commit: ${{ github.event.workflow_run.head_sha }}
           event_file: artifacts/Event File/event.json
           event_name: ${{ github.event.workflow_run.event }}
-          files: "artifacts/**/*.xml"
+          files: "artifacts/test-results/**/*.xml"
 
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index bb91ae765..ff353a246 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -69,6 +69,14 @@ jobs:
           # Run the tests service defined in docker-compose.yml
           docker compose -f ./.github/workflows/compose/docker-compose.yml up --exit-code-from tests tests
 
+      - name: Collect Knox Logs and Conf
+        if: always()
+        run: |
+          mkdir -p .github/workflows/artifacts/knox-logs
+          mkdir -p .github/workflows/artifacts/knox-conf
+          docker compose -f ./.github/workflows/compose/docker-compose.yml cp knox:/knox-runtime/logs .github/workflows/artifacts/knox-logs
+          docker compose -f ./.github/workflows/compose/docker-compose.yml cp knox:/knox-runtime/conf .github/workflows/artifacts/knox-conf
+
       - name: Upload Test Results
         if: (!cancelled())
         uses: actions/upload-artifact@v4
@@ -76,6 +84,28 @@ jobs:
           name: test-results
           path: .github/workflows/tests/test-results.xml
 
+      - name: Archive Knox Logs
+        if: always()
+        run: tar -cvzf knox-logs.tar.gz -C .github/workflows/artifacts/knox-logs .
+
+      - name: Upload Knox Logs
+        if: always()
+        uses: actions/upload-artifact@v4
+        with:
+          name: knox-logs
+          path: knox-logs.tar.gz
+
+      - name: Archive Knox Conf
+        if: always()
+        run: tar -cvzf knox-conf.tar.gz -C .github/workflows/artifacts/knox-conf .
+
+      - name: Upload Knox Conf
+        if: always()
+        uses: actions/upload-artifact@v4
+        with:
+          name: knox-conf
+          path: knox-conf.tar.gz
+
       - name: Upload Event File
         uses: actions/upload-artifact@v4
         with:
diff --git a/.github/workflows/tests/common_utils.py b/.github/workflows/tests/common_utils.py
index 0a773b44e..e80193374 100644
--- a/.github/workflows/tests/common_utils.py
+++ b/.github/workflows/tests/common_utils.py
@@ -17,6 +17,8 @@
 
 from __future__ import annotations
 
+import base64
+import json
 import os
 import unittest
 from typing import Any
@@ -72,3 +74,32 @@ def assert_hsts_header(testcase: unittest.TestCase, response: requests.Response)
     """Assert the response includes the expected Strict-Transport-Security header."""
     testcase.assertIn(HSTS_HEADER_NAME, response.headers)
     testcase.assertEqual(response.headers[HSTS_HEADER_NAME], HSTS_EXPECTED_VALUE)
+
+def get_token_id_display_text(uuid):
+    """
+    Format the token ID for display, matching Knox's getTokenIDDisplayText logic.
+    """
+    if uuid and len(uuid) == 36 and "-" in uuid:
+        first_dash = uuid.find('-')
+        last_dash = uuid.rfind('-')
+        return f"{uuid[:first_dash]}...{uuid[last_dash+1:]}"
+    return uuid
+
+
+def get_token_claim(token, claim):
+    """
+    Decodes a JWT token and returns the value of the specified claim.
+    """
+    try:
+        payload_b64 = token.split('.')[1]
+        # URL-safe base64 decoding usually needs padding adjustment
+        missing_padding = len(payload_b64) % 4
+        if missing_padding:
+            payload_b64 += '=' * (4 - missing_padding)
+        # Use urlsafe_b64decode just in case, though standard b64decode often works with padding
+        payload_json = base64.urlsafe_b64decode(payload_b64).decode('utf-8')
+        payload = json.loads(payload_json)
+        return payload.get(claim)
+    except Exception as e:
+        print(f"Failed to decode token for claim '{claim}': {e}")
+        return None
\ No newline at end of file
diff --git a/.github/workflows/tests/test_knoxidf.py b/.github/workflows/tests/test_knoxidf.py
new file mode 100644
index 000000000..918cc37d2
--- /dev/null
+++ b/.github/workflows/tests/test_knoxidf.py
@@ -0,0 +1,344 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to you under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import unittest
+import hashlib
+import base64
+from urllib.parse import urlparse, parse_qs
+from requests.auth import HTTPBasicAuth
+
+from common_utils import gateway_base_url, knox_get, knox_post, get_token_claim, get_token_id_display_text
+
+class TestKnoxIDF(unittest.TestCase):
+    def setUp(self):
+        # Get the Knox Gateway URL from environment variables
+        self.base_url =  gateway_base_url()
+        self.knoxidf_ldap_url = f"{self.base_url}gateway/knoxidf-ldap/"
+        self.knoxidf_token_url = f"{self.base_url}gateway/knoxidf-token/"
+        self.username = "guest"
+        self.password = "guest-password"
+
+    def test_discovery(self):
+        """
+        Test OIDC Discovery endpoint.
+        """
+        url = f"{self.knoxidf_ldap_url}knoxidf/api/v1/.well-known/openid-configuration"
+        print(f"Testing Discovery URL: {url}")
+        response = knox_get(url)
+        self.assertEqual(response.status_code, 200)
+        config = response.json()
+        
+        # Construct expected values based on dynamic base_url
+        expected_issuer = f"{self.knoxidf_ldap_url}knoxidf"
+        expected_auth_endpoint = f"{self.knoxidf_ldap_url}knoxidf/api/v1/authorize"
+        expected_token_endpoint = f"{self.knoxidf_token_url}knoxidf/api/v1/token"
+        expected_userinfo_endpoint = f"{self.knoxidf_token_url}knoxidf/api/v1/userinfo"
+        expected_jwks_uri = f"{self.knoxidf_ldap_url}knoxidf/api/v1/jwks"
+
+        self.assertEqual(config.get("issuer"), expected_issuer)
+        self.assertEqual(config.get("authorization_endpoint"), expected_auth_endpoint)
+        self.assertEqual(config.get("token_endpoint"), expected_token_endpoint)
+        self.assertEqual(config.get("userinfo_endpoint"), expected_userinfo_endpoint)
+        self.assertEqual(config.get("jwks_uri"), expected_jwks_uri)
+
+        self.assertEqual(config.get("response_types_supported"), ["code"])
+        self.assertEqual(config.get("grant_types_supported"), ["authorization_code", "refresh_token"])
+        self.assertEqual(config.get("id_token_signing_alg_values_supported"), ["RS256"])
+        self.assertEqual(config.get("scopes_supported"), ["openid", "email", "profile", "offline_access"])
+
+    def test_client_credentials_flow(self):
+        """
+        Test OIDC Client Credentials Flow.
+        """
+        # 1. Register client
+        reg_url = f"{self.knoxidf_ldap_url}knoxidf/api/v1/client/register"
+        print(f"Registering client at: {reg_url}")
+        data = {
+            "redirect_uris": "http://localhost/callback",
+            "allowed_scopes": "openid,profile,email,offline_access"
+        }
+        response = knox_post(
+            reg_url,
+            data=data,
+            auth=HTTPBasicAuth(self.username, self.password),
+        )
+        self.assertEqual(response.status_code, 200)
+        reg_info = response.json()
+        print(f"Registration response: {reg_info}")
+        client_id = reg_info["client_id"]
+        client_secret = reg_info["client_secret"]
+
+        # 2. Get token via client_credentials
+        token_url = f"{self.knoxidf_token_url}knoxtoken/api/v1/token"
+        print(f"Getting token at: {token_url}")
+        data = {
+            "grant_type": "client_credentials",
+            "scope": "openid",
+            "client_id": client_id,
+            "client_secret": client_secret
+        }
+        # ClientCredentialsResource uses Basic Auth for client authentication
+        response = knox_post(token_url, data=data, verify=False)
+        if response.status_code != 200:
+             print(f"Token error response: {response.text}")
+        self.assertEqual(response.status_code, 200)
+        tokens = response.json()
+        self.assertIn("access_token", tokens)
+        self.assertEqual(tokens["token_type"], "Bearer")
+
+    def test_authorization_code_flow(self):
+        """
+        Test OIDC Authorization Code Flow with Refresh Token.
+        """
+        # 1. Register client
+        reg_url = f"{self.knoxidf_ldap_url}knoxidf/api/v1/client/register"
+        print(f"Registering client at: {reg_url}")
+        data = {
+            "redirect_uris": "http://localhost/callback",
+            "allowed_scopes": "openid,profile,email,offline_access"
+        }
+        response = knox_post(
+            reg_url,
+            data=data,
+            auth=HTTPBasicAuth(self.username, self.password),
+        )
+        self.assertEqual(response.status_code, 200)
+        reg_info = response.json()
+        print(f"Registration response: {reg_info}")
+        client_id = reg_info["client_id"]
+        client_secret = reg_info["client_secret"]
+
+        # 2. Authorize (with Basic Auth for the user 'guest')
+        auth_url = f"{self.knoxidf_ldap_url}knoxidf/api/v1/authorize"
+        params = {
+            "response_type": "code",
+            "client_id": client_id,
+            "redirect_uri": "http://localhost/callback",
+            "scope": "openid offline_access",
+            "state": "test_state",
+            "auto_consent": "true"
+        }
+        print(f"Authorizing at: {auth_url}")
+        # allow_redirects=False to catch the redirect to redirect_uri
+        response = knox_get(auth_url, params=params, auth=(self.username, self.password), verify=False, allow_redirects=False)
+        
+        # Should be a redirect to the callback URL
+        self.assertEqual(response.status_code, 303)
+        location = response.headers.get("Location")
+        self.assertIsNotNone(location)
+        self.assertTrue(location.startswith("http://localhost/callback"))
+        
+        parsed_url = urlparse(location)
+        query_params = parse_qs(parsed_url.query)
+        self.assertIn("code", query_params)
+        self.assertIn("state", query_params)
+        self.assertEqual(query_params["state"][0], "test_state")
+        code = query_params["code"][0]
+
+        # 3. Exchange code for tokens
+        token_url = f"{self.knoxidf_token_url}knoxidf/api/v1/token"
+        print(f"Exchanging code for tokens at: {token_url}")
+        data = {
+            "grant_type": "authorization_code",
+            "code": code,
+            "redirect_uri": "http://localhost/callback",
+            "client_id": client_id,
+            "client_secret": client_secret
+        }
+        response = knox_post(token_url, data=data, verify=False)
+        if response.status_code != 200:
+             print(f"Code exchange error: {response.text}")
+        self.assertEqual(response.status_code, 200)
+        tokens = response.json()
+        self.assertIn("access_token", tokens)
+        self.assertIn("id_token", tokens)
+        self.assertIn("refresh_token", tokens)
+        
+        refresh_token = tokens["refresh_token"]
+
+        print(f"Refresh token: {refresh_token}")
+        refresh_token_id = get_token_claim(refresh_token, 'knox.id')
+        print(f"Refresh token knox.id: {refresh_token_id}")
+
+        # 4. Refresh the token (rotation)
+        print("Refreshing token...")
+        data = {
+            "grant_type": "refresh_token",
+            "refresh_token": refresh_token,
+            "client_id": client_id,
+            "client_secret": client_secret
+        }
+        response = knox_post(token_url, data=data, verify=False)
+        self.assertEqual(response.status_code, 200)
+        new_tokens = response.json()
+        self.assertIn("access_token", new_tokens)
+        self.assertIn("refresh_token", new_tokens)
+        
+        # Verify rotation: new refresh token should be different
+        self.assertNotEqual(refresh_token, new_tokens["refresh_token"])
+
+        # 5. Verify old refresh token is invalidated
+        print(f"Verifying old refresh token is invalidated...")
+        # Use same data (with old refresh_token)
+        data_old = {
+            "grant_type": "refresh_token",
+            "refresh_token": refresh_token,
+            "client_id": client_id,
+            "client_secret": client_secret
+        }
+        response = knox_post(token_url, data=data_old, verify=False, headers={"Accept": "application/json"})
+        self.assertEqual(response.status_code, 401)
+        error_info = response.json()
+        display_id = get_token_id_display_text(refresh_token_id)
+        self.assertEqual(error_info["status"], "401")
+        self.assertIn(f"Unknown token: {display_id}", error_info["message"])
+
+    def test_authorization_code_flow_pkce_s256(self):
+        """
+        Test OIDC Authorization Code Flow with PKCE (S256).
+        """
+        # 1. Register client
+        client_id, client_secret = self._register_test_client()
+
+        # 2. PKCE Setup
+        code_verifier = "thisshouldbealongandrandomstringthatissecure"
+        code_challenge = base64.urlsafe_b64encode(hashlib.sha256(code_verifier.encode()).digest()).decode().replace('=', '')
+
+        # 3. Authorize
+        auth_url = f"{self.knoxidf_ldap_url}knoxidf/api/v1/authorize"
+        params = {
+            "response_type": "code",
+            "client_id": client_id,
+            "redirect_uri": "http://localhost/callback",
+            "scope": "openid",
+            "state": "pkce_state",
+            "auto_consent": "true",
+            "code_challenge": code_challenge,
+            "code_challenge_method": "S256"
+        }
+        response = knox_get(auth_url, params=params, auth=(self.username, self.password), allow_redirects=False)
+        self.assertEqual(response.status_code, 303)
+        location = response.headers.get("Location")
+        code = parse_qs(urlparse(location).query)["code"][0]
+
+        # 4. Token Exchange
+        token_url = f"{self.knoxidf_token_url}knoxidf/api/v1/token"
+        data = {
+            "grant_type": "authorization_code",
+            "code": code,
+            "redirect_uri": "http://localhost/callback",
+            "client_id": client_id,
+            "client_secret": client_secret,
+            "code_verifier": code_verifier
+        }
+        response = knox_post(token_url, data=data)
+        self.assertEqual(response.status_code, 200)
+        tokens = response.json()
+        self.assertIn("access_token", tokens)
+
+    def test_authorization_code_flow_pkce_plain(self):
+        """
+        Test OIDC Authorization Code Flow with PKCE (plain).
+        """
+        # 1. Register client
+        client_id, client_secret = self._register_test_client()
+
+        # 2. PKCE Setup
+        code_verifier = "some-plain-verifier"
+        code_challenge = code_verifier
+
+        # 3. Authorize
+        auth_url = f"{self.knoxidf_ldap_url}knoxidf/api/v1/authorize"
+        params = {
+            "response_type": "code",
+            "client_id": client_id,
+            "redirect_uri": "http://localhost/callback",
+            "scope": "openid",
+            "state": "pkce_plain_state",
+            "auto_consent": "true",
+            "code_challenge": code_challenge,
+            "code_challenge_method": "plain"
+        }
+        response = knox_get(auth_url, params=params, auth=(self.username, self.password), allow_redirects=False)
+        self.assertEqual(response.status_code, 303)
+        location = response.headers.get("Location")
+        code = parse_qs(urlparse(location).query)["code"][0]
+
+        # 4. Token Exchange
+        token_url = f"{self.knoxidf_token_url}knoxidf/api/v1/token"
+        data = {
+            "grant_type": "authorization_code",
+            "code": code,
+            "redirect_uri": "http://localhost/callback",
+            "client_id": client_id,
+            "client_secret": client_secret,
+            "code_verifier": code_verifier
+        }
+        response = knox_post(token_url, data=data)
+        self.assertEqual(response.status_code, 200)
+        tokens = response.json()
+        self.assertIn("access_token", tokens)
+
+    def test_authorization_code_flow_pkce_failure(self):
+        """
+        Test PKCE Failure scenarios.
+        """
+        client_id, client_secret = self._register_test_client()
+        code_verifier = "correct-verifier"
+        code_challenge = base64.urlsafe_b64encode(hashlib.sha256(code_verifier.encode()).digest()).decode().replace('=', '')
+
+        # Authorize
+        auth_url = f"{self.knoxidf_ldap_url}knoxidf/api/v1/authorize"
+        params = {
+            "response_type": "code", "client_id": client_id, "redirect_uri": "http://localhost/callback",
+            "scope": "openid", "state": "pkce_fail", "auto_consent": "true",
+            "code_challenge": code_challenge, "code_challenge_method": "S256"
+        }
+        response = knox_get(auth_url, params=params, auth=(self.username, self.password), allow_redirects=False)
+        code = parse_qs(urlparse(response.headers.get("Location")).query)["code"][0]
+
+        token_url = f"{self.knoxidf_token_url}knoxidf/api/v1/token"
+
+        # 1. Invalid verifier
+        data = {
+            "grant_type": "authorization_code", "code": code, "redirect_uri": "http://localhost/callback",
+            "client_id": client_id, "client_secret": client_secret, "code_verifier": "wrong-verifier"
+        }
+        response = knox_post(token_url, data=data)
+        self.assertEqual(response.status_code, 401)
+        self.assertIn("Invalid code_verifier", response.json()["error_description"])
+
+        # Note: the code is revoked after first use, so we need a new one for the next test
+        response = knox_get(auth_url, params=params, auth=(self.username, self.password), allow_redirects=False)
+        code = parse_qs(urlparse(response.headers.get("Location")).query)["code"][0]
+
+        # 2. Missing verifier
+        data = {
+            "grant_type": "authorization_code", "code": code, "redirect_uri": "http://localhost/callback",
+            "client_id": client_id, "client_secret": client_secret
+        }
+        response = knox_post(token_url, data=data)
+        self.assertEqual(response.status_code, 401)
+        self.assertIn("Missing code_verifier", response.json()["error_description"])
+
+    def _register_test_client(self):
+        reg_url = f"{self.knoxidf_ldap_url}knoxidf/api/v1/client/register"
+        data = {"redirect_uris": "http://localhost/callback", "allowed_scopes": "openid,profile,email,offline_access"}
+        response = knox_post(reg_url, data=data, auth=HTTPBasicAuth(self.username, self.password))
+        reg_info = response.json()
+        return reg_info["client_id"], reg_info["client_secret"]
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/build-tools/src/main/resources/build-tools/spotbugs-filter.xml b/build-tools/src/main/resources/build-tools/spotbugs-filter.xml
index fb4d7857d..d27630435 100644
--- a/build-tools/src/main/resources/build-tools/spotbugs-filter.xml
+++ b/build-tools/src/main/resources/build-tools/spotbugs-filter.xml
@@ -85,4 +85,9 @@ limitations under the License.
     <Class name="org.apache.knox.gateway.audit.log4j.correlation.Log4jCorrelationService" />
     <Bug pattern="OBJECT_DESERIALIZATION" />
   </Match>
+
+  <Match>
+    <Class name="org.apache.knox.gateway.service.knoxidf.AuthConsentServlet" />
+    <Bug pattern="UNVALIDATED_REDIRECT" />
+  </Match>
 </FindBugsFilter>
diff --git a/gateway-applications/src/main/resources/applications/knoxauth/app/js/knoxauth.js b/gateway-applications/src/main/resources/applications/knoxauth/app/js/knoxauth.js
index 4f91c2fca..5a304a556 100644
--- a/gateway-applications/src/main/resources/applications/knoxauth/app/js/knoxauth.js
+++ b/gateway-applications/src/main/resources/applications/knoxauth/app/js/knoxauth.js
@@ -16,7 +16,8 @@
  */
 
 var loginPageSuffix = "/knoxauth/login.html";
-var webssoURL = "/api/v1/websso?originalUrl=";
+var webssoURLBase = "/api/v1/websso";
+var webssoURL = webssoURLBase + "?originalUrl=";
 var userAgent = navigator.userAgent.toLowerCase();
 
 function get(name) {
@@ -26,6 +27,11 @@ function get(name) {
 	}
 }
 
+function getSimpleParam(name) {
+	const params = new URLSearchParams(window.location.search);
+	return params.get(name);
+}
+
 function testSameOrigin(url) {
 	var loc = window.location,
 		a = document.createElement('a');
@@ -55,6 +61,35 @@ var keypressed = function(event) {
 	}
 };
 
+var loadFederatedOpLinks = function() {
+	const ops = getSimpleParam("federatedOpNames")?.split(",") ?? [];
+	const container = $("#federated-op-container");
+
+	if (ops.length > 0) {
+		container.before(`
+            <div class="or-separator">
+                <span>Or</span>
+            </div>
+        `);
+	}
+
+	ops.forEach(op => {
+		container.append(`
+            <div class="fed-op-btn" onclick="loginWithOp('${op}')">
+                <span class="fed-op-icon">🌐</span>
+                <span class="fed-op-label">Continue with ${op}</span>
+            </div>
+        `);
+	});
+};
+
+var loginWithOp = function(opName) {
+	const sessionId = getSimpleParam("federatedOpLoginSession");
+	var pathname = window.location.pathname;
+	var topologyContext = pathname.replace(loginPageSuffix, "");
+	redirect(topologyContext + webssoURLBase + "/federated/op?fedOpSid=" + sessionId + "&fedOpName=" + encodeURIComponent(opName));
+};
+
 var login = function() {
 	var pathname = window.location.pathname;
 	var topologyContext = pathname.replace(loginPageSuffix, "");
diff --git a/gateway-applications/src/main/resources/applications/knoxauth/app/login.html b/gateway-applications/src/main/resources/applications/knoxauth/app/login.html
index 8c69ec9c2..4a2bbc137 100644
--- a/gateway-applications/src/main/resources/applications/knoxauth/app/login.html
+++ b/gateway-applications/src/main/resources/applications/knoxauth/app/login.html
@@ -28,7 +28,7 @@
         <link href="styles/knox.css" media="all" rel="stylesheet" type="text/css" >
 
         <script src="libs/bower/jquery/js/jquery-3.5.1.min.js" ></script>
-    <script type="text/javascript" src="js/knoxauth.js"></script>
+        <script type="text/javascript" src="js/knoxauth.js"></script>
 
         <!-- Knox Theme Configuration -->
         <script type="text/javascript" src="theme-config.js"></script>
@@ -87,7 +87,7 @@
         </script>
 
     </head>
-    <body class="login" style="">
+    <body class="login" style="" onload="loadFederatedOpLinks();">
         <!-- Page content
         ================================================== -->
         <section id="signin-container" style="margin-top: 4.5px;">
@@ -114,5 +114,9 @@
             </form>
         </section>
 
+        <div id="federated-op-container" style="margin-top:20px; text-align:center;">
+            <!-- Federated login buttons will be injected by JS -->
+        </div>
+
     </body>
 </html>
diff --git a/gateway-applications/src/main/resources/applications/knoxauth/app/styles/knox.css b/gateway-applications/src/main/resources/applications/knoxauth/app/styles/knox.css
index ba3873571..e2e3d2f81 100644
--- a/gateway-applications/src/main/resources/applications/knoxauth/app/styles/knox.css
+++ b/gateway-applications/src/main/resources/applications/knoxauth/app/styles/knox.css
@@ -1984,4 +1984,71 @@ input[type="radio"], input[type="checkbox"] {margin-top: 0;}
     margin-left: -5px;
     margin-top: -2px;
     font-size: 11px;
+}
+
+.or-separator {
+  display: flex;
+  align-items: center;
+  text-align: center;
+  margin: 20px auto;
+  width: 250px; /* or whatever fits your design */
+  color: #888;
+  font-family: sans-serif;
+  font-size: 14px;
+}
+
+.or-separator::before,
+.or-separator::after {
+  content: "";
+  flex: 1;
+  height: 1px;
+  background: #ccc;
+}
+
+.or-separator::before {
+  margin-right: 8px;
+}
+
+.or-separator::after {
+  margin-left: 8px;
+}
+
+#federated-op-container {
+  margin: 20px auto 0; /* auto left/right centers it */
+  display: flex;
+  flex-direction: column;
+  gap: 12px;
+  width: fit-content; /* shrink to fit content */
+}
+
+.fed-op-btn {
+  display: flex;
+  align-items: center;
+  gap: 12px;
+  padding: 10px 16px;
+  border-radius: 6px;
+  background: #f7f7f7;
+  border: 1px solid #d0d0d0;
+  cursor: pointer;
+  font-family: sans-serif;
+  font-size: 15px;
+  transition: background 0.2s, transform 0.1s;
+  user-select: none;
+}
+
+.fed-op-btn:hover {
+  background: #ececec;
+}
+
+.fed-op-btn:active {
+  transform: scale(0.97);
+}
+
+.fed-op-icon {
+  font-size: 18px;
+}
+
+.fed-op-label {
+  flex: 1;
+  text-align: left;
 }
\ No newline at end of file
diff --git a/gateway-docker/src/main/resources/docker/Dockerfile b/gateway-docker/src/main/resources/docker/Dockerfile
index a4841dbb8..f7926ba9b 100644
--- a/gateway-docker/src/main/resources/docker/Dockerfile
+++ b/gateway-docker/src/main/resources/docker/Dockerfile
@@ -12,7 +12,6 @@
 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 # See the License for the specific language governing permissions and
 # limitations under the License.
-
 FROM dhi.io/eclipse-temurin:17-jdk-debian13-dev AS build
 LABEL maintainer="Apache Knox <de...@knox.apache.org>"
 
diff --git a/gateway-provider-security-jwt/src/main/java/org/apache/knox/gateway/provider/federation/jwt/filter/JWTFederationFilter.java b/gateway-provider-security-jwt/src/main/java/org/apache/knox/gateway/provider/federation/jwt/filter/JWTFederationFilter.java
index 557e81571..378c16a98 100644
--- a/gateway-provider-security-jwt/src/main/java/org/apache/knox/gateway/provider/federation/jwt/filter/JWTFederationFilter.java
+++ b/gateway-provider-security-jwt/src/main/java/org/apache/knox/gateway/provider/federation/jwt/filter/JWTFederationFilter.java
@@ -33,6 +33,7 @@ import org.apache.knox.gateway.util.AuthFilterUtils;
 import org.apache.knox.gateway.util.CertificateUtils;
 import org.apache.knox.gateway.util.CookieUtils;
 import org.apache.knox.gateway.util.ServletRequestUtils;
+import org.apache.knox.gateway.util.knoxidf.KnoxIDFConstants;
 
 import javax.security.auth.Subject;
 import javax.servlet.FilterChain;
@@ -54,10 +55,11 @@ import java.util.Map;
 import java.util.Set;
 
 import static java.nio.charset.StandardCharsets.UTF_8;
-import static org.apache.knox.gateway.security.CommonTokenConstants.GRANT_TYPE;
+import static org.apache.knox.gateway.security.CommonTokenConstants.AUTH_CODE;
 import static org.apache.knox.gateway.security.CommonTokenConstants.CLIENT_CREDENTIALS;
 import static org.apache.knox.gateway.security.CommonTokenConstants.CLIENT_ID;
 import static org.apache.knox.gateway.security.CommonTokenConstants.CLIENT_SECRET;
+import static org.apache.knox.gateway.security.CommonTokenConstants.GRANT_TYPE;
 import static org.apache.knox.gateway.util.AuthFilterUtils.DEFAULT_AUTH_UNAUTHENTICATED_PATHS_PARAM;
 
 public class JWTFederationFilter extends AbstractJWTFilter {
@@ -205,6 +207,7 @@ public class JWTFederationFilter extends AbstractJWTFilter {
           JWT token = parseAndValidateJWT((HttpServletRequest) request, (HttpServletResponse) response, chain, tokenValue);
           if (token != null) {
             Subject subject = createSubjectFromToken(token);
+            addKnoxIDFAttributes(request, token);
             continueWithEstablishedSecurityContext(subject, (HttpServletRequest) request, (HttpServletResponse) response, chain);
           }
         } catch (ParseException | UnknownTokenException ex) {
@@ -226,7 +229,8 @@ public class JWTFederationFilter extends AbstractJWTFilter {
         }
         if (validateToken((HttpServletRequest) request, (HttpServletResponse) response, chain, tokenId, passcode)) {
           try {
-            Subject subject = createSubjectFromTokenIdentifier(tokenId);
+            final Subject subject = createSubjectFromTokenIdentifier(tokenId);
+            request.setAttribute(KnoxIDFConstants.TOKEN_ID_ATTRIBUTE, tokenId);
             continueWithEstablishedSecurityContext(subject, (HttpServletRequest) request, (HttpServletResponse) response, chain);
           } catch (UnknownTokenException e) {
             ((HttpServletResponse) response).sendError(HttpServletResponse.SC_UNAUTHORIZED);
@@ -240,6 +244,14 @@ public class JWTFederationFilter extends AbstractJWTFilter {
     }
   }
 
+  private static void addKnoxIDFAttributes(ServletRequest request, JWT token) {
+    request.setAttribute(KnoxIDFConstants.TOKEN_ID_ATTRIBUTE, TokenUtils.getTokenId(token));
+    final String scope = token.getClaim(KnoxIDFConstants.SCOPE);
+    if (scope != null) {
+      request.setAttribute(KnoxIDFConstants.SCOPE_ATTRIBUTE, token.getClaim(scope));
+    }
+  }
+
   private void validateClientID(HttpServletRequest request, String tokenValue) {
     final String clientID = request.getParameter(CLIENT_ID);
     validateClientID(clientID, tokenValue);
@@ -338,8 +350,8 @@ public class JWTFederationFilter extends AbstractJWTFilter {
         HttpServletRequest unwrappedRequest = ServletRequestUtils.unwrapHttpServletRequest(request);
         final String grantType = unwrappedRequest.getParameter(GRANT_TYPE);
         final String clientAssertionType = unwrappedRequest.getParameter(CLIENT_ASSERTION_TYPE);
-        if (CLIENT_CREDENTIALS.equals(grantType)) {
-          if (clientAssertionType != null && CLIENT_ASSERTION_JWT_BEARER.equals(clientAssertionType)) {
+        if (CLIENT_CREDENTIALS.equals(grantType) || AUTH_CODE.equals(grantType)) {
+          if (CLIENT_ASSERTION_JWT_BEARER.equals(clientAssertionType)) {
             // short lived client assertion token expected
             return getClientTokenFromParams(unwrappedRequest, CLIENT_ASSERTION);
           }
diff --git a/gateway-provider-security-jwt/src/main/java/org/apache/knox/gateway/provider/federation/jwt/filter/SSOCookieFederationFilter.java b/gateway-provider-security-jwt/src/main/java/org/apache/knox/gateway/provider/federation/jwt/filter/SSOCookieFederationFilter.java
index a8e7b8f8d..7558f5a2e 100644
--- a/gateway-provider-security-jwt/src/main/java/org/apache/knox/gateway/provider/federation/jwt/filter/SSOCookieFederationFilter.java
+++ b/gateway-provider-security-jwt/src/main/java/org/apache/knox/gateway/provider/federation/jwt/filter/SSOCookieFederationFilter.java
@@ -31,6 +31,10 @@ import org.apache.knox.gateway.util.AuthFilterUtils;
 import org.apache.knox.gateway.util.CertificateUtils;
 import org.apache.knox.gateway.util.CookieUtils;
 import org.apache.knox.gateway.util.Urls;
+import org.apache.knox.gateway.util.knoxidf.AuthorizeRequestMetadataStore;
+import org.apache.knox.gateway.util.knoxidf.FederatedOpConfiguration;
+import org.apache.knox.gateway.util.knoxidf.FederatedOpConfigurationStore;
+import org.apache.knox.gateway.util.knoxidf.KnoxIDFUtils;
 import org.eclipse.jetty.http.MimeTypes;
 
 import javax.security.auth.Subject;
@@ -45,12 +49,15 @@ import javax.servlet.http.HttpServletResponse;
 import java.io.IOException;
 import java.net.MalformedURLException;
 import java.net.URL;
+import java.net.URLEncoder;
 import java.nio.charset.StandardCharsets;
 import java.text.ParseException;
 import java.util.ArrayList;
+import java.util.Comparator;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
+import java.util.stream.Collectors;
 
 public class SSOCookieFederationFilter extends AbstractJWTFilter {
   private static final JWTMessages LOGGER = MessagesFactory.get( JWTMessages.class );
@@ -103,6 +110,8 @@ public class SSOCookieFederationFilter extends AbstractJWTFilter {
   private boolean shouldUseOriginalUrlFromHeader = DEFAULT_SHOULD_USE_ORIGINAL_URL_FROM_HEADER;
   private boolean verifyOriginalUrlFromHeaderDomain = DEFAULT_VERIFY_ORIGINAL_URL_FROM_HEADER_DOMAIN;
   private final List<String> verifyOriginalUrlFromHeaderDomainWhitelist = new ArrayList<>();
+  private final AuthorizeRequestMetadataStore authorizeRequestMetadataStore = AuthorizeRequestMetadataStore.getInstance(120000L);
+  private final FederatedOpConfigurationStore federatedOpConfigurationStore = FederatedOpConfigurationStore.getInstance(120000L);
   private String originalUrlHeaderName;
 
   @Override
@@ -337,6 +346,22 @@ public class SSOCookieFederationFilter extends AbstractJWTFilter {
       delimiter = "&";
     }
 
+    final Set<FederatedOpConfiguration> enabledFederatedOpConfigs = KnoxIDFUtils.fetchEnabledFederatedOpConfigs(request);
+    if (!enabledFederatedOpConfigs.isEmpty()) {
+      final String loginSessionId = request.getSession().getId();
+      authorizeRequestMetadataStore.put(loginSessionId, KnoxIDFUtils.buildAuthRequestMetadata(request));
+      federatedOpConfigurationStore.put(loginSessionId, enabledFederatedOpConfigs);
+      final List<String> opNames = enabledFederatedOpConfigs.stream()
+              .sorted(Comparator.comparing(FederatedOpConfiguration::getName))
+              .map(FederatedOpConfiguration::getName)
+              .collect(Collectors.toList());
+      providerURL += delimiter
+              + "federatedOpLoginSession=" + URLEncoder.encode(loginSessionId, StandardCharsets.UTF_8)
+              + "&federatedOpNames=" + URLEncoder.encode(String.join(",", opNames), StandardCharsets.UTF_8);
+
+      delimiter = "&";
+    }
+
     if(shouldUseOriginalUrlFromHeader && (request.getHeader(originalUrlHeaderName) != null) && !request.getHeader(originalUrlHeaderName).trim().isEmpty()) {
       final String originalUrlFromHeader = request.getHeader(originalUrlHeaderName);
       LOGGER.usingOriginalUrlFromHeader(originalUrlFromHeader);
diff --git a/gateway-provider-security-jwt/src/test/java/org/apache/knox/gateway/provider/federation/SSOCookieProviderTest.java b/gateway-provider-security-jwt/src/test/java/org/apache/knox/gateway/provider/federation/SSOCookieProviderTest.java
index 90c056f2e..d96b2442a 100644
--- a/gateway-provider-security-jwt/src/test/java/org/apache/knox/gateway/provider/federation/SSOCookieProviderTest.java
+++ b/gateway-provider-security-jwt/src/test/java/org/apache/knox/gateway/provider/federation/SSOCookieProviderTest.java
@@ -17,23 +17,7 @@
  */
 package org.apache.knox.gateway.provider.federation;
 
-import static org.apache.knox.gateway.provider.federation.jwt.filter.SSOCookieFederationFilter.XHR_HEADER;
-import static org.apache.knox.gateway.provider.federation.jwt.filter.SSOCookieFederationFilter.XHR_VALUE;
-import static org.junit.Assert.fail;
-
-import java.nio.charset.StandardCharsets;
-import java.security.Principal;
-import java.time.Instant;
-import java.util.Properties;
-import java.util.Date;
-import java.util.Set;
-import java.util.concurrent.ThreadLocalRandom;
-
-import javax.servlet.ServletException;
-import javax.servlet.http.Cookie;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-
+import com.nimbusds.jwt.SignedJWT;
 import org.apache.knox.gateway.provider.federation.jwt.filter.AbstractJWTFilter;
 import org.apache.knox.gateway.provider.federation.jwt.filter.SSOCookieFederationFilter;
 import org.apache.knox.gateway.security.PrimaryPrincipal;
@@ -44,11 +28,25 @@ import org.easymock.EasyMock;
 import org.junit.Assert;
 import org.junit.Before;
 import org.junit.Test;
-
-import com.nimbusds.jwt.SignedJWT;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import javax.servlet.ServletException;
+import javax.servlet.http.Cookie;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.nio.charset.StandardCharsets;
+import java.security.Principal;
+import java.time.Instant;
+import java.util.Date;
+import java.util.Properties;
+import java.util.Set;
+import java.util.concurrent.ThreadLocalRandom;
+
+import static org.apache.knox.gateway.provider.federation.jwt.filter.SSOCookieFederationFilter.XHR_HEADER;
+import static org.apache.knox.gateway.provider.federation.jwt.filter.SSOCookieFederationFilter.XHR_VALUE;
+import static org.junit.Assert.fail;
+
 public class SSOCookieProviderTest extends AbstractJWTFilterTest {
   private static final Logger LOGGER = LoggerFactory.getLogger(SSOCookieProviderTest.class);
 
diff --git a/gateway-provider-security-shiro/src/main/java/org/apache/knox/gateway/filter/RedirectToUrlFilter.java b/gateway-provider-security-shiro/src/main/java/org/apache/knox/gateway/filter/RedirectToUrlFilter.java
index f0f14b365..7501dbe4b 100644
--- a/gateway-provider-security-shiro/src/main/java/org/apache/knox/gateway/filter/RedirectToUrlFilter.java
+++ b/gateway-provider-security-shiro/src/main/java/org/apache/knox/gateway/filter/RedirectToUrlFilter.java
@@ -47,7 +47,7 @@ public class RedirectToUrlFilter extends AbstractGatewayFilter {
 
   @Override
   protected void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
-    if (redirectUrl != null && request.getHeader("Authorization") == null) {
+    if (redirectUrl != null && request.getHeader("Authorization") == null && request.getParameter("fedOpSid") == null) {
       response.sendRedirect(redirectUrl + getOriginalQueryString(request));
     }
     chain.doFilter(request, response);
diff --git a/gateway-release/home/conf/topologies/knoxsso.xml b/gateway-release/home/conf/topologies/knoxsso.xml
index 99600f874..cfee258c3 100644
--- a/gateway-release/home/conf/topologies/knoxsso.xml
+++ b/gateway-release/home/conf/topologies/knoxsso.xml
@@ -73,6 +73,10 @@
                 <name>main.ldapRealm.contextFactory.authenticationMechanism</name>
                 <value>simple</value>
             </param>
+            <param>
+                <name>urls./api/v1/websso/federated/op</name>
+                <value>anon</value>
+            </param>
             <param>
                 <name>urls./**</name>
                 <value>authcBasic</value>
diff --git a/gateway-release/home/conf/users.ldif b/gateway-release/home/conf/users.ldif
index 4f1c6a955..999f82403 100644
--- a/gateway-release/home/conf/users.ldif
+++ b/gateway-release/home/conf/users.ldif
@@ -39,7 +39,9 @@ objectclass:organizationalPerson
 objectclass:inetOrgPerson
 cn: Guest
 sn: User
+givenName: Guest
 uid: guest
+mail: guest@example.org
 userPassword:guest-password
 
 # entry for sample user admin
@@ -48,9 +50,11 @@ objectclass:top
 objectclass:person
 objectclass:organizationalPerson
 objectclass:inetOrgPerson
-cn: Admin
-sn: Admin
+cn: System Administrator
+sn: Administrator
+givenName: System
 uid: admin
+mail: admin@example.org
 userPassword:admin-password
 
 # entry for sample user sam
@@ -59,9 +63,11 @@ objectclass:top
 objectclass:person
 objectclass:organizationalPerson
 objectclass:inetOrgPerson
-cn: sam
-sn: sam
+cn: Sam Peterson
+sn: Peterson
+givenName: Sam
 uid: sam
+mail: sam@example.org
 userPassword:sam-password
 
 # entry for sample user tom
@@ -70,9 +76,11 @@ objectclass:top
 objectclass:person
 objectclass:organizationalPerson
 objectclass:inetOrgPerson
-cn: tom
-sn: tom
+cn: Tom Richards
+sn: Richards
+givenName: Tom
 uid: tom
+mail: tom@example.org
 userPassword:tom-password
 
 # create FIRST Level groups branch
diff --git a/gateway-release/pom.xml b/gateway-release/pom.xml
index 7df1c6c22..00566f9d7 100644
--- a/gateway-release/pom.xml
+++ b/gateway-release/pom.xml
@@ -524,5 +524,9 @@
             <groupId>org.apache.knox</groupId>
             <artifactId>gateway-service-restcatalog</artifactId>
         </dependency>
+        <dependency>
+            <groupId>org.apache.knox</groupId>
+            <artifactId>gateway-service-knoxidf</artifactId>
+        </dependency>
     </dependencies>
 </project>
diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/UrlEncodedFormRequest.java b/gateway-server/src/main/java/org/apache/knox/gateway/UrlEncodedFormRequest.java
index 2e2482aa1..139e51862 100644
--- a/gateway-server/src/main/java/org/apache/knox/gateway/UrlEncodedFormRequest.java
+++ b/gateway-server/src/main/java/org/apache/knox/gateway/UrlEncodedFormRequest.java
@@ -17,17 +17,17 @@
  */
 package org.apache.knox.gateway;
 
+import org.apache.knox.gateway.i18n.messages.MessagesFactory;
+import org.eclipse.jetty.util.MultiMap;
+import org.eclipse.jetty.util.UrlEncoded;
+
+import javax.servlet.ServletRequest;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletRequestWrapper;
 import java.io.IOException;
 import java.util.Enumeration;
 import java.util.Iterator;
 import java.util.Map;
-import javax.servlet.ServletRequest;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletRequestWrapper;
-
-import org.apache.knox.gateway.i18n.messages.MessagesFactory;
-import org.eclipse.jetty.util.MultiMap;
-import org.eclipse.jetty.util.UrlEncoded;
 
 /**
  * HttpServletRequest
diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/database/AbstractDataSourceFactory.java b/gateway-server/src/main/java/org/apache/knox/gateway/database/AbstractDataSourceFactory.java
index 7a7afadd1..a9d544fe8 100644
--- a/gateway-server/src/main/java/org/apache/knox/gateway/database/AbstractDataSourceFactory.java
+++ b/gateway-server/src/main/java/org/apache/knox/gateway/database/AbstractDataSourceFactory.java
@@ -42,6 +42,14 @@ public abstract class AbstractDataSourceFactory {
     public static final String DERBY_KNOX_PROVIDERS_TABLE_CREATE_SQL_FILE_NAME = "createKnoxProvidersTableDerby.sql";
     public static final String DERBY_KNOX_DESCRIPTORS_TABLE_CREATE_SQL_FILE_NAME = "createKnoxDescriptorsTableDerby.sql";
 
+    //KNOXIDF
+    public static final String KNOXIDF_FED_IDENTITY_TABLE_CREATE_SQL_FILE_NAME = "createKnoxIDFFederatedIdentityTable.sql";
+    public static final String KNOXIDF_FED_IDENTITY_ATTR_TABLE_CREATE_SQL_FILE_NAME = "createKnoxIDFFederatedIdentityAttributesTable.sql";
+    public static final String ORACLE_KNOXIDF_FED_IDENTITY_TABLE_CREATE_SQL_FILE_NAME = "createKnoxIDFFederatedIdentityTableOracle.sql";
+    public static final String ORACLE_KNOXIDF_FED_IDENTITY_ATTR_TABLE_CREATE_SQL_FILE_NAME = "createKnoxIDFFederatedIdentityAttributesTableOracle.sql";
+    public static final String DERBY_KNOXIDF_FED_IDENTITY_TABLE_CREATE_SQL_FILE_NAME = "createKnoxIDFFederatedIdentityTableDerby.sql";
+    public static final String DERBY_KNOXIDF_FED_IDENTITY_ATTR_TABLE_CREATE_SQL_FILE_NAME = "createKnoxIDFFederatedIdentityAttributesTableDerby.sql";
+
     public static final String DATABASE_USER_ALIAS_NAME = "gateway_database_user";
     public static final String DATABASE_PASSWORD_ALIAS_NAME = "gateway_database_password";
     public static final String DATABASE_TRUSTSTORE_PASSWORD_ALIAS_NAME = "gateway_database_ssl_truststore_password";
diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/database/DatabaseType.java b/gateway-server/src/main/java/org/apache/knox/gateway/database/DatabaseType.java
index 200987278..3f627bdac 100644
--- a/gateway-server/src/main/java/org/apache/knox/gateway/database/DatabaseType.java
+++ b/gateway-server/src/main/java/org/apache/knox/gateway/database/DatabaseType.java
@@ -22,37 +22,50 @@ public enum DatabaseType {
             AbstractDataSourceFactory.POSTGRES_TOKENS_TABLE_CREATE_SQL_FILE_NAME,
             AbstractDataSourceFactory.POSTGRES_TOKEN_METADATA_TABLE_CREATE_SQL_FILE_NAME,
             AbstractDataSourceFactory.KNOX_PROVIDERS_TABLE_CREATE_SQL_FILE_NAME,
-            AbstractDataSourceFactory.KNOX_DESCRIPTORS_TABLE_CREATE_SQL_FILE_NAME
+            AbstractDataSourceFactory.KNOX_DESCRIPTORS_TABLE_CREATE_SQL_FILE_NAME,
+            AbstractDataSourceFactory.KNOXIDF_FED_IDENTITY_TABLE_CREATE_SQL_FILE_NAME,
+            AbstractDataSourceFactory.KNOXIDF_FED_IDENTITY_ATTR_TABLE_CREATE_SQL_FILE_NAME
     ),
     MYSQL("mysql",
             AbstractDataSourceFactory.TOKENS_TABLE_CREATE_SQL_FILE_NAME,
             AbstractDataSourceFactory.TOKEN_METADATA_TABLE_CREATE_SQL_FILE_NAME,
             AbstractDataSourceFactory.KNOX_PROVIDERS_TABLE_CREATE_SQL_FILE_NAME,
-            AbstractDataSourceFactory.KNOX_DESCRIPTORS_TABLE_CREATE_SQL_FILE_NAME
+            AbstractDataSourceFactory.KNOX_DESCRIPTORS_TABLE_CREATE_SQL_FILE_NAME,
+            AbstractDataSourceFactory.KNOXIDF_FED_IDENTITY_TABLE_CREATE_SQL_FILE_NAME,
+            AbstractDataSourceFactory.KNOXIDF_FED_IDENTITY_ATTR_TABLE_CREATE_SQL_FILE_NAME
     ),
     MARIADB("mariadb",
             AbstractDataSourceFactory.TOKENS_TABLE_CREATE_SQL_FILE_NAME,
             AbstractDataSourceFactory.TOKEN_METADATA_TABLE_CREATE_SQL_FILE_NAME,
             AbstractDataSourceFactory.KNOX_PROVIDERS_TABLE_CREATE_SQL_FILE_NAME,
-            AbstractDataSourceFactory.KNOX_DESCRIPTORS_TABLE_CREATE_SQL_FILE_NAME
+            AbstractDataSourceFactory.KNOX_DESCRIPTORS_TABLE_CREATE_SQL_FILE_NAME,
+            AbstractDataSourceFactory.KNOXIDF_FED_IDENTITY_TABLE_CREATE_SQL_FILE_NAME,
+            AbstractDataSourceFactory.KNOXIDF_FED_IDENTITY_ATTR_TABLE_CREATE_SQL_FILE_NAME
     ),
     HSQL("hsql",
             AbstractDataSourceFactory.TOKENS_TABLE_CREATE_SQL_FILE_NAME,
             AbstractDataSourceFactory.TOKEN_METADATA_TABLE_CREATE_SQL_FILE_NAME,
             AbstractDataSourceFactory.KNOX_PROVIDERS_TABLE_CREATE_SQL_FILE_NAME,
-            AbstractDataSourceFactory.KNOX_DESCRIPTORS_TABLE_CREATE_SQL_FILE_NAME
+            AbstractDataSourceFactory.KNOX_DESCRIPTORS_TABLE_CREATE_SQL_FILE_NAME,
+            AbstractDataSourceFactory.KNOXIDF_FED_IDENTITY_TABLE_CREATE_SQL_FILE_NAME,
+            AbstractDataSourceFactory.KNOXIDF_FED_IDENTITY_ATTR_TABLE_CREATE_SQL_FILE_NAME
     ),
     DERBY("derbydb",
             AbstractDataSourceFactory.DERBY_TOKENS_TABLE_CREATE_SQL_FILE_NAME,
             AbstractDataSourceFactory.DERBY_TOKEN_METADATA_TABLE_CREATE_SQL_FILE_NAME,
             AbstractDataSourceFactory.DERBY_KNOX_PROVIDERS_TABLE_CREATE_SQL_FILE_NAME,
-            AbstractDataSourceFactory.DERBY_KNOX_DESCRIPTORS_TABLE_CREATE_SQL_FILE_NAME
+            AbstractDataSourceFactory.DERBY_KNOX_DESCRIPTORS_TABLE_CREATE_SQL_FILE_NAME,
+            AbstractDataSourceFactory.DERBY_KNOXIDF_FED_IDENTITY_TABLE_CREATE_SQL_FILE_NAME,
+            AbstractDataSourceFactory.DERBY_KNOXIDF_FED_IDENTITY_ATTR_TABLE_CREATE_SQL_FILE_NAME
+
     ),
     ORACLE("oracle",
             AbstractDataSourceFactory.ORACLE_TOKENS_TABLE_CREATE_SQL_FILE_NAME,
             AbstractDataSourceFactory.ORACLE_TOKEN_METADATA_TABLE_CREATE_SQL_FILE_NAME,
             AbstractDataSourceFactory.ORACLE_KNOX_PROVIDERS_TABLE_CREATE_SQL_FILE_NAME,
-            AbstractDataSourceFactory.ORACLE_KNOX_DESCRIPTORS_TABLE_CREATE_SQL_FILE_NAME
+            AbstractDataSourceFactory.ORACLE_KNOX_DESCRIPTORS_TABLE_CREATE_SQL_FILE_NAME,
+            AbstractDataSourceFactory.ORACLE_KNOXIDF_FED_IDENTITY_TABLE_CREATE_SQL_FILE_NAME,
+            AbstractDataSourceFactory.ORACLE_KNOXIDF_FED_IDENTITY_ATTR_TABLE_CREATE_SQL_FILE_NAME
     );
 
     private final String type;
@@ -60,13 +73,17 @@ public enum DatabaseType {
     private final String metadataTableSql;
     private final String providersTableSql;
     private final String descriptorsTableSql;
+    private final String federatedIdentityTableSql;
+    private final String federatedIdentityAttrTableSql;
 
-    DatabaseType(String type, String tokensTableSql, String metadataTableSql,  String providersTableSql, String descriptorsTableSql) {
+    DatabaseType(String type, String tokensTableSql, String metadataTableSql, String providersTableSql, String descriptorsTableSql, String federatedIdentityTableSql, String federatedIdentityAttrTableSql) {
         this.type = type;
         this.tokensTableSql = tokensTableSql;
         this.metadataTableSql = metadataTableSql;
         this.providersTableSql = providersTableSql;
         this.descriptorsTableSql = descriptorsTableSql;
+        this.federatedIdentityTableSql = federatedIdentityTableSql;
+        this.federatedIdentityAttrTableSql = federatedIdentityAttrTableSql;
     }
 
     public String type() {
@@ -89,6 +106,14 @@ public enum DatabaseType {
         return descriptorsTableSql;
     }
 
+    public String federatedIdentityTableSql() {
+        return federatedIdentityTableSql;
+    }
+
+    public String federatedIdentityAttrTableSql() {
+        return federatedIdentityAttrTableSql;
+    }
+
     public static DatabaseType fromString(String dbType) {
         for (DatabaseType dt : values()) {
             if (dt.type.equalsIgnoreCase(dbType)) {
diff --git a/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/TokenMetadataType.java b/gateway-server/src/main/java/org/apache/knox/gateway/database/KnoxDatabase.java
similarity index 56%
copy from gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/TokenMetadataType.java
copy to gateway-server/src/main/java/org/apache/knox/gateway/database/KnoxDatabase.java
index 17e82e0af..261b36899 100644
--- a/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/TokenMetadataType.java
+++ b/gateway-server/src/main/java/org/apache/knox/gateway/database/KnoxDatabase.java
@@ -5,19 +5,32 @@
  * licenses this file to you under the Apache License, Version 2.0 (the
  * "License"); you may not use this file except in compliance with the License.
  * You may obtain a copy of the License at
- *
+ * <p>
  * http://www.apache.org/licenses/LICENSE-2.0
- *
+ * <p>
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
  * License for the specific language governing permissions and limitations under
  * the License.
  */
-package org.apache.knox.gateway.services.security.token;
+package org.apache.knox.gateway.database;
 
-public enum TokenMetadataType {
+import org.apache.knox.gateway.services.token.impl.TokenStateDatabase;
 
-  JWT, KNOXSSO_COOKIE, CLIENT_ID, API_KEY;
+import javax.sql.DataSource;
 
+public class KnoxDatabase {
+
+    protected final DataSource dataSource;
+
+    public KnoxDatabase(DataSource dataSource) {
+        this.dataSource = dataSource;
+    }
+
+    protected void createTableIfNotExists(String tableName, String createSqlFileName) throws Exception {
+        if (!JDBCUtils.tableExists(tableName, dataSource)) {
+            JDBCUtils.createTableFromSQL(createSqlFileName, dataSource, TokenStateDatabase.class.getClassLoader());
+        }
+    }
 }
diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/deploy/DeploymentFactory.java b/gateway-server/src/main/java/org/apache/knox/gateway/deploy/DeploymentFactory.java
index dfe4a4ea9..564f79300 100644
--- a/gateway-server/src/main/java/org/apache/knox/gateway/deploy/DeploymentFactory.java
+++ b/gateway-server/src/main/java/org/apache/knox/gateway/deploy/DeploymentFactory.java
@@ -376,6 +376,13 @@ public abstract class DeploymentFactory {
       GatewayConfig gatewayConfig) {
     WebAppDescriptor wad = context.getWebAppDescriptor();
     String topoName = context.getTopology().getName();
+
+    final boolean hasKnoxIdf = services!= null && services.entrySet().stream().anyMatch( e -> e.getKey().equalsIgnoreCase("KNOXIDF") );
+    if (hasKnoxIdf) {
+      wad.createServlet().servletName("auth-consent-redirect").servletClass("org.apache.knox.gateway.service.knoxidf.AuthConsentServlet");
+      wad.createServletMapping().servletName("auth-consent-redirect").urlPattern("/authConsent");
+    }
+
     boolean asyncSupported = gatewayConfig.isAsyncSupported() || gatewayConfig.isTopologyAsyncSupported(topoName);
     if( applications == null ) {
       String servletName = topoName + SERVLET_NAME_SUFFIX;
diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/services/DefaultGatewayServices.java b/gateway-server/src/main/java/org/apache/knox/gateway/services/DefaultGatewayServices.java
index a38026f9a..2d7d12f13 100644
--- a/gateway-server/src/main/java/org/apache/knox/gateway/services/DefaultGatewayServices.java
+++ b/gateway-server/src/main/java/org/apache/knox/gateway/services/DefaultGatewayServices.java
@@ -86,6 +86,8 @@ public class DefaultGatewayServices extends AbstractGatewayServices {
     addService(ServiceType.LDAP_ROLES_LOOKUP_SERVICE,  gatewayServiceFactory.create(this, ServiceType.LDAP_ROLES_LOOKUP_SERVICE, config, options));
 
     addService(ServiceType.LDAP_SERVICE, gatewayServiceFactory.create(this, ServiceType.LDAP_SERVICE, config, options));
+
+    addService(ServiceType.KNOXIDF_FEDERATED_IDENTITY_SERVICE, gatewayServiceFactory.create(this, ServiceType.KNOXIDF_FEDERATED_IDENTITY_SERVICE, config, options));
   }
 
   @Override
diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/services/factory/FederatedIdentityServiceFactory.java b/gateway-server/src/main/java/org/apache/knox/gateway/services/factory/FederatedIdentityServiceFactory.java
new file mode 100644
index 000000000..f78f96af6
--- /dev/null
+++ b/gateway-server/src/main/java/org/apache/knox/gateway/services/factory/FederatedIdentityServiceFactory.java
@@ -0,0 +1,97 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. The ASF
+ * licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * <p>
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * <p>
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package org.apache.knox.gateway.services.factory;
+
+import org.apache.knox.gateway.GatewayMessages;
+import org.apache.knox.gateway.config.GatewayConfig;
+import org.apache.knox.gateway.i18n.messages.MessagesFactory;
+import org.apache.knox.gateway.services.GatewayServices;
+import org.apache.knox.gateway.services.Service;
+import org.apache.knox.gateway.services.ServiceLifecycleException;
+import org.apache.knox.gateway.services.ServiceType;
+import org.apache.knox.gateway.services.knoxidf.federation.EmptyFederatedIdentitityService;
+import org.apache.knox.gateway.services.knoxidf.federation.FederatedIdentityService;
+import org.apache.knox.gateway.services.knoxidf.federation.JdbcFederatedIdentityService;
+import org.apache.knox.gateway.services.topology.TopologyService;
+import org.apache.knox.gateway.topology.Topology;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+
+public class FederatedIdentityServiceFactory extends AbstractServiceFactory {
+
+    private static final GatewayMessages LOG = MessagesFactory.get(GatewayMessages.class);
+    private static final String DEFAULT_IMPLEMENTATION = EmptyFederatedIdentitityService.class.getName();
+
+    @Override
+    protected Service createService(GatewayServices gatewayServices, ServiceType serviceType, GatewayConfig gatewayConfig, Map<String, String> options, String implementation)
+            throws ServiceLifecycleException {
+
+        String implementationToUse = implementation;
+        // If implementation is empty, check if we should auto-enable JdbcFederatedIdentityService
+        if (isEmptyDefaultImplementation(implementationToUse)) {
+            if (isKnoxIdfEnabledInAnyTopology(gatewayServices)) {
+                implementationToUse = JdbcFederatedIdentityService.class.getName();
+            }
+        }
+
+        FederatedIdentityService service = null;
+        if (shouldCreateService(implementationToUse)) {
+            if (matchesImplementation(implementationToUse, EmptyFederatedIdentitityService.class, true)) {
+                service = new EmptyFederatedIdentitityService();
+            } else if (matchesImplementation(implementationToUse, JdbcFederatedIdentityService.class)) {
+                try {
+                    try {
+                        service = new JdbcFederatedIdentityService();
+                        ((JdbcFederatedIdentityService) service).setAliasService(getAliasService(gatewayServices));
+                        service.init(gatewayConfig, options);
+                    } catch (ServiceLifecycleException e) {
+                        LOG.errorInitializingService(implementationToUse, e.getMessage(), e);
+                        service =  new EmptyFederatedIdentitityService();
+                    }
+                } catch (Exception e) {
+                    throw new ServiceLifecycleException("Error while creating Federated Identity Service: " + e, e);
+                }
+            }
+            logServiceUsage(service.getClass().getName(), serviceType);
+        }
+        return service;
+    }
+
+    private boolean isKnoxIdfEnabledInAnyTopology(GatewayServices gatewayServices) {
+        final TopologyService topologyService = gatewayServices.getService(ServiceType.TOPOLOGY_SERVICE);
+        if (topologyService != null) {
+            for (Topology topology : topologyService.getTopologies()) {
+                if (topology.getServices().stream().anyMatch(service -> "KNOXIDF".equals(service.getRole()))) {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
+    @Override
+    protected ServiceType getServiceType() {
+        return ServiceType.KNOXIDF_FEDERATED_IDENTITY_SERVICE;
+    }
+
+    @Override
+    protected Collection<String> getKnownImplementations() {
+        return List.of(DEFAULT_IMPLEMENTATION, JdbcFederatedIdentityService.class.getName());
+    }
+}
diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/services/knoxidf/federation/EmptyFederatedIdentitityService.java b/gateway-server/src/main/java/org/apache/knox/gateway/services/knoxidf/federation/EmptyFederatedIdentitityService.java
new file mode 100644
index 000000000..8ce9e550e
--- /dev/null
+++ b/gateway-server/src/main/java/org/apache/knox/gateway/services/knoxidf/federation/EmptyFederatedIdentitityService.java
@@ -0,0 +1,51 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. The ASF
+ * licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * <p>
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * <p>
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package org.apache.knox.gateway.services.knoxidf.federation;
+
+import org.apache.knox.gateway.config.GatewayConfig;
+import org.apache.knox.gateway.services.ServiceLifecycleException;
+
+import java.util.Map;
+import java.util.Optional;
+
+public class EmptyFederatedIdentitityService implements FederatedIdentityService {
+    @Override
+    public void addFederatedIdentity(FederatedIdentity identity) {
+    }
+
+    @Override
+    public Optional<FederatedIdentity> findById(String identityId) {
+        return Optional.empty();
+    }
+
+    @Override
+    public Optional<FederatedIdentity> findByProviderAndSubject(String provider, String externalIssuer, String externalSubject) {
+        return Optional.empty();
+    }
+
+    @Override
+    public void init(GatewayConfig config, Map<String, String> options) throws ServiceLifecycleException {
+    }
+
+    @Override
+    public void start() throws ServiceLifecycleException {
+    }
+
+    @Override
+    public void stop() throws ServiceLifecycleException {
+    }
+}
diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/services/knoxidf/federation/FederatedIdentityDatabase.java b/gateway-server/src/main/java/org/apache/knox/gateway/services/knoxidf/federation/FederatedIdentityDatabase.java
new file mode 100644
index 000000000..649bf3038
--- /dev/null
+++ b/gateway-server/src/main/java/org/apache/knox/gateway/services/knoxidf/federation/FederatedIdentityDatabase.java
@@ -0,0 +1,132 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. The ASF
+ * licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * <p>
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * <p>
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package org.apache.knox.gateway.services.knoxidf.federation;
+
+import org.apache.knox.gateway.database.DatabaseType;
+import org.apache.knox.gateway.database.KnoxDatabase;
+
+import javax.sql.DataSource;
+import java.sql.Connection;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Timestamp;
+import java.util.HashMap;
+import java.util.Optional;
+
+class FederatedIdentityDatabase extends KnoxDatabase {
+    private static final String FEDERATED_IDENTITY_TABLE_NAME = "federated_identity";
+    private static final String FEDERATED_IDENTITY_ATTRIBUTES_TABLE_NAME = "federated_identity_attr";
+    private static final String ADD_FEDERATED_IDENTITY_SQL = "INSERT INTO " + FEDERATED_IDENTITY_TABLE_NAME
+            + " (id, user_id, provider, external_subject, external_issuer, created_at) VALUES (?, ?, ?, ?, ?, ?)";
+    private static final String ADD_FEDERATED_IDENTITY_ATTR_SQL = "INSERT INTO " + FEDERATED_IDENTITY_ATTRIBUTES_TABLE_NAME +
+            " (identity_id, attr_key, attr_value) VALUES (?, ?, ?)";
+    private static final String FETCH_FEDERATED_IDENTITY_BY_PROV_ISS_SUB_SQL = "SELECT * FROM " + FEDERATED_IDENTITY_TABLE_NAME +
+            " WHERE provider = ? AND external_issuer = ? AND external_subject = ?";
+    private static final String FETCH_FEDERATED_IDENTITY_SQL_BY_ID = "SELECT id, user_id, provider, external_subject, external_issuer, created_at FROM "
+            + FEDERATED_IDENTITY_TABLE_NAME + " WHERE id = ?";
+    private static final String FETCH_FEDERATED_IDENTITY_ATTR_SQL = "SELECT attr_key, attr_value FROM " + FEDERATED_IDENTITY_ATTRIBUTES_TABLE_NAME + " WHERE identity_id = ?";
+
+    FederatedIdentityDatabase(DataSource dataSource, String dbType) throws Exception {
+        super(dataSource);
+        DatabaseType databaseType = DatabaseType.fromString(dbType);
+        createTableIfNotExists(FEDERATED_IDENTITY_TABLE_NAME, databaseType.federatedIdentityTableSql());
+        createTableIfNotExists(FEDERATED_IDENTITY_ATTRIBUTES_TABLE_NAME, databaseType.federatedIdentityAttrTableSql());
+    }
+
+    void addFederatedIdentity(FederatedIdentity identity) throws SQLException {
+        // save core metadata first
+        try (Connection connection = dataSource.getConnection(); PreparedStatement addFederatedIdentityStatement = connection.prepareStatement(ADD_FEDERATED_IDENTITY_SQL)) {
+            addFederatedIdentityStatement.setString(1, identity.getId());
+            addFederatedIdentityStatement.setString(2, identity.getUserId());
+            addFederatedIdentityStatement.setString(3, identity.getProvider());
+            addFederatedIdentityStatement.setString(4, identity.getExternalSubject());
+            addFederatedIdentityStatement.setString(5, identity.getExternalIssuer());
+            addFederatedIdentityStatement.setTimestamp(6, Timestamp.from(identity.getCreatedAt()));
+            addFederatedIdentityStatement.executeUpdate();
+        }
+
+        // save attributes
+        try (Connection connection = dataSource.getConnection(); PreparedStatement addFederatedIdentityAttrStatement = connection.prepareStatement(ADD_FEDERATED_IDENTITY_ATTR_SQL)) {
+            for (var attribute : identity.getAttributes().entrySet()) {
+                addFederatedIdentityAttrStatement.setString(1, identity.getId());
+                addFederatedIdentityAttrStatement.setString(2, attribute.getKey());
+                addFederatedIdentityAttrStatement.setString(3, attribute.getValue());
+                addFederatedIdentityAttrStatement.addBatch();
+            }
+            addFederatedIdentityAttrStatement.executeBatch();
+        }
+    }
+
+
+    Optional<FederatedIdentity> findByProviderAndSubject(String provider, String issuer, String subject) throws SQLException {
+        FederatedIdentity federatedIdentity = null;
+        try (Connection connection = dataSource.getConnection(); PreparedStatement getFederatedIdentityStatement = connection.prepareStatement(FETCH_FEDERATED_IDENTITY_BY_PROV_ISS_SUB_SQL)) {
+            getFederatedIdentityStatement.setString(1, provider);
+            getFederatedIdentityStatement.setString(2, issuer);
+            getFederatedIdentityStatement.setString(3, subject);
+            try (ResultSet rs = getFederatedIdentityStatement.executeQuery()) {
+                if (rs.next()) {
+                    federatedIdentity = new FederatedIdentity(
+                            rs.getString("id"),
+                            rs.getString("user_id"),
+                            provider,
+                            subject,
+                            issuer,
+                            rs.getTimestamp("created_at").toInstant(), new HashMap<>());
+                } else {
+                    return Optional.empty();
+                }
+            }
+        }
+        populateAttributes(federatedIdentity);
+        return Optional.of(federatedIdentity);
+    }
+
+    Optional<FederatedIdentity> findById(String id) throws SQLException {
+        FederatedIdentity federatedIdentity = null;
+        try (Connection connection = dataSource.getConnection(); PreparedStatement getFederatedIdentityStatement = connection.prepareStatement(FETCH_FEDERATED_IDENTITY_SQL_BY_ID)) {
+            getFederatedIdentityStatement.setString(1, id);
+            try (ResultSet rs = getFederatedIdentityStatement.executeQuery()) {
+                if (rs.next()) {
+                    federatedIdentity = new FederatedIdentity(
+                            id,
+                            rs.getString("user_id"),
+                            rs.getString("provider"),
+                            rs.getString("external_subject"),
+                            rs.getString("external_issuer"),
+                            rs.getTimestamp("created_at").toInstant(), new HashMap<>());
+                } else {
+                    return Optional.empty();
+                }
+            }
+        }
+        populateAttributes(federatedIdentity);
+        return Optional.of(federatedIdentity);
+    }
+
+    private void populateAttributes(FederatedIdentity federatedIdentity) throws SQLException {
+        try (Connection connection = dataSource.getConnection(); PreparedStatement getFederatedIdentityAttrStatement = connection.prepareStatement(FETCH_FEDERATED_IDENTITY_ATTR_SQL)) {
+            getFederatedIdentityAttrStatement.setString(1, federatedIdentity.getId());
+            try (ResultSet rs = getFederatedIdentityAttrStatement.executeQuery()) {
+                while (rs.next()) {
+                    federatedIdentity.getAttributes().put(rs.getString(1), rs.getString(2));
+                }
+            }
+        }
+    }
+}
diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/services/knoxidf/federation/FederatedIdentityServiceMessages.java b/gateway-server/src/main/java/org/apache/knox/gateway/services/knoxidf/federation/FederatedIdentityServiceMessages.java
new file mode 100644
index 000000000..d6f820a1d
--- /dev/null
+++ b/gateway-server/src/main/java/org/apache/knox/gateway/services/knoxidf/federation/FederatedIdentityServiceMessages.java
@@ -0,0 +1,35 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. The ASF
+ * licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * <p>
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * <p>
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package org.apache.knox.gateway.services.knoxidf.federation;
+
+import org.apache.knox.gateway.i18n.messages.Message;
+import org.apache.knox.gateway.i18n.messages.MessageLevel;
+import org.apache.knox.gateway.i18n.messages.Messages;
+import org.apache.knox.gateway.i18n.messages.StackTrace;
+
+@Messages(logger="org.apache.knox.gateway.knoxidf.federated.identity.service")
+public interface FederatedIdentityServiceMessages {
+
+    @Message(level = MessageLevel.ERROR, text = "An error occurred while saving federated identity {0} in the database : {1}")
+    void errorSavingFederatedIdentityInDatabase(String federatedIdentityId, String errorMessage, @StackTrace(level = MessageLevel.DEBUG) Exception e);
+
+    @Message(level = MessageLevel.ERROR, text = "An error occurred while fetching federated identity ({0} / {1} / {2}) from the database : {3}")
+    void errorFetchingFederatedIdentityFromDatabase(String provider, String issuer, String subject, String errorMessage, @StackTrace(level = MessageLevel.DEBUG) Exception e);
+
+    @Message(level = MessageLevel.ERROR, text = "An error occurred while fetching federated identity ({0}) from the database : {1}")
+    void errorFetchingFederatedIdentityFromDatabase(String id, String errorMessage, @StackTrace(level = MessageLevel.DEBUG) Exception e);
+}
diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/services/knoxidf/federation/JdbcFederatedIdentityService.java b/gateway-server/src/main/java/org/apache/knox/gateway/services/knoxidf/federation/JdbcFederatedIdentityService.java
new file mode 100644
index 000000000..8e26bdc1d
--- /dev/null
+++ b/gateway-server/src/main/java/org/apache/knox/gateway/services/knoxidf/federation/JdbcFederatedIdentityService.java
@@ -0,0 +1,108 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. The ASF
+ * licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * <p>
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * <p>
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package org.apache.knox.gateway.services.knoxidf.federation;
+
+import org.apache.knox.gateway.config.GatewayConfig;
+import org.apache.knox.gateway.database.DataSourceProvider;
+import org.apache.knox.gateway.i18n.messages.MessagesFactory;
+import org.apache.knox.gateway.services.ServiceLifecycleException;
+import org.apache.knox.gateway.services.security.AliasService;
+
+import java.sql.SQLException;
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
+
+public class JdbcFederatedIdentityService implements FederatedIdentityService {
+    private static final FederatedIdentityServiceMessages LOG = MessagesFactory.get(FederatedIdentityServiceMessages.class);
+
+    private final AtomicBoolean initialized = new AtomicBoolean(false);
+    private final Lock initLock = new ReentrantLock(true);
+    private AliasService aliasService; // connection username/pw are stored here
+    private FederatedIdentityDatabase federatedIdentityDatabase;
+
+    @Override
+    public void init(GatewayConfig config, Map<String, String> options) throws ServiceLifecycleException {
+        if (!initialized.get()) {
+            initLock.lock();
+            try {
+                if (aliasService == null) {
+                    throw new ServiceLifecycleException("The required AliasService reference has not been set.");
+                }
+                try {
+                    this.federatedIdentityDatabase = new FederatedIdentityDatabase(DataSourceProvider.getDataSource(config, aliasService), config.getDatabaseType());
+                    initialized.set(true);
+                } catch (Exception e) {
+                    throw new ServiceLifecycleException("Error while initiating JDBCTokenStateService: " + e, e);
+                }
+            } finally {
+                initLock.unlock();
+            }
+        }
+    }
+
+    @Override
+    public void start() throws ServiceLifecycleException {
+    }
+
+    @Override
+    public void stop() throws ServiceLifecycleException {
+    }
+
+    public void setAliasService(AliasService aliasService) {
+        this.aliasService = aliasService;
+    }
+
+    protected AliasService getAliasService() {
+        return aliasService;
+    }
+
+    @Override
+    public void addFederatedIdentity(FederatedIdentity identity) {
+        try {
+            if (findByProviderAndSubject(identity.getProvider(), identity.getExternalIssuer(), identity.getExternalSubject()).isEmpty()) {
+                federatedIdentityDatabase.addFederatedIdentity(identity);
+            }
+        } catch (SQLException e) {
+            LOG.errorSavingFederatedIdentityInDatabase(identity.getId(), e.getMessage(), e);
+            throw new FederatedIdentityServiceException("An error occurred while saving Federated Identity " + identity.getId() + " in the database", e);
+        }
+    }
+
+    @Override
+    public Optional<FederatedIdentity> findByProviderAndSubject(String provider, String issuer, String subject) {
+        try {
+            return federatedIdentityDatabase.findByProviderAndSubject(provider, issuer, subject);
+        } catch (SQLException e) {
+            LOG.errorFetchingFederatedIdentityFromDatabase(provider, subject, issuer, e.getMessage(), e);
+        }
+        return Optional.empty();
+    }
+
+    @Override
+    public Optional<FederatedIdentity> findById(String id) {
+        try {
+            return federatedIdentityDatabase.findById(id);
+        } catch (SQLException e) {
+            LOG.errorFetchingFederatedIdentityFromDatabase(id, e.getMessage(), e);
+        }
+        return Optional.empty();
+    }
+
+}
diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/services/token/impl/TokenStateDatabase.java b/gateway-server/src/main/java/org/apache/knox/gateway/services/token/impl/TokenStateDatabase.java
index dbc89d695..c7579a480 100644
--- a/gateway-server/src/main/java/org/apache/knox/gateway/services/token/impl/TokenStateDatabase.java
+++ b/gateway-server/src/main/java/org/apache/knox/gateway/services/token/impl/TokenStateDatabase.java
@@ -19,7 +19,7 @@ package org.apache.knox.gateway.services.token.impl;
 
 import org.apache.commons.codec.binary.Base64;
 import org.apache.knox.gateway.database.DatabaseType;
-import org.apache.knox.gateway.database.JDBCUtils;
+import org.apache.knox.gateway.database.KnoxDatabase;
 import org.apache.knox.gateway.services.security.token.KnoxToken;
 import org.apache.knox.gateway.services.security.token.TokenMetadata;
 
@@ -37,7 +37,7 @@ import java.util.Set;
 
 import static java.nio.charset.StandardCharsets.UTF_8;
 
-public class TokenStateDatabase {
+public class TokenStateDatabase extends KnoxDatabase {
   static final String TOKENS_TABLE_NAME = "KNOX_TOKENS";
   static final String TOKEN_METADATA_TABLE_NAME = "KNOX_TOKEN_METADATA";
   private static final String ADD_TOKEN_SQL = "INSERT INTO " + TOKENS_TABLE_NAME + "(token_id, issue_time, expiration, max_lifetime) VALUES(?, ?, ?, ?)";
@@ -58,21 +58,13 @@ public class TokenStateDatabase {
   private static final String GET_TOKENS_CREATED_BY_USER_NAME_SQL = GET_ALL_TOKENS_SQL + " AND kt.token_id IN (SELECT token_id FROM " + TOKEN_METADATA_TABLE_NAME + " WHERE md_name = '" + TokenMetadata.CREATED_BY + "' AND md_value = ? )"
       + " ORDER BY kt.issue_time";
 
-  private final DataSource dataSource;
-
   TokenStateDatabase(DataSource dataSource, String dbType) throws Exception {
-    this.dataSource = dataSource;
+    super(dataSource);
     DatabaseType databaseType = DatabaseType.fromString(dbType);
     createTableIfNotExists(TOKENS_TABLE_NAME, databaseType.tokensTableSql());
     createTableIfNotExists(TOKEN_METADATA_TABLE_NAME, databaseType.metadataTableSql());
   }
 
-  private void createTableIfNotExists(String tableName, String createSqlFileName) throws Exception {
-    if (!JDBCUtils.tableExists(tableName, dataSource)) {
-      JDBCUtils.createTableFromSQL(createSqlFileName, dataSource, TokenStateDatabase.class.getClassLoader());
-    }
-  }
-
   boolean addToken(String tokenId, long issueTime, long expiration, long maxLifetimeDuration) throws SQLException {
     try (Connection connection = dataSource.getConnection(); PreparedStatement addTokenStatement = connection.prepareStatement(ADD_TOKEN_SQL)) {
       addTokenStatement.setString(1, tokenId);
diff --git a/gateway-server/src/main/resources/META-INF/services/org.apache.knox.gateway.services.ServiceFactory b/gateway-server/src/main/resources/META-INF/services/org.apache.knox.gateway.services.ServiceFactory
index bd808747c..93bc7845a 100644
--- a/gateway-server/src/main/resources/META-INF/services/org.apache.knox.gateway.services.ServiceFactory
+++ b/gateway-server/src/main/resources/META-INF/services/org.apache.knox.gateway.services.ServiceFactory
@@ -16,9 +16,14 @@
 # limitations under the License.
 ##########################################################################
 
+# Please keep the alphabetical order of service factories!
+
 org.apache.knox.gateway.services.factory.AliasServiceFactory
+org.apache.knox.gateway.services.factory.ConcurrentSessionVerifierFactory
 org.apache.knox.gateway.services.factory.ClusterConfigurationMonitorServiceFactory
 org.apache.knox.gateway.services.factory.CryptoServiceFactory
+org.apache.knox.gateway.services.factory.FederatedIdentityServiceFactory
+org.apache.knox.gateway.services.factory.GatewayStatusServiceFactory
 org.apache.knox.gateway.services.factory.HostMappingServiceFactory
 org.apache.knox.gateway.services.factory.KeystoreServiceFactory
 org.apache.knox.gateway.services.factory.MasterServiceFactory
@@ -28,10 +33,8 @@ org.apache.knox.gateway.services.factory.ServerInfoServiceFactory
 org.apache.knox.gateway.services.factory.ServiceDefinitionRegistryFactory
 org.apache.knox.gateway.services.factory.ServiceRegistryServiceFactory
 org.apache.knox.gateway.services.factory.SslServiceFactory
-org.apache.knox.gateway.services.factory.TokenServiceFactory
 org.apache.knox.gateway.services.factory.TokenStateServiceFactory
 org.apache.knox.gateway.services.factory.TopologyServiceFactory
-org.apache.knox.gateway.services.factory.ConcurrentSessionVerifierFactory
-org.apache.knox.gateway.services.factory.GatewayStatusServiceFactory
 org.apache.knox.gateway.services.factory.LdapServiceFactory
 org.apache.knox.gateway.services.factory.LDAPRolesLookupServiceFactory
+org.apache.knox.gateway.services.factory.TokenServiceFactory
diff --git a/gateway-server/src/main/resources/createKnoxIDFFederatedIdentityAttributesTable.sql b/gateway-server/src/main/resources/createKnoxIDFFederatedIdentityAttributesTable.sql
new file mode 100644
index 000000000..239cb5df1
--- /dev/null
+++ b/gateway-server/src/main/resources/createKnoxIDFFederatedIdentityAttributesTable.sql
@@ -0,0 +1,21 @@
+--  Licensed to the Apache Software Foundation (ASF) under one or more
+--  contributor license agreements. See the NOTICE file distributed with this
+--  work for additional information regarding copyright ownership. The ASF
+--  licenses this file to you under the Apache License, Version 2.0 (the
+--  "License"); you may not use this file except in compliance with the License.
+--  You may obtain a copy of the License at
+--
+--  http://www.apache.org/licenses/LICENSE-2.0
+--
+--  Unless required by applicable law or agreed to in writing, software
+--  distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+--  WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+--  License for the specific language governing permissions and limitations under
+--  the License.
+CREATE TABLE FEDERATED_IDENTITY_ATTR (
+    identity_id VARCHAR(36)  NOT NULL,
+    attr_key    VARCHAR(128) NOT NULL,
+    attr_value  TEXT,
+    PRIMARY KEY (identity_id, attr_key),
+    FOREIGN KEY (identity_id) REFERENCES FEDERATED_IDENTITY (id) ON DELETE CASCADE
+);
\ No newline at end of file
diff --git a/gateway-server/src/main/resources/createKnoxIDFFederatedIdentityAttributesTableDerby.sql b/gateway-server/src/main/resources/createKnoxIDFFederatedIdentityAttributesTableDerby.sql
new file mode 100644
index 000000000..13ae9ab11
--- /dev/null
+++ b/gateway-server/src/main/resources/createKnoxIDFFederatedIdentityAttributesTableDerby.sql
@@ -0,0 +1,22 @@
+--  Licensed to the Apache Software Foundation (ASF) under one or more
+--  contributor license agreements. See the NOTICE file distributed with this
+--  work for additional information regarding copyright ownership. The ASF
+--  licenses this file to you under the Apache License, Version 2.0 (the
+--  "License"); you may not use this file except in compliance with the License.
+--  You may obtain a copy of the License at
+--
+--  http://www.apache.org/licenses/LICENSE-2.0
+--
+--  Unless required by applicable law or agreed to in writing, software
+--  distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+--  WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+--  License for the specific language governing permissions and limitations under
+--  the License.
+
+CREATE TABLE FEDERATED_IDENTITY_ATTR (
+    identity_id VARCHAR(36),
+    attr_key    VARCHAR(128),
+    attr_value  CLOB,
+    PRIMARY KEY (identity_id, attr_key),
+    CONSTRAINT fk_fed_attr FOREIGN KEY (identity_id) REFERENCES FEDERATED_IDENTITY(id) ON DELETE CASCADE
+);
\ No newline at end of file
diff --git a/gateway-server/src/main/resources/createKnoxIDFFederatedIdentityAttributesTableOracle.sql b/gateway-server/src/main/resources/createKnoxIDFFederatedIdentityAttributesTableOracle.sql
new file mode 100644
index 000000000..7ed07b1b0
--- /dev/null
+++ b/gateway-server/src/main/resources/createKnoxIDFFederatedIdentityAttributesTableOracle.sql
@@ -0,0 +1,22 @@
+--  Licensed to the Apache Software Foundation (ASF) under one or more
+--  contributor license agreements. See the NOTICE file distributed with this
+--  work for additional information regarding copyright ownership. The ASF
+--  licenses this file to you under the Apache License, Version 2.0 (the
+--  "License"); you may not use this file except in compliance with the License.
+--  You may obtain a copy of the License at
+--
+--  http://www.apache.org/licenses/LICENSE-2.0
+--
+--  Unless required by applicable law or agreed to in writing, software
+--  distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+--  WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+--  License for the specific language governing permissions and limitations under
+--  the License.
+
+CREATE TABLE FEDERATED_IDENTITY_ATTR (
+    identity_id VARCHAR2(36) NOT NULL,
+    attr_key    VARCHAR2(128) NOT NULL,
+    attr_value  CLOB,
+    CONSTRAINT pk_fed_attr PRIMARY KEY (identity_id, attr_key),
+    CONSTRAINT fk_fed_attr FOREIGN KEY (identity_id) REFERENCES FEDERATED_IDENTITY(id) ON DELETE CASCADE
+);
\ No newline at end of file
diff --git a/gateway-server/src/main/resources/createKnoxIDFFederatedIdentityTable.sql b/gateway-server/src/main/resources/createKnoxIDFFederatedIdentityTable.sql
new file mode 100644
index 000000000..acaf1c340
--- /dev/null
+++ b/gateway-server/src/main/resources/createKnoxIDFFederatedIdentityTable.sql
@@ -0,0 +1,25 @@
+--  Licensed to the Apache Software Foundation (ASF) under one or more
+--  contributor license agreements. See the NOTICE file distributed with this
+--  work for additional information regarding copyright ownership. The ASF
+--  licenses this file to you under the Apache License, Version 2.0 (the
+--  "License"); you may not use this file except in compliance with the License.
+--  You may obtain a copy of the License at
+--
+--  http://www.apache.org/licenses/LICENSE-2.0
+--
+--  Unless required by applicable law or agreed to in writing, software
+--  distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+--  WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+--  License for the specific language governing permissions and limitations under
+--  the License.
+
+CREATE TABLE FEDERATED_IDENTITY (
+    id               VARCHAR(36) PRIMARY KEY,
+    user_id          VARCHAR(36)  NOT NULL,
+    provider         VARCHAR(64)  NOT NULL,
+    external_subject VARCHAR(255) NOT NULL,
+    external_issuer  VARCHAR(255) NOT NULL,
+    created_at       TIMESTAMP    NOT NULL
+);
+
+CREATE UNIQUE INDEX UX_FED_IDENTITY ON FEDERATED_IDENTITY (provider, external_issuer, external_subject);
\ No newline at end of file
diff --git a/gateway-server/src/main/resources/createKnoxIDFFederatedIdentityTableDerby.sql b/gateway-server/src/main/resources/createKnoxIDFFederatedIdentityTableDerby.sql
new file mode 100644
index 000000000..7152d4c71
--- /dev/null
+++ b/gateway-server/src/main/resources/createKnoxIDFFederatedIdentityTableDerby.sql
@@ -0,0 +1,25 @@
+--  Licensed to the Apache Software Foundation (ASF) under one or more
+--  contributor license agreements. See the NOTICE file distributed with this
+--  work for additional information regarding copyright ownership. The ASF
+--  licenses this file to you under the Apache License, Version 2.0 (the
+--  "License"); you may not use this file except in compliance with the License.
+--  You may obtain a copy of the License at
+--
+--  http://www.apache.org/licenses/LICENSE-2.0
+--
+--  Unless required by applicable law or agreed to in writing, software
+--  distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+--  WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+--  License for the specific language governing permissions and limitations under
+--  the License.
+
+CREATE TABLE FEDERATED_IDENTITY (
+    id               VARCHAR(36) PRIMARY KEY,
+    user_id          VARCHAR(36),
+    provider         VARCHAR(64),
+    external_subject VARCHAR(255),
+    external_issuer  VARCHAR(255),
+    created_at       TIMESTAMP
+);
+
+CREATE UNIQUE INDEX UX_FED_IDENTITY ON FEDERATED_IDENTITY (provider, external_issuer, external_subject);
\ No newline at end of file
diff --git a/gateway-server/src/main/resources/createKnoxIDFFederatedIdentityTableOracle.sql b/gateway-server/src/main/resources/createKnoxIDFFederatedIdentityTableOracle.sql
new file mode 100644
index 000000000..0dc42c1f7
--- /dev/null
+++ b/gateway-server/src/main/resources/createKnoxIDFFederatedIdentityTableOracle.sql
@@ -0,0 +1,25 @@
+--  Licensed to the Apache Software Foundation (ASF) under one or more
+--  contributor license agreements. See the NOTICE file distributed with this
+--  work for additional information regarding copyright ownership. The ASF
+--  licenses this file to you under the Apache License, Version 2.0 (the
+--  "License"); you may not use this file except in compliance with the License.
+--  You may obtain a copy of the License at
+--
+--  http://www.apache.org/licenses/LICENSE-2.0
+--
+--  Unless required by applicable law or agreed to in writing, software
+--  distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+--  WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+--  License for the specific language governing permissions and limitations under
+--  the License.
+
+CREATE TABLE FEDERATED_IDENTITY (
+    id               VARCHAR2(36) PRIMARY KEY,
+    user_id          VARCHAR2(36) NOT NULL,
+    provider         VARCHAR2(64) NOT NULL,
+    external_subject VARCHAR2(255) NOT NULL,
+    external_issuer  VARCHAR2(255) NOT NULL,
+    created_at       TIMESTAMP NOT NULL
+);
+
+CREATE UNIQUE INDEX UX_FED_IDENTITY ON FEDERATED_IDENTITY (provider, external_issuer, external_subject);
\ No newline at end of file
diff --git a/gateway-server/src/test/java/org/apache/knox/gateway/services/AbstractGatewayServicesTest.java b/gateway-server/src/test/java/org/apache/knox/gateway/services/AbstractGatewayServicesTest.java
index ff58ad6f9..57271324c 100644
--- a/gateway-server/src/test/java/org/apache/knox/gateway/services/AbstractGatewayServicesTest.java
+++ b/gateway-server/src/test/java/org/apache/knox/gateway/services/AbstractGatewayServicesTest.java
@@ -67,7 +67,8 @@ public class AbstractGatewayServicesTest {
         ServiceType.REMOTE_CONFIGURATION_MONITOR,
         ServiceType.GATEWAY_STATUS_SERVICE,
         ServiceType.LDAP_SERVICE,
-        ServiceType.LDAP_ROLES_LOOKUP_SERVICE
+        ServiceType.LDAP_ROLES_LOOKUP_SERVICE,
+        ServiceType.KNOXIDF_FEDERATED_IDENTITY_SERVICE
     };
 
     assertNotEquals(ServiceType.values(), orderedServiceTypes);
diff --git a/gateway-util-common/pom.xml b/gateway-service-knoxidf/pom.xml
similarity index 63%
copy from gateway-util-common/pom.xml
copy to gateway-service-knoxidf/pom.xml
index 1eec58fe9..48296b435 100644
--- a/gateway-util-common/pom.xml
+++ b/gateway-service-knoxidf/pom.xml
@@ -24,99 +24,92 @@
         <artifactId>gateway</artifactId>
         <version>3.0.0-SNAPSHOT</version>
     </parent>
-    
-    <artifactId>gateway-util-common</artifactId>
-    <name>gateway-util-common</name>
-    <description>A collection of common utilities.</description>
+
+    <artifactId>gateway-service-knoxidf</artifactId>
+    <name>gateway-service-knoxidf</name>
 
     <dependencies>
         <dependency>
             <groupId>org.apache.knox</groupId>
             <artifactId>gateway-i18n</artifactId>
         </dependency>
-
         <dependency>
-            <groupId>org.bouncycastle</groupId>
-            <artifactId>bcprov-jdk18on</artifactId>
+            <groupId>org.apache.knox</groupId>
+            <artifactId>gateway-spi</artifactId>
         </dependency>
-
         <dependency>
-            <groupId>org.bouncycastle</groupId>
-            <artifactId>bcpkix-jdk18on</artifactId>
-        </dependency>
-
-        <dependency>
-            <groupId>com.fasterxml.jackson.core</groupId>
-            <artifactId>jackson-core</artifactId>
+            <groupId>org.apache.knox</groupId>
+            <artifactId>gateway-provider-jersey</artifactId>
         </dependency>
         <dependency>
-            <groupId>com.fasterxml.jackson.core</groupId>
-            <artifactId>jackson-databind</artifactId>
+            <groupId>org.apache.knox</groupId>
+            <artifactId>gateway-util-common</artifactId>
         </dependency>
         <dependency>
-            <groupId>com.fasterxml.jackson.datatype</groupId>
-            <artifactId>jackson-datatype-jsr310</artifactId>
+            <groupId>org.apache.knox</groupId>
+            <artifactId>gateway-service-knoxtoken</artifactId>
         </dependency>
-        
+
         <dependency>
-            <groupId>commons-codec</groupId>
-            <artifactId>commons-codec</artifactId>
+            <groupId>javax.annotation</groupId>
+            <artifactId>javax.annotation-api</artifactId>
         </dependency>
-
         <dependency>
-            <groupId>org.apache.commons</groupId>
-            <artifactId>commons-lang3</artifactId>
+            <groupId>javax.ws.rs</groupId>
+            <artifactId>javax.ws.rs-api</artifactId>
         </dependency>
-
         <dependency>
             <groupId>javax.servlet</groupId>
             <artifactId>javax.servlet-api</artifactId>
         </dependency>
 
         <dependency>
-            <groupId>com.sun.activation</groupId>
-            <artifactId>jakarta.activation</artifactId>
+            <groupId>com.google.guava</groupId>
+            <artifactId>guava</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>commons-io</groupId>
+            <artifactId>commons-io</artifactId>
         </dependency>
-
         <dependency>
-            <groupId>jakarta.activation</groupId>
-            <artifactId>jakarta.activation-api</artifactId>
+            <groupId>com.fasterxml.jackson.core</groupId>
+            <artifactId>jackson-core</artifactId>
         </dependency>
-
         <dependency>
-            <groupId>org.apache.directory.server</groupId>
-            <artifactId>apacheds-jdbm</artifactId>
+            <groupId>com.fasterxml.jackson.core</groupId>
+            <artifactId>jackson-databind</artifactId>
         </dependency>
-
         <dependency>
-            <groupId>org.apache.logging.log4j</groupId>
-            <artifactId>log4j-api</artifactId>
+            <groupId>com.fasterxml.uuid</groupId>
+            <artifactId>java-uuid-generator</artifactId>
         </dependency>
         <dependency>
-            <groupId>org.apache.logging.log4j</groupId>
-            <artifactId>log4j-core</artifactId>
+            <groupId>com.nimbusds</groupId>
+            <artifactId>nimbus-jose-jwt</artifactId>
         </dependency>
         <dependency>
-            <groupId>org.apache.logging.log4j</groupId>
-            <artifactId>log4j-slf4j2-impl</artifactId>
+            <groupId>com.github.ben-manes.caffeine</groupId>
+            <artifactId>caffeine</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.commons</groupId>
+            <artifactId>commons-lang3</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.commons</groupId>
+            <artifactId>commons-text</artifactId>
         </dependency>
         <dependency>
             <groupId>org.apache.httpcomponents</groupId>
             <artifactId>httpclient</artifactId>
         </dependency>
-        <!-- ********** ********** ********** ********** ********** ********** -->
-        <!-- ********** Test Dependencies                           ********** -->
-        <!-- ********** ********** ********** ********** ********** ********** -->
-
         <dependency>
-            <groupId>org.apache.knox</groupId>
-            <artifactId>gateway-test-utils</artifactId>
-            <scope>test</scope>
+            <groupId>org.apache.httpcomponents</groupId>
+            <artifactId>httpcore</artifactId>
         </dependency>
         <dependency>
-            <groupId>commons-io</groupId>
-            <artifactId>commons-io</artifactId>
-            <scope>test</scope>
+            <groupId>org.glassfish.jersey.core</groupId>
+            <artifactId>jersey-common</artifactId>
         </dependency>
     </dependencies>
 </project>
diff --git a/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/AuthConsentServlet.java b/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/AuthConsentServlet.java
new file mode 100644
index 000000000..6200edc83
--- /dev/null
+++ b/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/AuthConsentServlet.java
@@ -0,0 +1,132 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. The ASF
+ * licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * <p>
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * <p>
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package org.apache.knox.gateway.service.knoxidf;
+
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.UriInfo;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Locale;
+import java.util.Set;
+
+import static org.apache.knox.gateway.util.knoxidf.KnoxIDFUtils.getRequestParamSafe;
+
+
+public class AuthConsentServlet extends HttpServlet {
+
+    @Context
+    UriInfo uriInfo;
+
+    @Override
+    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
+        response.setContentType("text/html;charset=UTF-8");
+        final String clientId = getRequestParamSafe(request, "client_id");
+        final String state = getRequestParamSafe(request, "state");
+        final String scope = getRequestParamSafe(request, "scope");
+        final Set<String> scopes = new HashSet<>(Arrays.asList(scope.split("\\s+")));
+
+        try (PrintWriter out = response.getWriter()) {
+            out.println("<!DOCTYPE html>");
+            out.println("<html><head><title>Consent Required</title>");
+            out.println("<style>");
+            out.println("body { font-family: 'Segoe UI', Arial, sans-serif; background: #f5f6f8; padding: 40px; margin: 0; }");
+            out.println(".container { background: #fff; padding: 30px 40px; border-radius: 10px; max-width: 480px; width: 100%; box-shadow: 0 4px 12px rgba(0,0,0,0.1); margin: 0 auto; }");
+            out.println("h2 { color: #333; margin-bottom: 20px; }");
+            out.println("p { font-size: 15px; color: #555; margin-bottom: 20px; }");
+            out.println("ul { text-align: left; padding-left: 20px; margin-bottom: 30px; }");
+            out.println("li { margin-bottom: 8px; }");
+            out.println("button { padding: 10px 24px; margin: 0 10px; font-size: 15px; cursor: pointer; border-radius: 6px; border: none; transition: background-color 0.25s ease; }");
+            out.println(".accept { background: #007bff; color: #fff; }");
+            out.println(".accept:hover { background: #0069d9; }");
+            out.println(".deny { background: #e0e0e0; color: #333; }");
+            out.println(".deny:hover { background: #d0d0d0; }");
+            out.println("</style>");
+            out.println("</head><body>");
+            out.println("<div class='container'>");
+            out.println("<h2>Application Consent Required</h2>");
+            out.printf(Locale.US, "<p>The application <b>%s</b> is requesting access to your account.</p>%n", clientId);
+
+            if (!scopes.isEmpty()) {
+                out.println("<p>This application will be able to:</p>");
+                out.println("<ul>");
+                for (String s : scopes) {
+                    out.printf(Locale.US, "<li>%s</li>%n", describeScope(s));
+                }
+                out.println("</ul>");
+            }
+
+            out.println("<form method='post' onsubmit='return confirmAction();'>");
+            out.printf(Locale.US, "<input type='hidden' name='state' value='%s'/>%n", state);
+            out.println("<div style='display: flex; justify-content: center; gap: 20px;'>");
+            out.println("<button type='submit' name='action' value='accept' class='accept'>Accept</button>");
+            out.println("<button type='submit' name='action' value='deny' class='deny'>Deny</button>");
+            out.println("</div>");
+            out.println("</form>");
+            out.println("</div>");
+            out.println("<script>");
+            out.println("function confirmAction() {");
+            out.println("  const action = event.submitter.value;");
+            out.println("  if (action === 'accept') return confirm('Do you want to grant consent to this application?');");
+            out.println("  return true;");
+            out.println("}");
+            out.println("</script>");
+            out.println("</body></html>");
+        }
+    }
+
+    private String describeScope(String scope) {
+        if (scope == null) {
+            return "";
+        }
+
+        switch (scope) {
+            case "openid":
+                return "Authenticate using your account";
+            case "profile":
+                return "View your basic profile information";
+            case "email":
+                return "View your email address";
+            case "address":
+                return "View your address information";
+            case "phone":
+                return "View your phone number";
+            case "calendar.read":
+                return "Read your calendar events";
+            case "calendar.write":
+                return "Modify your calendar events";
+            default:
+                return scope;
+        }
+    }
+
+    //Redirect target is application-local and state is encoded/controlled
+    @SuppressWarnings("UNVALIDATED_REDIRECT")
+    @Override
+    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException {
+        final String action = request.getParameter("action");
+        final String state = request.getParameter("state");
+        final String redirectUri = request.getServletContext().getContextPath() + "/" + AuthorizeResource.RESOURCE_PATH +
+                ("accept".equals(action) ? "/consentAccepted?state=" + state : "/consentDenied");
+        response.sendRedirect(redirectUri);
+    }
+
+}
diff --git a/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/AuthorizeResource.java b/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/AuthorizeResource.java
new file mode 100644
index 000000000..94415de19
--- /dev/null
+++ b/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/AuthorizeResource.java
@@ -0,0 +1,408 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. The ASF
+ * licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * <p>
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * <p>
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package org.apache.knox.gateway.service.knoxidf;
+
+import com.fasterxml.uuid.Generators;
+import com.fasterxml.uuid.impl.NameBasedGenerator;
+import com.nimbusds.jose.KeyLengthException;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.lang3.tuple.Pair;
+import org.apache.http.NameValuePair;
+import org.apache.http.client.entity.UrlEncodedFormEntity;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClients;
+import org.apache.http.message.BasicNameValuePair;
+import org.apache.http.util.EntityUtils;
+import org.apache.knox.gateway.security.SubjectUtils;
+import org.apache.knox.gateway.service.knoxtoken.PasscodeTokenResourceBase;
+import org.apache.knox.gateway.services.GatewayServices;
+import org.apache.knox.gateway.services.ServiceLifecycleException;
+import org.apache.knox.gateway.services.ServiceType;
+import org.apache.knox.gateway.services.knoxidf.federation.FederatedIdentity;
+import org.apache.knox.gateway.services.knoxidf.federation.FederatedIdentityService;
+import org.apache.knox.gateway.services.security.AliasServiceException;
+import org.apache.knox.gateway.services.security.token.TokenMetadata;
+import org.apache.knox.gateway.services.security.token.TokenMetadataType;
+import org.apache.knox.gateway.services.security.token.UnknownTokenException;
+import org.apache.knox.gateway.services.security.token.impl.JWT;
+import org.apache.knox.gateway.services.security.token.impl.JWTToken;
+import org.apache.knox.gateway.util.JsonUtils;
+import org.apache.knox.gateway.util.knoxidf.AuthorizeRequestMetadata;
+import org.apache.knox.gateway.util.knoxidf.AuthorizeRequestMetadataStore;
+import org.apache.knox.gateway.util.knoxidf.FederatedOpConfiguration;
+import org.apache.knox.gateway.util.knoxidf.FederatedOpConfigurationStore;
+import org.apache.knox.gateway.util.knoxidf.KnoxIDFUtils;
+
+import javax.annotation.PostConstruct;
+import javax.servlet.ServletContext;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.ws.rs.GET;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.Response;
+import java.io.UnsupportedEncodingException;
+import java.net.URI;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import java.text.ParseException;
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+import java.util.UUID;
+import java.util.stream.Collectors;
+
+import static org.apache.knox.gateway.security.CommonTokenConstants.CLIENT_SECRET;
+import static org.apache.knox.gateway.security.CommonTokenConstants.GRANT_TYPE;
+import static org.apache.knox.gateway.util.knoxidf.KnoxIDFConstants.ALLOWED_SCOPES;
+import static org.apache.knox.gateway.util.knoxidf.KnoxIDFConstants.BASE_RESORCE_PATH;
+import static org.apache.knox.gateway.util.knoxidf.KnoxIDFConstants.CLIENT_ID;
+import static org.apache.knox.gateway.util.knoxidf.KnoxIDFConstants.CODE;
+import static org.apache.knox.gateway.util.knoxidf.KnoxIDFConstants.CODE_CHALLENGE;
+import static org.apache.knox.gateway.util.knoxidf.KnoxIDFConstants.CODE_CHALLENGE_METHOD;
+import static org.apache.knox.gateway.util.knoxidf.KnoxIDFConstants.DEFAULT_SCOPES;
+import static org.apache.knox.gateway.util.knoxidf.KnoxIDFConstants.FEDERATED_IDENTITY_ID;
+import static org.apache.knox.gateway.util.knoxidf.KnoxIDFConstants.NONCE;
+import static org.apache.knox.gateway.util.knoxidf.KnoxIDFConstants.OFFLINE_ACCESS_SCOPE;
+import static org.apache.knox.gateway.util.knoxidf.KnoxIDFConstants.REDIRECT_URI;
+import static org.apache.knox.gateway.util.knoxidf.KnoxIDFConstants.REDIRECT_URIS;
+import static org.apache.knox.gateway.util.knoxidf.KnoxIDFConstants.RESPONSE_TYPE;
+import static org.apache.knox.gateway.util.knoxidf.KnoxIDFConstants.SCOPE;
+import static org.apache.knox.gateway.util.knoxidf.KnoxIDFConstants.STATE;
+import static org.apache.knox.gateway.util.knoxidf.KnoxIDFUtils.error;
+
+
+@Path(AuthorizeResource.RESOURCE_PATH)
+public class AuthorizeResource extends PasscodeTokenResourceBase {
+    static final String RESOURCE_PATH = BASE_RESORCE_PATH + "/authorize";
+    private static final UUID KNOX_NAMESPACE = UUID.fromString("6ba7b811-9dad-11d1-80b4-00c04fd430c8");
+    private static final NameBasedGenerator UUID_V5 = Generators.nameBasedGenerator(KNOX_NAMESPACE);
+    public static final Set<String> ALLOWED_CLAIMS = Set.of("preferred_username", "email", "email_verified",
+            "given_name", "family_name", "name", "locale");
+
+    private static final String UTF_8 = StandardCharsets.UTF_8.name();
+    private AuthorizeRequestMetadataStore authorizeRequestMetadataStore;
+    private final FederatedOpConfigurationStore federatedOpConfigurationStore = FederatedOpConfigurationStore.getInstance(120000L);
+
+    @Context
+    private HttpServletRequest request;
+
+    @Context
+    private ServletContext servletContext;
+
+    private FederatedIdentityService federatedIdentityService;
+
+    @PostConstruct
+    @Override
+    public void init() throws ServletException, AliasServiceException, ServiceLifecycleException, KeyLengthException {
+        super.init();
+        this.authorizeRequestMetadataStore = AuthorizeRequestMetadataStore.getInstance(tokenTTL);
+        final GatewayServices services = (GatewayServices) servletContext.getAttribute(GatewayServices.GATEWAY_SERVICES_ATTRIBUTE);
+        federatedIdentityService = services.getService(ServiceType.KNOXIDF_FEDERATED_IDENTITY_SERVICE);
+    }
+
+    @Override
+    @GET
+    public Response doGet() {
+        return authorize();
+    }
+
+    @Override
+    @POST
+    public Response doPost() {
+        return authorize();
+    }
+
+    private Response authorize() {
+        return authorize(request.getParameter(RESPONSE_TYPE), request.getParameter(CLIENT_ID), request.getParameter(REDIRECT_URI),
+                request.getParameter(SCOPE), request.getParameter(STATE), request.getParameter(NONCE),
+                request.getParameter(CODE_CHALLENGE), request.getParameter(CODE_CHALLENGE_METHOD));
+    }
+
+    private Response authorize(String responseType,
+                               String clientId,
+                               String redirectUri,
+                               String scope,
+                               String state,
+                               String nonce,
+                               String codeChallenge,
+                               String codeChallengeMethod) {
+        final String subject = SubjectUtils.getCurrentEffectivePrincipalName();
+        final Set<String> requestedScopes = StringUtils.isBlank(scope) ? DEFAULT_SCOPES : new HashSet<>(Arrays.asList(scope.split("\\s+")));
+        final AuthorizeRequestMetadata authorizeRequestMetadata = new AuthorizeRequestMetadata(clientId, subject, responseType, redirectUri, requestedScopes, state, nonce, codeChallenge, codeChallengeMethod);
+        final Response verificationErrorResponse = verifyParams(authorizeRequestMetadata);
+        if (verificationErrorResponse != null) {
+            return verificationErrorResponse;
+        }
+
+        if (!hasConsent(authorizeRequestMetadata)) {
+            if ("true".equalsIgnoreCase(request.getParameter("auto_consent"))) {
+                markConsentAccepted(authorizeRequestMetadata);
+            } else {
+                final String consentAuthState = UUID.randomUUID().toString();
+                authorizeRequestMetadataStore.put(consentAuthState, authorizeRequestMetadata);
+                final String baseUri = servletContext.getContextPath() + "/authConsent";
+                final String scopeParam = URLEncoder.encode(authorizeRequestMetadata.getJoinedRequestedScopes(), StandardCharsets.UTF_8);
+                final String redirect = String.format(Locale.US, "%s?client_id=%s&state=%s&scope=%s", baseUri, clientId, consentAuthState, scopeParam);
+                return Response.seeOther(java.net.URI.create(redirect)).build();
+            }
+        }
+        return getAuthCodeFromKnox(authorizeRequestMetadata, null);
+    }
+
+    private boolean hasConsent(final AuthorizeRequestMetadata authorizeRequestMetadata) {
+        try {
+            final TokenMetadata tokenMetadata = tokenStateService.getTokenMetadata(authorizeRequestMetadata.getClientId());
+            final String consentKey = "consentAccepted_" + authorizeRequestMetadata.getSubject();
+            final String storedScopes = tokenMetadata.getMetadataMap().get(consentKey);
+            if (storedScopes == null || storedScopes.isEmpty()) {
+                return false;
+            }
+            final Set<String> storedScopeSet = new HashSet<>(Arrays.asList(storedScopes.split("\\s+")));
+            return storedScopeSet.containsAll(authorizeRequestMetadata.getRequestedScopes());
+        } catch (UnknownTokenException e) {
+            //this should not happen as we validated the client_id already
+            return false;
+        }
+    }
+
+    private void markConsentAccepted(AuthorizeRequestMetadata authorizeRequestMetadata) {
+        final TokenMetadata consentAcceptedMetadata = new TokenMetadata();
+        consentAcceptedMetadata.add("consentAccepted_" + authorizeRequestMetadata.getSubject(), authorizeRequestMetadata.getJoinedRequestedScopes());
+        tokenStateService.addMetadata(authorizeRequestMetadata.getClientId(), consentAcceptedMetadata);
+    }
+
+    private Response getAuthCodeFromKnox(final AuthorizeRequestMetadata authorizeRequestMetadata, final Pair<String, String> federatedTokens) {
+        final Response tokenResponse = getAuthenticationToken();
+        if (tokenResponse.getStatus() == Response.Status.OK.getStatusCode()) {
+            final Map<String, String> tokenResponseMap = JsonUtils.getMapFromJsonString(tokenResponse.getEntity().toString());
+            final String tokenId = tokenResponseMap.get(TOKEN_ID);
+            decorateAuthCodeToken(tokenId, authorizeRequestMetadata, federatedTokens);
+            return redirectToAuthSuccess(authorizeRequestMetadata, tokenId);
+        }
+        return tokenResponse;
+    }
+
+    private Response redirectToAuthSuccess(final AuthorizeRequestMetadata authorizeRequestMetadata, final String code) {
+        final String redirectLocation;
+        try {
+            redirectLocation = authorizeRequestMetadata.getRedirectUri()
+                    + "?code=" + URLEncoder.encode(code, UTF_8)
+                    + "&state=" + URLEncoder.encode(authorizeRequestMetadata.getState(), UTF_8);
+        } catch (UnsupportedEncodingException e) {
+            throw new RuntimeException(e); //This should never happen with UTF-8
+        }
+        return Response.seeOther(URI.create(redirectLocation)).build();
+    }
+
+    @GET
+    @Path("/callback")
+    public Response authCallback() throws Exception {
+        //This is the callback for the federated OP
+        final String federatedAuthCode = request.getParameter(CODE);
+        final String state = request.getParameter(STATE);
+        final AuthorizeRequestMetadata authorizeRequestMetadata = authorizeRequestMetadataStore.get(state);
+        //at this point, there has to be exactly 1 federated OP config
+        final FederatedOpConfiguration federatedOpConfiguration = federatedOpConfigurationStore.get(state).stream().findFirst().get();
+        final Pair<String, String> federatedTokens = exchangeFederatedAuthCodeToTokens(federatedAuthCode, federatedOpConfiguration);
+        final FederatedIdentity federatedIdentity = resolveFederatedIdentity(federatedTokens.getLeft(), federatedOpConfiguration.getName());
+        return getAuthCodeFromKnox(authorizeRequestMetadata, Pair.of(federatedIdentity.getId(), federatedTokens.getRight()));
+    }
+
+    @GET
+    @Path("/consentAccepted")
+    public Response consentAccepted() throws Exception {
+        final String state = request.getParameter(STATE);
+        final AuthorizeRequestMetadata authorizeRequestMetadata = authorizeRequestMetadataStore.get(state);
+        if (authorizeRequestMetadata == null) {
+            return error("Consent cannot be accepted", "Invalid state");
+        }
+        markConsentAccepted(authorizeRequestMetadata);
+        return authorize(authorizeRequestMetadata.getResponseType(),
+                authorizeRequestMetadata.getClientId(),
+                authorizeRequestMetadata.getRedirectUri(),
+                authorizeRequestMetadata.getJoinedRequestedScopes(),
+                authorizeRequestMetadata.getState(),
+                authorizeRequestMetadata.getNonce(),
+                authorizeRequestMetadata.getCodeChallenge(),
+                authorizeRequestMetadata.getCodeChallengeMethod());
+    }
+
+    @GET
+    @Path("/consentDenied")
+    public Response consentDenied() throws Exception {
+        return Response.status(Response.Status.FORBIDDEN).entity("Consent denied!").build();
+    }
+
+    private void decorateAuthCodeToken(final String tokenId, final AuthorizeRequestMetadata authorizeRequestMetadata, final Pair<String, String> federatedTokens) {
+        final Map<String, String> authCodeTokenMap = new HashMap<>();
+        authCodeTokenMap.put(TokenMetadata.TYPE, TokenMetadataType.AUTH_CODE.name());
+        authCodeTokenMap.put(CLIENT_ID, authorizeRequestMetadata.getClientId());
+        authCodeTokenMap.put(REDIRECT_URI, authorizeRequestMetadata.getRedirectUri());
+        authCodeTokenMap.put(TokenMetadata.USER_NAME, authorizeRequestMetadata.getSubject());
+        authCodeTokenMap.put(SCOPE, authorizeRequestMetadata.getJoinedRequestedScopes());
+        if (authorizeRequestMetadata.getRequestedScopes().contains(OFFLINE_ACCESS_SCOPE)) {
+            authCodeTokenMap.put(OFFLINE_ACCESS_SCOPE, "true");
+        }
+        if (StringUtils.isNotBlank(authorizeRequestMetadata.getNonce())) {
+            authCodeTokenMap.put(NONCE, authorizeRequestMetadata.getNonce());
+        }
+        if (StringUtils.isNotBlank(authorizeRequestMetadata.getCodeChallenge())) {
+            authCodeTokenMap.put(CODE_CHALLENGE, authorizeRequestMetadata.getCodeChallenge());
+            authCodeTokenMap.put(CODE_CHALLENGE_METHOD, StringUtils.defaultIfBlank(authorizeRequestMetadata.getCodeChallengeMethod(), "plain"));
+        }
+        if (federatedTokens != null) {
+            authCodeTokenMap.put(FEDERATED_IDENTITY_ID, federatedTokens.getLeft());
+            authCodeTokenMap.putAll(KnoxIDFUtils.splitFederatedToken(federatedTokens.getRight(), false));
+        }
+        tokenStateService.addMetadata(tokenId, new TokenMetadata(authCodeTokenMap));
+    }
+
+    private Response verifyParams(final AuthorizeRequestMetadata authorizeRequestMetadata) {
+        final Response basicVerificationResponse = authorizeRequestMetadata.verify();
+        if (basicVerificationResponse == null) {
+            final TokenMetadata tokenMetadata;
+            // Verify client ID
+            try {
+                //This is ok for a POC, but we should cache that later
+                tokenMetadata = tokenStateService.getTokenMetadata(authorizeRequestMetadata.getClientId());
+            } catch (UnknownTokenException e) {
+                return error("invalid_request", "Unknown client_id");
+            }
+
+            // Verify redirect URI
+            final String storedRedirectUris = tokenMetadata.getMetadata(REDIRECT_URIS);
+            if (StringUtils.isBlank(storedRedirectUris)) {
+                return error("invalid_request", "Missing stored redirect_uris, cannot authorize the request");
+            }
+            final Set<String> registeredRedirectUris = new HashSet<>(Arrays.asList(storedRedirectUris.split(",")));
+            if (!matchesRedirectUri(authorizeRequestMetadata.getRedirectUri(), registeredRedirectUris)) {
+                return error("invalid_request", "Invalid redirect_uri");
+            }
+
+            // Verify scope(s)
+            final String storedAllowedScopes = tokenMetadata.getMetadata(ALLOWED_SCOPES);
+            if (StringUtils.isBlank(storedAllowedScopes)) {
+                return error("invalid_scope", "Missing stored allowed_scopes, cannot authorize the request");
+            }
+            final Set<String> registeredScopes = new HashSet<>(Arrays.asList(storedAllowedScopes.trim().split("\\s+")));
+            if (authorizeRequestMetadata.getRequestedScopes().stream().anyMatch(scope -> !registeredScopes.contains(scope))) {
+                return error("invalid_scope", "One or more requested scopes are not allowed");
+            }
+
+            return null;
+        }
+        return basicVerificationResponse;
+    }
+
+    private boolean matchesRedirectUri(String requestedUri, Set<String> registeredUris) {
+        for (String registered : registeredUris) {
+            if (registered.endsWith("*")) {
+                String prefix = registered.substring(0, registered.length() - 1);
+                if (requestedUri.startsWith(prefix)) {
+                    return true;
+                }
+            } else if (registered.equals(requestedUri)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private Pair<String, String> exchangeFederatedAuthCodeToTokens(String federatedAuthCode, FederatedOpConfiguration opConfig) {
+        String federatedIdToken = null;
+        String federatedAccessToken = null;
+        final Response federatedTokenExchangeResponse = fetchFederatedTokens(federatedAuthCode, opConfig);
+        if (federatedTokenExchangeResponse.getStatus() == Response.Status.OK.getStatusCode()) {
+            final Map<String, String> federatedTokenExchangeResponseBodyMap = JsonUtils.getMapFromJsonString((String) federatedTokenExchangeResponse.getEntity());
+            federatedIdToken = federatedTokenExchangeResponseBodyMap.get("id_token");
+            federatedAccessToken = federatedTokenExchangeResponseBodyMap.get("access_token");
+            return Pair.of(federatedIdToken, federatedAccessToken);
+        } else {
+            throw new RuntimeException("Error fetching Federated Tokens from Federated Auth Code: " + federatedTokenExchangeResponse.getEntity());
+        }
+    }
+
+    private Response fetchFederatedTokens(final String code, FederatedOpConfiguration opConfig) {
+        final List<NameValuePair> params = new ArrayList<>();
+        params.add(new BasicNameValuePair(CODE, code));
+        params.add(new BasicNameValuePair(REDIRECT_URI, opConfig.getAuthorizeCallback()));
+        params.add(new BasicNameValuePair(GRANT_TYPE, "authorization_code"));
+        params.add(new BasicNameValuePair(CLIENT_ID, opConfig.getClientId()));
+        params.add(new BasicNameValuePair(CLIENT_SECRET, opConfig.getClientSecret()));
+
+        try (CloseableHttpClient httpClient = HttpClients.createDefault()) {
+            HttpPost post = new HttpPost(opConfig.getTokenEndpoint());
+            post.setHeader("Content-Type", "application/x-www-form-urlencoded");
+            post.setEntity(new UrlEncodedFormEntity(params, StandardCharsets.UTF_8));
+
+            try (CloseableHttpResponse response = httpClient.execute(post)) {
+                int status = response.getStatusLine().getStatusCode();
+                String body = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8);
+                return Response.status(status).entity(body).build();
+            }
+        } catch (Exception e) {
+            return Response.status(Response.Status.INTERNAL_SERVER_ERROR).entity("{\"error\":\"" + e.getMessage() + "\"}").build();
+        }
+    }
+
+    private FederatedIdentity resolveFederatedIdentity(String federatedIdToken, String opName) throws ParseException {
+        final JWT jwt = new JWTToken(federatedIdToken);
+        final String issuer = jwt.getIssuer();
+        final String subject = jwt.getSubject();
+        return federatedIdentityService.findByProviderAndSubject(opName.toUpperCase(Locale.US), issuer, subject).orElseGet(() -> persistFederatedIdentity(jwt, opName));
+    }
+
+    private FederatedIdentity persistFederatedIdentity(final JWT jwt, String opName) {
+        final Map<String, String> attributes = jwt.getJWTClaimsSet().getClaims().entrySet().stream()
+                .filter(e -> ALLOWED_CLAIMS.contains(e.getKey()))
+                .filter(e -> e.getValue() != null)
+                .collect(Collectors.toMap(
+                        Map.Entry::getKey,
+                        e -> String.valueOf(e.getValue()),
+                        (a, b) -> a,           // defensive: ignore duplicates
+                        HashMap::new
+                ));
+        final FederatedIdentity federatedIdentity = new FederatedIdentity(
+                deriveKnoxSubject(jwt.getSubject(), jwt.getIssuer()),  // internal user id (generated)
+                opName.toUpperCase(Locale.US),                         // provider
+                jwt.getSubject(),                                      // external subject
+                jwt.getIssuer(),                                       // external issuer
+                Instant.now(),                                         // createdAt
+                attributes
+        );
+
+        federatedIdentityService.addFederatedIdentity(federatedIdentity);
+
+        return federatedIdentity;
+    }
+
+    private String deriveKnoxSubject(String subject, String issuer) {
+        final String name = issuer + "|" + subject;
+        final UUID uuid = UUID_V5.generate(name.getBytes(StandardCharsets.UTF_8));
+        return uuid.toString();
+    }
+}
diff --git a/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/DiscoveryResource.java b/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/DiscoveryResource.java
new file mode 100644
index 000000000..ce895549d
--- /dev/null
+++ b/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/DiscoveryResource.java
@@ -0,0 +1,76 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. The ASF
+ * licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * <p>
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * <p>
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package org.apache.knox.gateway.service.knoxidf;
+
+import org.apache.knox.gateway.services.GatewayServices;
+import org.apache.knox.gateway.util.JsonUtils;
+import org.apache.knox.gateway.util.knoxidf.KnoxIDFConstants;
+
+import javax.annotation.PostConstruct;
+import javax.servlet.ServletContext;
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.UriInfo;
+import java.util.HashMap;
+import java.util.Map;
+
+import static org.apache.knox.gateway.util.knoxidf.KnoxIDFConstants.BASE_RESORCE_PATH;
+import static org.apache.knox.gateway.util.knoxidf.KnoxIDFConstants.TOKEN_EXCHANGE_TOPOLOGY_NAME;
+
+@Path(BASE_RESORCE_PATH + "/.well-known/openid-configuration")
+@Produces(MediaType.APPLICATION_JSON)
+public class DiscoveryResource {
+    private String currentTopologyName;
+    private String tokenExchangeTopologyName;
+
+    @Context
+    private ServletContext servletContext;
+
+    @PostConstruct
+    public void init() {
+        tokenExchangeTopologyName = servletContext.getInitParameter(TOKEN_EXCHANGE_TOPOLOGY_NAME);
+        currentTopologyName = (String) servletContext.getAttribute(GatewayServices.GATEWAY_CLUSTER_ATTRIBUTE);
+    }
+
+    @GET
+    public Response getConfig(@Context UriInfo uriInfo) {
+        final String baseUrl = uriInfo.getBaseUri().toString();
+        final Map<String, Object> config = new HashMap<>();
+        config.put("issuer", baseUrl + "knoxidf");
+        config.put("authorization_endpoint", baseUrl + AuthorizeResource.RESOURCE_PATH);
+        String tokenEndpoint = baseUrl + TokenResource.RESOURCE_PATH;
+        String userInfoEndpoint = baseUrl + UserInfoResource.RESOURCE_PATH;
+        if (tokenExchangeTopologyName != null) {
+            tokenEndpoint = tokenEndpoint.replaceAll(currentTopologyName, tokenExchangeTopologyName);
+            userInfoEndpoint = userInfoEndpoint.replaceAll(currentTopologyName, tokenExchangeTopologyName);
+        }
+        config.put("token_endpoint", tokenEndpoint);
+        config.put("userinfo_endpoint", userInfoEndpoint);
+        config.put("jwks_uri", baseUrl + JwksResource.RESOURCE_PATH);
+        config.put("response_types_supported", new String[]{KnoxIDFConstants.CODE});
+        config.put("grant_types_supported", new String[]{KnoxIDFConstants.AUTH_CODE, KnoxIDFConstants.REFRESH_TOKEN});
+        config.put("scopes_supported", KnoxIDFConstants.DEFAULT_SCOPES);
+        config.put("id_token_signing_alg_values_supported", new String[]{"RS256"});
+        config.put("code_challenge_methods_supported", new String[]{KnoxIDFConstants.PKCE_METHOD_PLAIN, KnoxIDFConstants.PKCE_METHOD_S256});
+        return Response.ok(JsonUtils.renderAsJsonString(config)).build();
+    }
+
+}
diff --git a/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/TokenMetadataType.java b/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/JwksResource.java
similarity index 55%
copy from gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/TokenMetadataType.java
copy to gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/JwksResource.java
index 17e82e0af..47f802b04 100644
--- a/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/TokenMetadataType.java
+++ b/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/JwksResource.java
@@ -5,19 +5,35 @@
  * licenses this file to you under the Apache License, Version 2.0 (the
  * "License"); you may not use this file except in compliance with the License.
  * You may obtain a copy of the License at
- *
+ * <p>
  * http://www.apache.org/licenses/LICENSE-2.0
- *
+ * <p>
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
  * License for the specific language governing permissions and limitations under
  * the License.
  */
-package org.apache.knox.gateway.services.security.token;
+package org.apache.knox.gateway.service.knoxidf;
 
-public enum TokenMetadataType {
+import org.apache.knox.gateway.service.knoxtoken.JWKSResource;
 
-  JWT, KNOXSSO_COOKIE, CLIENT_ID, API_KEY;
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
 
+import static org.apache.knox.gateway.util.knoxidf.KnoxIDFConstants.BASE_RESORCE_PATH;
+
+@Path(JwksResource.RESOURCE_PATH)
+@Produces(MediaType.APPLICATION_JSON)
+public class JwksResource extends JWKSResource {
+    static final String RESOURCE_PATH = BASE_RESORCE_PATH + "/jwks";
+
+    @GET
+    public Response getKeys() {
+        return getJwksResponse();
+    }
 }
+
diff --git a/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/OIDCScope.java b/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/OIDCScope.java
new file mode 100644
index 000000000..d673f3f45
--- /dev/null
+++ b/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/OIDCScope.java
@@ -0,0 +1,66 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. The ASF
+ * licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * <p>
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * <p>
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package org.apache.knox.gateway.service.knoxidf;
+
+import org.apache.commons.lang3.StringUtils;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Locale;
+import java.util.Set;
+
+public enum OIDCScope {
+    OPENID(new HashSet<>(Collections.singletonList("sub"))),
+    PROFILE(new HashSet<>(Arrays.asList(
+            "name", "family_name", "given_name",
+            "middle_name", "nickname", "preferred_username",
+            "profile", "picture", "website", "gender",
+            "birthdate", "zoneinfo", "locale", "updated_at"
+    ))),
+    EMAIL(new HashSet<>(Arrays.asList("email", "email_verified"))),
+    ADDRESS(new HashSet<>(Collections.singletonList("address"))),
+    PHONE(new HashSet<>(Arrays.asList("phone_number", "phone_number_verified"))),
+    ROLES(new HashSet<>(Collections.singletonList("roles"))); // custom extension
+
+    private final Set<String> claims;
+
+    OIDCScope(Set<String> claims) {
+        this.claims = Collections.unmodifiableSet(new HashSet<>(claims));
+    }
+
+    public Set<String> getClaims() {
+        return claims;
+    }
+
+    public static Set<String> claimsForScopes(String scopeString) {
+        Set<String> result = new HashSet<>();
+        if (StringUtils.isEmpty(scopeString)) {
+            return result;
+        }
+
+        for (String s : scopeString.split("\\s+")) {
+            try {
+                OIDCScope scope = OIDCScope.valueOf(s.toUpperCase(Locale.US));
+                result.addAll(scope.getClaims());
+            } catch (IllegalArgumentException ignored) {
+                // ignore unknown scopes
+            }
+        }
+        return result;
+    }
+}
diff --git a/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/RegistrationResource.java b/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/RegistrationResource.java
new file mode 100644
index 000000000..05675e491
--- /dev/null
+++ b/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/RegistrationResource.java
@@ -0,0 +1,153 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. The ASF
+ * licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * <p>
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * <p>
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package org.apache.knox.gateway.service.knoxidf;
+
+import com.nimbusds.jose.KeyLengthException;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.knox.gateway.service.knoxtoken.ClientCredentialsResource;
+import org.apache.knox.gateway.services.ServiceLifecycleException;
+import org.apache.knox.gateway.services.security.AliasServiceException;
+import org.apache.knox.gateway.services.security.token.TokenMetadata;
+import org.glassfish.jersey.process.internal.RequestScoped;
+
+import javax.annotation.PostConstruct;
+import javax.servlet.ServletException;
+import javax.ws.rs.Consumes;
+import javax.ws.rs.FormParam;
+import javax.ws.rs.GET;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+
+import static org.apache.knox.gateway.util.knoxidf.KnoxIDFConstants.BASE_RESORCE_PATH;
+import static org.apache.knox.gateway.util.knoxidf.KnoxIDFConstants.DEFAULT_SCOPES;
+import static org.apache.knox.gateway.util.knoxidf.KnoxIDFUtils.error;
+
+@Path(RegistrationResource.RESOURCE_PATH)
+@RequestScoped //this is important because redirectUris/allowedScopes are part of the state of this class
+public class RegistrationResource extends ClientCredentialsResource {
+
+    static final String RESOURCE_PATH = BASE_RESORCE_PATH + "/client";
+    private List<String> redirectUris;
+    private List<String> allowedScopes;
+
+    @PostConstruct
+    @Override
+    public void init() throws ServletException, AliasServiceException, ServiceLifecycleException, KeyLengthException {
+        super.init();
+    }
+
+    @Override
+    @GET
+    public Response doGet() {
+        throw new UnsupportedOperationException("Not supported yet.");
+    }
+
+    @Override
+    @POST
+    public Response doPost() {
+        throw new UnsupportedOperationException("Not supported yet.");
+    }
+
+    @Path("/register")
+    @POST
+    @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
+    public Response registerClient(@FormParam("redirect_uris") String redirectUris,
+                                   @FormParam("allowed_scopes") String allowedScopes) {
+        this.redirectUris = Arrays.asList(redirectUris.split(","));
+        final Response redirectUriVerificationResponse = verifyRedirectUris();
+        if (redirectUriVerificationResponse != null) {
+            return redirectUriVerificationResponse;
+        }
+
+        if (StringUtils.isBlank(allowedScopes)) {
+            this.allowedScopes = new ArrayList<>(DEFAULT_SCOPES);
+        } else {
+            this.allowedScopes = Arrays.asList(allowedScopes.split(","));
+            if (!this.allowedScopes.contains("openid")) {
+                return error("invalid_request", "allowed_scopes must include 'openid'");
+            }
+        }
+        return super.doPost();
+    }
+
+    private Response verifyRedirectUris() {
+        if (redirectUris == null || redirectUris.isEmpty()) {
+            return error("invalid_request", "redirect_uris must be provided");
+        }
+
+        for (String uriStr : redirectUris) {
+            URI uri;
+            try {
+                uri = new URI(uriStr);
+            } catch (URISyntaxException e) {
+                return error("invalid_request", "Invalid redirect URI: " + uriStr);
+            }
+
+            // Scheme check
+            if (!"https".equalsIgnoreCase(uri.getScheme()) && !"http".equalsIgnoreCase(uri.getScheme())) {
+                return error("invalid_request", "Redirect URI must use HTTPS or HTTP as scheme: " + uriStr);
+            }
+
+            // Host check (no wildcard allowed)
+            if (uri.getHost() == null || uri.getHost().contains("*")) {
+                return error("invalid_request", "Wildcard not allowed in host: " + uriStr);
+            }
+
+            // Path wildcard check
+            String path = uri.getPath();
+            if (path != null && path.contains("*") && !path.endsWith("*")) {
+                return error("invalid_request", "Wildcard '*' only allowed at end of path: " + uriStr);
+            }
+
+            // Query/fragment check
+            if ((uri.getQuery() != null && uri.getQuery().contains("*")) ||
+                    (uri.getFragment() != null && uri.getFragment().contains("*"))) {
+                return error("invalid_request", "Wildcard '*' not allowed in query or fragment: " + uriStr);
+            }
+        }
+        return null;
+    }
+
+    @Override
+    protected void addArbitraryTokenMetadata(TokenMetadata tokenMetadata) {
+        tokenMetadata.add("redirect_uris", getRedirectUris());
+        tokenMetadata.add("allowed_scopes", getAllowedScopes().replaceAll(",", " "));
+        super.addArbitraryTokenMetadata(tokenMetadata);
+    }
+
+    @Override
+    protected void decorateResponseMap(Map<String, Object> responseMap) {
+        responseMap.put("redirect_uris", getRedirectUris());
+        responseMap.put("allowed_scopes", getAllowedScopes());
+    }
+
+    private String getRedirectUris() {
+        return String.join(",", redirectUris);
+    }
+
+    private String getAllowedScopes() {
+        return String.join(",", allowedScopes);
+    }
+}
diff --git a/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/TokenResource.java b/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/TokenResource.java
new file mode 100644
index 000000000..54e697eb9
--- /dev/null
+++ b/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/TokenResource.java
@@ -0,0 +1,452 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. The ASF
+ * licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * <p>
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * <p>
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package org.apache.knox.gateway.service.knoxidf;
+
+import com.nimbusds.jose.KeyLengthException;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.knox.gateway.service.knoxidf.userparams.UserParamsProvider;
+import org.apache.knox.gateway.service.knoxidf.userparams.UserParamsProviderFactory;
+import org.apache.knox.gateway.service.knoxtoken.PasscodeTokenResourceBase;
+import org.apache.knox.gateway.services.GatewayServices;
+import org.apache.knox.gateway.services.ServiceLifecycleException;
+import org.apache.knox.gateway.services.ServiceType;
+import org.apache.knox.gateway.services.knoxidf.federation.FederatedIdentity;
+import org.apache.knox.gateway.services.knoxidf.federation.FederatedIdentityService;
+import org.apache.knox.gateway.services.security.AliasServiceException;
+import org.apache.knox.gateway.services.security.token.JWTokenAttributesBuilder;
+import org.apache.knox.gateway.services.security.token.JWTokenAuthority;
+import org.apache.knox.gateway.services.security.token.TokenMetadata;
+import org.apache.knox.gateway.services.security.token.TokenMetadataType;
+import org.apache.knox.gateway.services.security.token.TokenServiceException;
+import org.apache.knox.gateway.services.security.token.TokenUtils;
+import org.apache.knox.gateway.services.security.token.UnknownTokenException;
+import org.apache.knox.gateway.services.security.token.impl.JWT;
+import org.apache.knox.gateway.util.ServletRequestUtils;
+
+import javax.annotation.PostConstruct;
+import javax.servlet.ServletContext;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.text.ParseException;
+import java.util.Base64;
+import java.util.HashMap;
+import java.util.Map;
+
+import static org.apache.knox.gateway.security.CommonTokenConstants.GRANT_TYPE;
+import static org.apache.knox.gateway.util.knoxidf.KnoxIDFConstants.AUTH_CODE;
+import static org.apache.knox.gateway.util.knoxidf.KnoxIDFConstants.BASE_RESORCE_PATH;
+import static org.apache.knox.gateway.util.knoxidf.KnoxIDFConstants.CLIENT_ID;
+import static org.apache.knox.gateway.util.knoxidf.KnoxIDFConstants.CODE;
+import static org.apache.knox.gateway.util.knoxidf.KnoxIDFConstants.CODE_CHALLENGE;
+import static org.apache.knox.gateway.util.knoxidf.KnoxIDFConstants.CODE_CHALLENGE_METHOD;
+import static org.apache.knox.gateway.util.knoxidf.KnoxIDFConstants.CODE_VERIFIER;
+import static org.apache.knox.gateway.util.knoxidf.KnoxIDFConstants.FEDERATED_IDENTITY_ID;
+import static org.apache.knox.gateway.util.knoxidf.KnoxIDFConstants.OFFLINE_ACCESS_SCOPE;
+import static org.apache.knox.gateway.util.knoxidf.KnoxIDFConstants.PKCE_METHOD_PLAIN;
+import static org.apache.knox.gateway.util.knoxidf.KnoxIDFConstants.PKCE_METHOD_S256;
+import static org.apache.knox.gateway.util.knoxidf.KnoxIDFConstants.REDIRECT_URI;
+import static org.apache.knox.gateway.util.knoxidf.KnoxIDFConstants.REFRESH_TOKEN;
+import static org.apache.knox.gateway.util.knoxidf.KnoxIDFConstants.REFRESH_TOKEN_TTL;
+import static org.apache.knox.gateway.util.knoxidf.KnoxIDFConstants.REFRESH_TOKEN_TTL_DEFAULT;
+import static org.apache.knox.gateway.util.knoxidf.KnoxIDFConstants.SCOPE;
+import static org.apache.knox.gateway.util.knoxidf.KnoxIDFUtils.error;
+
+@Path(TokenResource.RESOURCE_PATH)
+@Produces(MediaType.APPLICATION_JSON)
+public class TokenResource extends PasscodeTokenResourceBase {
+    static final String RESOURCE_PATH = BASE_RESORCE_PATH + "/token";
+    private UserParamsProvider userParamsProvider;
+
+    @Context
+    private HttpServletRequest request;
+
+    @Context
+    private ServletContext servletContext;
+
+    private FederatedIdentityService federatedIdentityService;
+    private long refreshTokenTTL;
+
+    @Override
+    public String getPrefix() {
+        return "knoxidf";
+    }
+
+    @PostConstruct
+    @Override
+    public void init() throws ServletException, AliasServiceException, ServiceLifecycleException, KeyLengthException {
+        super.init();
+        this.servletContext = wrapContextForDefaultParams(this.servletContext);
+        this.userParamsProvider = UserParamsProviderFactory.getUserParamsProvider(servletContext);
+        final GatewayServices services = (GatewayServices) servletContext.getAttribute(GatewayServices.GATEWAY_SERVICES_ATTRIBUTE);
+        federatedIdentityService = services.getService(ServiceType.KNOXIDF_FEDERATED_IDENTITY_SERVICE);
+        setRefreshTokenTTL();
+    }
+
+    private void setRefreshTokenTTL() {
+        final String configuredRefreshTokenTTL = servletContext.getInitParameter(REFRESH_TOKEN_TTL);
+        if (StringUtils.isNotBlank(configuredRefreshTokenTTL)) {
+            this.refreshTokenTTL = Long.parseLong(configuredRefreshTokenTTL);
+        } else {
+            refreshTokenTTL = REFRESH_TOKEN_TTL_DEFAULT;
+        }
+    }
+
+    @Override
+    @POST
+    public Response doPost() {
+        final String grantType = getRequestParam(GRANT_TYPE);
+        if (REFRESH_TOKEN.equals(grantType)) {
+            return handleRefreshToken();
+        } else if (AUTH_CODE.equals(grantType)) {
+            return handleAuthorizationCodeFlow();
+        }
+        return error("invalid_request", "invalid grant type: " + grantType);
+    }
+
+    @Override
+    protected UserContext buildUserContext(HttpServletRequest request) {
+        try {
+            final String code = getRequestParam(CODE);
+            final TokenMetadata tokenMetadata = tokenStateService.getTokenMetadata(code);
+            final String scope = tokenMetadata.getMetadata(SCOPE);
+            final Map<String, Object> userParams = userParamsProvider.getParamsFor(tokenMetadata.getUserName(), scope);
+            userParams.put(SCOPE, scope);
+            return new UserContext(tokenMetadata.getUserName(), null, userParams);
+        } catch (UnknownTokenException e) {
+            //this should not happen as we have just validated the auth code
+            throw new RuntimeException(e);
+        }
+    }
+
+    @Override
+    protected void addArbitraryTokenMetadata(TokenMetadata tokenMetadata) {
+        try {
+            super.addArbitraryTokenMetadata(tokenMetadata);
+            final String code = getRequestParam(CODE);
+            if (StringUtils.isNotBlank(code)) {
+                final TokenMetadata authCodeTokenMetadata = tokenStateService.getTokenMetadata(code);
+
+                //if the auth code token was a result of a federated OIDC call, we need to save the associated
+                //federated identity ID in the JWT too (so that it can be looked up while fetching user info)
+                final String federatedIdentityId = authCodeTokenMetadata.getMetadata(FEDERATED_IDENTITY_ID);
+                if (StringUtils.isNotBlank(federatedIdentityId)) {
+                    tokenMetadata.add(FEDERATED_IDENTITY_ID, federatedIdentityId);
+                }
+            }
+        } catch (UnknownTokenException e) {
+            //this should not happen as we have just validated the auth code
+            throw new RuntimeException(e);
+        }
+    }
+
+    @Override
+    protected ResponseMap buildResponseMap(JWT token, long expires) throws TokenServiceException {
+        final ResponseMap responseMap = super.buildResponseMap(token, expires);
+
+        final String code = getRequestParam(CODE);
+        TokenMetadata authCodeTokenMetadata = null;
+        if (StringUtils.isNotBlank(code)) {
+            try {
+                authCodeTokenMetadata = tokenStateService.getTokenMetadata(code);
+            } catch (UnknownTokenException e) {
+                //NOP
+            }
+        }
+
+        responseMap.map.put("id_token", generateIdToken(token, authCodeTokenMetadata));
+
+        final String refreshToken = generateRefreshToken(token);
+        if (StringUtils.isNotBlank(refreshToken)) {
+            responseMap.map.put(REFRESH_TOKEN, refreshToken);
+        }
+
+        return responseMap;
+    }
+
+    private Response handleRefreshToken() {
+        try {
+            final String refreshTokenParam = getRequestParam(REFRESH_TOKEN);
+            final String refreshTokenId = TokenUtils.getTokenId(refreshTokenParam);
+            final TokenMetadata refreshTokenMetadata = tokenStateService.getTokenMetadata(refreshTokenId);
+            validateRefreshTokenGrant(refreshTokenParam, refreshTokenId, refreshTokenMetadata);
+            // Valid refresh token -> issue new access token and new refresh token (rotation)
+            final String userName = refreshTokenMetadata.getUserName();
+            final String scope = refreshTokenMetadata.getMetadata(SCOPE);
+            final Map<String, Object> userParams = userParamsProvider.getParamsFor(userName, scope);
+            userParams.put(SCOPE, scope);
+
+            // Revoke old refresh token (rotation)
+            tokenStateService.revokeToken(refreshTokenId);
+
+            // Build new tokens
+            final UserContext userContext = new UserContext(userName, null, userParams);
+            final TokenResponseContext resp = getTokenResponse(userContext);
+            return resp.build();
+        } catch (ParseException e) {
+            return error("invalid_grant", "Malformed refresh_token");
+        } catch (UnknownTokenException e) {
+            return error("invalid_grant", "Unknown refresh_token");
+        } catch (RefreshTokenValidationError e) {
+            return error("Refresh token validation error", e.getMessage());
+        }
+
+    }
+
+    private void validateRefreshTokenGrant(String refreshTokenParam, String refreshTokenId, TokenMetadata refreshTokenMetadata) throws UnknownTokenException, RefreshTokenValidationError {
+        final String clientId = getRequestParam(CLIENT_ID);
+
+        if (StringUtils.isBlank(refreshTokenParam)) {
+            throw new RefreshTokenValidationError("Invalid request: Missing refresh_token");
+        }
+
+        if (StringUtils.isBlank(clientId)) {
+            throw new RefreshTokenValidationError("Invalid request: Missing client_id");
+        }
+
+        if (refreshTokenMetadata == null || !TokenMetadataType.REFRESH_TOKEN.name().equals(refreshTokenMetadata.getType())) {
+            throw new RefreshTokenValidationError("Invalid grant: invalid refresh_token");
+        }
+
+        if (tokenStateService.getTokenExpiration(refreshTokenId) <= System.currentTimeMillis()) {
+            throw new RefreshTokenValidationError("Invalid grant: Refresh token expired");
+        }
+
+        final String associatedClientId = refreshTokenMetadata.getMetadata(CLIENT_ID);
+        if (!clientId.equals(associatedClientId)) {
+            throw new RefreshTokenValidationError("Invalid grant: client_id mismatch");
+        }
+    }
+
+    private Response handleAuthorizationCodeFlow() {
+        final String code = getRequestParam(CODE);
+        final String redirectUri = getRequestParam(REDIRECT_URI);
+
+        try {
+            validateAuthCode(code, redirectUri);
+            return getAuthenticationToken();
+        } catch (AuthTokenValidationError e) {
+            return error("Auth code validation error", e.getMessage());
+        } finally {
+            try {
+                tokenStateService.revokeToken(code);
+            } catch (UnknownTokenException e) {
+                //NOP: this should have been handled by the above UnknownTokenException already
+            }
+        }
+    }
+
+    private void validateAuthCode(String code, String redirectUri) throws AuthTokenValidationError {
+        try {
+            if (code == null || code.isEmpty()) {
+                throw new AuthTokenValidationError("Invalid request: missing code");
+            }
+
+            if (redirectUri == null || redirectUri.isEmpty()) {
+                throw new AuthTokenValidationError("Invalid request: missing redirect_uri");
+            }
+
+            final TokenMetadata authCodeTokenMetadata = tokenStateService.getTokenMetadata(code);
+            final String associateRedirectUri = authCodeTokenMetadata.getMetadata(REDIRECT_URI);
+            if (!authCodeTokenMetadata.isAuthCode()) {
+                throw new AuthTokenValidationError("Invalid auth_code: not an auth code token");
+            } else if (tokenStateService.getTokenExpiration(code) <= System.currentTimeMillis()) {
+                throw new AuthTokenValidationError("Invalid auth_code: expired");
+            } else if (!associateRedirectUri.equals(redirectUri)) {
+                throw new AuthTokenValidationError("Invalid redirect_uri: " + redirectUri);
+            } else {
+                final String associatedClientId = authCodeTokenMetadata.getMetadata(CLIENT_ID);
+                final String clientId = getRequestParam(CLIENT_ID);
+                if (!associatedClientId.equals(clientId)) {
+                    throw new AuthTokenValidationError("Invalid client_id: " + clientId);
+                }
+            }
+
+            // PKCE validation
+            final String codeChallenge = authCodeTokenMetadata.getMetadata(CODE_CHALLENGE);
+            if (StringUtils.isNotBlank(codeChallenge)) {
+                final String codeChallengeMethod = authCodeTokenMetadata.getMetadata(CODE_CHALLENGE_METHOD);
+                final String codeVerifier = getRequestParam(CODE_VERIFIER);
+                if (StringUtils.isBlank(codeVerifier)) {
+                    throw new AuthTokenValidationError("Missing code_verifier");
+                }
+                if (!validatePKCE(codeVerifier, codeChallenge, codeChallengeMethod)) {
+                    throw new AuthTokenValidationError("Invalid code_verifier");
+                }
+            }
+        } catch (UnknownTokenException e) {
+            throw new AuthTokenValidationError("Unknown auth_code");
+        }
+    }
+
+    private boolean validatePKCE(String codeVerifier, String codeChallenge, String method) {
+        if (PKCE_METHOD_PLAIN.equals(method)) {
+            return codeVerifier.equals(codeChallenge);
+        } else if (PKCE_METHOD_S256.equals(method)) {
+            try {
+                return generateS256Challenge(codeVerifier).equals(codeChallenge);
+            } catch (NoSuchAlgorithmException e) {
+                return false;
+            }
+        }
+        return false;
+    }
+
+    private String generateS256Challenge(String codeVerifier) throws NoSuchAlgorithmException {
+        MessageDigest md = MessageDigest.getInstance("SHA-256");
+        byte[] digest = md.digest(codeVerifier.getBytes(StandardCharsets.UTF_8));
+        return Base64.getUrlEncoder().withoutPadding().encodeToString(digest);
+    }
+
+    private String generateIdToken(JWT accessToken, TokenMetadata authCodeTokenMetadata) throws TokenServiceException {
+        final boolean hasFederatedIdToken = authCodeTokenMetadata != null && StringUtils.isNotBlank(authCodeTokenMetadata.getMetadata(FEDERATED_IDENTITY_ID));
+
+        if (hasFederatedIdToken) {
+            return generateFederatedIdToken(accessToken, authCodeTokenMetadata);
+        } else {
+            return generateLocalIdToken(accessToken, authCodeTokenMetadata);
+        }
+    }
+
+    private String generateFederatedIdToken(JWT accessToken, TokenMetadata tokenMetadata) throws TokenServiceException {
+        final String fedIdentityId = tokenMetadata.getMetadata(FEDERATED_IDENTITY_ID);
+        final FederatedIdentity federatedIdentity = federatedIdentityService
+                .findById(fedIdentityId)
+                .orElseThrow(() -> new TokenServiceException("Federated identity not found"));
+
+        final JWTokenAttributesBuilder builder = new JWTokenAttributesBuilder();
+        builder.setAlgorithm(accessToken.getSignatureAlgorithm().getName())
+                .setUserName(federatedIdentity.getUserId())
+                .setIssueTime(System.currentTimeMillis())
+                .setExpires(Long.parseLong(accessToken.getExpires()))
+                .setIssuer(accessToken.getIssuer())
+                .setAudiences(tokenMetadata.getMetadata(CLIENT_ID));
+
+        final Map<String, Object> claims = new HashMap<>(federatedIdentity.getAttributes());
+        claims.keySet().retainAll(AuthorizeResource.ALLOWED_CLAIMS);
+        String nonce = tokenMetadata.getMetadata("nonce");
+        if (StringUtils.isNotBlank(nonce)) {
+            claims.put("nonce", nonce);
+        }
+
+        // Optional: indicate source for auditing/logging
+        claims.put("federated_idp", federatedIdentity.getProvider());
+        claims.put("federated_sub", federatedIdentity.getExternalSubject());
+        claims.put("federated_iss", federatedIdentity.getExternalIssuer());
+
+        builder.setCustomAttributes(claims);
+
+        return issueToken(builder).toString();
+    }
+
+    private String generateLocalIdToken(JWT accessToken, TokenMetadata authCodeTokenMetadata) throws TokenServiceException {
+        final JWTokenAttributesBuilder idTokenAttributesBuilder = new JWTokenAttributesBuilder();
+        idTokenAttributesBuilder
+                .setAlgorithm(accessToken.getSignatureAlgorithm().getName())
+                .setUserName(accessToken.getSubject())
+                .setIssueTime(System.currentTimeMillis())
+                .setExpires(Long.parseLong(accessToken.getExpires()))
+                .setIssuer(accessToken.getIssuer());
+
+        if (authCodeTokenMetadata != null) {
+            final String associatedClientId = authCodeTokenMetadata.getMetadata("client_id");
+            idTokenAttributesBuilder.setAudiences(associatedClientId);
+            final String nonce = authCodeTokenMetadata.getMetadata("nonce");
+            if (StringUtils.isNotBlank(nonce)) {
+                idTokenAttributesBuilder.setCustomAttributes(Map.of("nonce", nonce));
+            }
+        } else {
+            // If there is no auth code (e.g. refresh token grant), we use the client_id from the request
+            idTokenAttributesBuilder.setAudiences(getRequestParam(CLIENT_ID));
+        }
+
+        return issueToken(idTokenAttributesBuilder).toString();
+    }
+
+    private String generateRefreshToken(JWT accessToken) throws TokenServiceException {
+        final String scope = (String) accessToken.getJWTClaimsSet().getClaim(SCOPE);
+        if (StringUtils.isNotBlank(scope) && scope.contains(OFFLINE_ACCESS_SCOPE)) {
+            return issueRefreshToken(accessToken, scope);
+        } else {
+            return null;
+        }
+    }
+
+    private String issueRefreshToken(JWT accessToken, String scope) throws TokenServiceException {
+        final JWTokenAttributesBuilder refreshTokenAttributesBuilder = new JWTokenAttributesBuilder();
+
+        final long issueTime = System.currentTimeMillis();
+        final long expires = issueTime + refreshTokenTTL;
+        final String clientId = getRequestParam(CLIENT_ID);
+
+        refreshTokenAttributesBuilder.setIssuer(accessToken.getIssuer())
+                .setUserName(accessToken.getSubject())
+                .setAlgorithm(accessToken.getSignatureAlgorithm().getName())
+                .setAudiences(clientId)
+                .setIssueTime(issueTime)
+                .setExpires(expires)
+                .setManaged(tokenStateService != null)
+                .setType(TokenMetadataType.REFRESH_TOKEN.name());
+
+        final JWT refreshToken = issueToken(refreshTokenAttributesBuilder);
+
+        if (tokenStateService != null) {
+            final String tokenId = TokenUtils.getTokenId(refreshToken);
+            tokenStateService.addToken(tokenId, issueTime, expires, tokenStateService.getDefaultMaxLifetimeDuration());
+            final TokenMetadata metadata = new TokenMetadata(refreshToken.getSubject());
+            metadata.setType(TokenMetadataType.REFRESH_TOKEN);
+            metadata.add("client_id", clientId);
+            metadata.add("scope", scope);
+            tokenStateService.addMetadata(tokenId, metadata);
+        }
+
+        return refreshToken.toString();
+    }
+
+    private JWT issueToken(final JWTokenAttributesBuilder builder) throws TokenServiceException {
+        final JWTokenAuthority ts = getGatewayServices().getService(ServiceType.TOKEN_SERVICE);
+        return ts.issueToken(builder.build());
+    }
+
+    private String getRequestParam(String paramName) {
+        String requestParamValue = request.getParameter(paramName);
+        if (requestParamValue == null) {
+            requestParamValue = ServletRequestUtils.unwrapHttpServletRequest(request).getParameter(paramName);
+        }
+        return requestParamValue;
+    }
+
+    private static class AuthTokenValidationError extends Exception {
+        AuthTokenValidationError(String message) {
+            super(message);
+        }
+    }
+
+    private static class RefreshTokenValidationError extends Exception {
+        RefreshTokenValidationError(String message) {
+            super(message);
+        }
+    }
+}
diff --git a/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/UserInfoResource.java b/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/UserInfoResource.java
new file mode 100644
index 000000000..c837605ae
--- /dev/null
+++ b/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/UserInfoResource.java
@@ -0,0 +1,141 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. The ASF
+ * licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * <p>
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * <p>
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package org.apache.knox.gateway.service.knoxidf;
+
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.knox.gateway.service.knoxidf.userparams.UserParamsProvider;
+import org.apache.knox.gateway.service.knoxidf.userparams.UserParamsProviderFactory;
+import org.apache.knox.gateway.services.GatewayServices;
+import org.apache.knox.gateway.services.ServiceType;
+import org.apache.knox.gateway.services.knoxidf.federation.FederatedIdentity;
+import org.apache.knox.gateway.services.knoxidf.federation.FederatedIdentityService;
+import org.apache.knox.gateway.services.security.token.TokenMetadata;
+import org.apache.knox.gateway.services.security.token.TokenServiceException;
+import org.apache.knox.gateway.services.security.token.TokenStateService;
+import org.apache.knox.gateway.services.security.token.UnknownTokenException;
+import org.apache.knox.gateway.util.JsonUtils;
+
+import javax.annotation.PostConstruct;
+import javax.servlet.ServletContext;
+import javax.servlet.http.HttpServletRequest;
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+import static org.apache.knox.gateway.util.knoxidf.KnoxIDFConstants.BASE_RESORCE_PATH;
+import static org.apache.knox.gateway.util.knoxidf.KnoxIDFConstants.SCOPE_ATTRIBUTE;
+import static org.apache.knox.gateway.util.knoxidf.KnoxIDFConstants.TOKEN_ID_ATTRIBUTE;
+import static org.apache.knox.gateway.util.knoxidf.KnoxIDFUtils.error;
+
+
+@Path(UserInfoResource.RESOURCE_PATH)
+@Produces(MediaType.APPLICATION_JSON)
+public class UserInfoResource {
+
+    static final String RESOURCE_PATH = BASE_RESORCE_PATH + "/userinfo";
+    private UserParamsProvider userParamsProvider;
+
+    @Context
+    private ServletContext servletContext;
+
+    @Context
+    private HttpServletRequest request;
+
+    private FederatedIdentityService federatedIdentityService;
+
+    @PostConstruct
+    public void init() {
+        this.userParamsProvider = UserParamsProviderFactory.getUserParamsProvider(servletContext);
+        final GatewayServices services = (GatewayServices) servletContext.getAttribute(GatewayServices.GATEWAY_SERVICES_ATTRIBUTE);
+        federatedIdentityService = services.getService(ServiceType.KNOXIDF_FEDERATED_IDENTITY_SERVICE);
+    }
+
+    public Response doGet() {
+        try {
+            return getUserInfo();
+        } catch (UnknownTokenException | TokenServiceException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    public Response doPost() {
+        throw new UnsupportedOperationException();
+    }
+
+    @GET
+    @Produces(MediaType.APPLICATION_JSON)
+    public Response getUserInfo() throws UnknownTokenException, TokenServiceException {
+        final String tokenId = request.getAttribute(TOKEN_ID_ATTRIBUTE) == null ? null : request.getAttribute(TOKEN_ID_ATTRIBUTE).toString();
+        if (tokenId == null) {
+            return error("invalid_request", "Cannot find tokenId");
+        }
+
+        final String scope = request.getAttribute(SCOPE_ATTRIBUTE) == null ? "" : request.getAttribute(SCOPE_ATTRIBUTE).toString();
+        final TokenMetadata tokenMetadata = getReadonlyTokenStateService().getTokenMetadata(tokenId);
+        final Map<String, Object> userInfo = new HashMap<>();
+
+        // Check if this token has a federated identity
+        final String federatedIdentityId = tokenMetadata.getMetadata("federated_identity_id");
+
+        if (StringUtils.isNotBlank(federatedIdentityId)) {
+            // Federated user
+            final FederatedIdentity federatedIdentity = federatedIdentityService
+                    .findById(federatedIdentityId)
+                    .orElseThrow(() -> new TokenServiceException("Federated identity not found"));
+
+            // Include only allowed claims
+            Map<String, Object> claims = federatedIdentity.getAttributes().entrySet().stream()
+                    .filter(e -> AuthorizeResource.ALLOWED_CLAIMS.contains(e.getKey()))
+                    .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
+
+            // Mandatory claims for OIDC
+            claims.put("sub", federatedIdentity.getUserId()); // internal Knox subject
+            claims.put("idp", federatedIdentity.getProvider());
+
+            // Optional: federated info for auditing
+            claims.put("federated_sub", federatedIdentity.getExternalSubject());
+            claims.put("federated_iss", federatedIdentity.getExternalIssuer());
+
+            // Add nonce if available
+            String nonce = tokenMetadata.getMetadata("nonce");
+            if (StringUtils.isNotBlank(nonce)) {
+                claims.put("nonce", nonce);
+            }
+
+            userInfo.putAll(claims);
+        } else {
+            // Local Knox user
+            userInfo.putAll(userParamsProvider.getParamsFor(tokenMetadata.getUserName(), scope));
+        }
+
+        return Response.ok(JsonUtils.renderAsJsonString(userInfo, true)).build();
+    }
+
+    private TokenStateService getReadonlyTokenStateService() {
+        GatewayServices services = (GatewayServices) servletContext.getAttribute(GatewayServices.GATEWAY_SERVICES_ATTRIBUTE);
+        return services.getService(ServiceType.TOKEN_STATE_SERVICE);
+    }
+
+}
+
diff --git a/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/TokenMetadataType.java b/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/deploy/KnoxIDFServiceDeploymentContributor.java
similarity index 54%
copy from gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/TokenMetadataType.java
copy to gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/deploy/KnoxIDFServiceDeploymentContributor.java
index 17e82e0af..cfbb4647c 100644
--- a/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/TokenMetadataType.java
+++ b/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/deploy/KnoxIDFServiceDeploymentContributor.java
@@ -5,19 +5,38 @@
  * licenses this file to you under the Apache License, Version 2.0 (the
  * "License"); you may not use this file except in compliance with the License.
  * You may obtain a copy of the License at
- *
+ * <p>
  * http://www.apache.org/licenses/LICENSE-2.0
- *
+ * <p>
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
  * License for the specific language governing permissions and limitations under
  * the License.
  */
-package org.apache.knox.gateway.services.security.token;
+package org.apache.knox.gateway.service.knoxidf.deploy;
 
-public enum TokenMetadataType {
+import org.apache.knox.gateway.jersey.JerseyServiceDeploymentContributorBase;
 
-  JWT, KNOXSSO_COOKIE, CLIENT_ID, API_KEY;
+public class KnoxIDFServiceDeploymentContributor extends JerseyServiceDeploymentContributorBase {
 
+    @Override
+    public String getRole() {
+        return "KNOXIDF";
+    }
+
+    @Override
+    public String getName() {
+        return "KnoxIdentityFederation";
+    }
+
+    @Override
+    protected String[] getPackages() {
+        return new String[] { "org.apache.knox.gateway.service.knoxidf" };
+    }
+
+    @Override
+    protected String[] getPatterns() {
+        return new String[] { "knoxidf/api/**?**" };
+    }
 }
diff --git a/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/TokenMetadataType.java b/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/userparams/EmptyUserParamsProvider.java
similarity index 71%
copy from gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/TokenMetadataType.java
copy to gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/userparams/EmptyUserParamsProvider.java
index 17e82e0af..f39d3eb77 100644
--- a/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/TokenMetadataType.java
+++ b/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/userparams/EmptyUserParamsProvider.java
@@ -5,19 +5,24 @@
  * licenses this file to you under the Apache License, Version 2.0 (the
  * "License"); you may not use this file except in compliance with the License.
  * You may obtain a copy of the License at
- *
+ * <p>
  * http://www.apache.org/licenses/LICENSE-2.0
- *
+ * <p>
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
  * License for the specific language governing permissions and limitations under
  * the License.
  */
-package org.apache.knox.gateway.services.security.token;
+package org.apache.knox.gateway.service.knoxidf.userparams;
 
-public enum TokenMetadataType {
+import java.util.HashMap;
+import java.util.Map;
 
-  JWT, KNOXSSO_COOKIE, CLIENT_ID, API_KEY;
+public class EmptyUserParamsProvider implements UserParamsProvider {
 
+    @Override
+    public Map<String, Object> getParamsFor(String subjectName, String scope) {
+        return new HashMap<>();
+    }
 }
diff --git a/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/userparams/LdapUserParamsProvider.java b/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/userparams/LdapUserParamsProvider.java
new file mode 100644
index 000000000..a17248451
--- /dev/null
+++ b/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/userparams/LdapUserParamsProvider.java
@@ -0,0 +1,201 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. The ASF
+ * licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * <p>
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * <p>
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package org.apache.knox.gateway.service.knoxidf.userparams;
+
+import org.apache.knox.gateway.service.knoxidf.OIDCScope;
+import org.apache.knox.gateway.services.GatewayServices;
+import org.apache.knox.gateway.services.ServiceType;
+import org.apache.knox.gateway.services.security.AliasService;
+import org.apache.knox.gateway.services.security.AliasServiceException;
+
+import javax.naming.Context;
+import javax.naming.NamingEnumeration;
+import javax.naming.directory.Attribute;
+import javax.naming.directory.Attributes;
+import javax.naming.directory.SearchControls;
+import javax.naming.directory.SearchResult;
+import javax.naming.ldap.InitialLdapContext;
+import javax.naming.ldap.LdapContext;
+import javax.servlet.ServletContext;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Hashtable;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+
+public class LdapUserParamsProvider implements UserParamsProvider {
+    private static final String PREFIX = "user.params.provider.ldap.";
+    static final String LDAP_URL = PREFIX + "url";
+    private static final String LDAP_BASE_DN = PREFIX + "baseDn";
+    private static final String LDAP_USER_DN_TEMPLATE = PREFIX + "userDnTemplate";
+    private static final String LDAP_SYSTEM_USER = PREFIX + "systemUser";
+    private static final String LDAP_SYSTEM_PASSWORD_ALIAS = PREFIX + "systemPasswordAlias";
+
+    // === Defaults point to Knox's demo LDAP ===
+    private static final String DEFAULT_BASE_DN = "dc=hadoop,dc=apache,dc=org";
+    private static final String DEFAULT_USER_DN_TEMPLATE = "uid=%s,ou=people," + DEFAULT_BASE_DN;
+    private static final String DEFAULT_SYSTEM_USER = "uid=admin,ou=people," + DEFAULT_BASE_DN;
+    private static final String DEFAULT_SYSTEM_PASSWORD = "admin-password";
+
+    private static final String[] ATTRIBUTES = {"cn", "sn", "givenName", "mail"};
+
+    private final String ldapUrl;
+    private final String ldapBaseDn;
+    private final String ldapUserDnTemplate;
+    private final String ldapSystemUser;
+    private final String ldapSystemPassword;
+
+    LdapUserParamsProvider(ServletContext servletContext) {
+        this.ldapUrl = servletContext.getInitParameter(LDAP_URL);
+        this.ldapBaseDn = getInitParamOrDefault(servletContext, LDAP_BASE_DN, DEFAULT_BASE_DN);
+        this.ldapUserDnTemplate = getInitParamOrDefault(servletContext, LDAP_USER_DN_TEMPLATE, DEFAULT_USER_DN_TEMPLATE);
+        this.ldapSystemUser = getInitParamOrDefault(servletContext, LDAP_SYSTEM_USER, DEFAULT_SYSTEM_USER);
+        this.ldapSystemPassword = getSystemPassword(servletContext);
+    }
+
+    private String getInitParamOrDefault(ServletContext servletContext, String key, String defaultValue) {
+        final String value = servletContext.getInitParameter(key);
+        return value == null ? defaultValue : value;
+    }
+
+    private String getSystemPassword(ServletContext servletContext) {
+        final GatewayServices services = (GatewayServices) servletContext.getAttribute(GatewayServices.GATEWAY_SERVICES_ATTRIBUTE);
+        final AliasService aliasService = services.getService(ServiceType.ALIAS_SERVICE);
+        try {
+            final char[] systemPassword = aliasService.getPasswordFromAliasForGateway(LDAP_SYSTEM_PASSWORD_ALIAS);
+            return systemPassword == null ? DEFAULT_SYSTEM_PASSWORD : new String(systemPassword);
+        } catch (AliasServiceException e) {
+            return DEFAULT_SYSTEM_PASSWORD;
+        }
+    }
+
+    @Override
+    public Map<String, Object> getParamsFor(String subjectName, String scope) {
+        Map<String, Object> userParams = new HashMap<>();
+        if ("anonymous".equalsIgnoreCase(subjectName)) {
+            return userParams;
+        }
+
+        Set<String> requestedClaims = OIDCScope.claimsForScopes(scope);
+
+        LdapContext ctx = null;
+        try {
+            ctx = createSystemContext();
+
+            String userDn = String.format(Locale.US, ldapUserDnTemplate, subjectName);
+
+            SearchControls controls = new SearchControls();
+            controls.setSearchScope(SearchControls.OBJECT_SCOPE);
+            controls.setReturningAttributes(ATTRIBUTES);
+
+            NamingEnumeration<SearchResult> results = ctx.search(userDn, "(objectClass=*)", controls);
+            if (results.hasMore()) {
+                SearchResult sr = results.next();
+                Attributes attrs = sr.getAttributes();
+
+                // --- OIDC standard claims ---
+                if (requestedClaims.contains("sub")) {
+                    userParams.put("sub", subjectName);
+                }
+                if (requestedClaims.contains("name")) {
+                    userParams.put("name", getAttr(attrs, "cn"));
+                }
+                if (requestedClaims.contains("family_name")) {
+                    userParams.put("family_name", getAttr(attrs, "sn"));
+                }
+                if (requestedClaims.contains("given_name")) {
+                    userParams.put("given_name", getAttr(attrs, "givenName"));
+                }
+                if (requestedClaims.contains("email")) {
+                    userParams.put("email", getAttr(attrs, "mail"));
+                }
+                if (requestedClaims.contains("email_verified")) {
+                    userParams.put("email_verified", Boolean.TRUE);
+                }
+
+                // --- Custom: roles ---
+                if (requestedClaims.contains("roles")) {
+                    List<String> roles = fetchRoles(ctx, userDn);
+                    userParams.put("roles", roles);
+                }
+            }
+
+        } catch (Exception e) {
+            throw new RuntimeException("Failed to fetch user parameters for " + subjectName, e);
+        } finally {
+            closeContext(ctx);
+        }
+
+        return userParams;
+    }
+
+    private List<String> fetchRoles(LdapContext ctx, String userDn) throws Exception {
+        List<String> roles = new ArrayList<>();
+
+        SearchControls groupControls = new SearchControls();
+        groupControls.setSearchScope(SearchControls.ONELEVEL_SCOPE);
+        groupControls.setReturningAttributes(new String[]{"cn", "member"});
+
+        String groupsBase = "ou=groups," + ldapBaseDn;
+        NamingEnumeration<SearchResult> groupResults =
+                ctx.search(groupsBase, "(objectClass=groupOfNames)", groupControls);
+
+        while (groupResults.hasMore()) {
+            SearchResult group = groupResults.next();
+            Attributes groupAttrs = group.getAttributes();
+            Attribute members = groupAttrs.get("member");
+            if (members != null) {
+                NamingEnumeration<?> e = members.getAll();
+                while (e.hasMore()) {
+                    String memberDn = (String) e.next();
+                    if (memberDn.equalsIgnoreCase(userDn)) {
+                        roles.add(getAttr(groupAttrs, "cn"));
+                        break;
+                    }
+                }
+            }
+        }
+        return roles;
+    }
+
+    private LdapContext createSystemContext() throws Exception {
+        Hashtable<String, Object> env = new Hashtable<>();
+        env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
+        env.put(Context.PROVIDER_URL, ldapUrl);
+        env.put(Context.SECURITY_AUTHENTICATION, "simple");
+        env.put(Context.SECURITY_PRINCIPAL, ldapSystemUser);
+        env.put(Context.SECURITY_CREDENTIALS, ldapSystemPassword);
+        return new InitialLdapContext(env, null);
+    }
+
+    private String getAttr(Attributes attrs, String attrName) throws Exception {
+        Attribute attr = attrs.get(attrName);
+        return attr != null ? (String) attr.get() : null;
+    }
+
+    private void closeContext(LdapContext ctx) {
+        if (ctx != null) {
+            try {
+                ctx.close();
+            } catch (Exception ignored) {
+            }
+        }
+    }
+}
+
diff --git a/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/TokenMetadataType.java b/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/userparams/UserParamsProvider.java
similarity index 65%
copy from gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/TokenMetadataType.java
copy to gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/userparams/UserParamsProvider.java
index 17e82e0af..d052ef040 100644
--- a/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/TokenMetadataType.java
+++ b/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/userparams/UserParamsProvider.java
@@ -5,19 +5,26 @@
  * licenses this file to you under the Apache License, Version 2.0 (the
  * "License"); you may not use this file except in compliance with the License.
  * You may obtain a copy of the License at
- *
+ * <p>
  * http://www.apache.org/licenses/LICENSE-2.0
- *
+ * <p>
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
  * License for the specific language governing permissions and limitations under
  * the License.
  */
-package org.apache.knox.gateway.services.security.token;
+package org.apache.knox.gateway.service.knoxidf.userparams;
 
-public enum TokenMetadataType {
+import java.util.Map;
 
-  JWT, KNOXSSO_COOKIE, CLIENT_ID, API_KEY;
+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);
 }
diff --git a/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/TokenMetadataType.java b/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/userparams/UserParamsProviderFactory.java
similarity index 63%
copy from gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/TokenMetadataType.java
copy to gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/userparams/UserParamsProviderFactory.java
index 17e82e0af..7cdf31984 100644
--- a/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/TokenMetadataType.java
+++ b/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/userparams/UserParamsProviderFactory.java
@@ -5,19 +5,22 @@
  * licenses this file to you under the Apache License, Version 2.0 (the
  * "License"); you may not use this file except in compliance with the License.
  * You may obtain a copy of the License at
- *
+ * <p>
  * http://www.apache.org/licenses/LICENSE-2.0
- *
+ * <p>
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
  * License for the specific language governing permissions and limitations under
  * the License.
  */
-package org.apache.knox.gateway.services.security.token;
+package org.apache.knox.gateway.service.knoxidf.userparams;
 
-public enum TokenMetadataType {
-
-  JWT, KNOXSSO_COOKIE, CLIENT_ID, API_KEY;
+import javax.servlet.ServletContext;
 
+public class UserParamsProviderFactory {
+    public static UserParamsProvider getUserParamsProvider(ServletContext servletContext) {
+        final String ldapUrl = servletContext.getInitParameter(LdapUserParamsProvider.LDAP_URL);
+        return ldapUrl == null ? new EmptyUserParamsProvider() : new LdapUserParamsProvider(servletContext);
+    }
 }
diff --git a/gateway-service-knoxidf/src/main/resources/META-INF/services/org.apache.knox.gateway.deploy.ServiceDeploymentContributor b/gateway-service-knoxidf/src/main/resources/META-INF/services/org.apache.knox.gateway.deploy.ServiceDeploymentContributor
new file mode 100644
index 000000000..49fc687f1
--- /dev/null
+++ b/gateway-service-knoxidf/src/main/resources/META-INF/services/org.apache.knox.gateway.deploy.ServiceDeploymentContributor
@@ -0,0 +1,18 @@
+##########################################################################
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##########################################################################
+org.apache.knox.gateway.service.knoxidf.deploy.KnoxIDFServiceDeploymentContributor
diff --git a/gateway-service-knoxsso/src/main/java/org/apache/knox/gateway/service/knoxsso/WebSSOResource.java b/gateway-service-knoxsso/src/main/java/org/apache/knox/gateway/service/knoxsso/WebSSOResource.java
index 16e676240..5da9f343e 100644
--- a/gateway-service-knoxsso/src/main/java/org/apache/knox/gateway/service/knoxsso/WebSSOResource.java
+++ b/gateway-service-knoxsso/src/main/java/org/apache/knox/gateway/service/knoxsso/WebSSOResource.java
@@ -17,35 +17,6 @@
  */
 package org.apache.knox.gateway.service.knoxsso;
 
-import static javax.ws.rs.core.MediaType.APPLICATION_JSON;
-import static javax.ws.rs.core.MediaType.APPLICATION_XML;
-import static org.apache.knox.gateway.services.GatewayServices.GATEWAY_CLUSTER_ATTRIBUTE;
-
-import java.io.IOException;
-import java.net.MalformedURLException;
-import java.net.URI;
-import java.net.URISyntaxException;
-import java.security.Principal;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
-import java.util.Map;
-import java.util.Map.Entry;
-
-import javax.annotation.PostConstruct;
-import javax.servlet.ServletContext;
-import javax.servlet.http.Cookie;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpSession;
-import javax.ws.rs.GET;
-import javax.ws.rs.POST;
-import javax.ws.rs.Path;
-import javax.ws.rs.Produces;
-import javax.ws.rs.WebApplicationException;
-import javax.ws.rs.core.Context;
-import javax.ws.rs.core.Response;
-
 import com.nimbusds.jose.JOSEObjectType;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.knox.gateway.audit.log4j.audit.Log4jAuditor;
@@ -70,6 +41,39 @@ import org.apache.knox.gateway.util.SetCookieHeader;
 import org.apache.knox.gateway.util.Tokens;
 import org.apache.knox.gateway.util.Urls;
 import org.apache.knox.gateway.util.WhitelistUtils;
+import org.apache.knox.gateway.util.knoxidf.FederatedOpConfiguration;
+import org.apache.knox.gateway.util.knoxidf.FederatedOpConfigurationStore;
+import org.apache.knox.gateway.util.knoxidf.KnoxIDFUtils;
+
+import javax.annotation.PostConstruct;
+import javax.servlet.ServletContext;
+import javax.servlet.http.Cookie;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.HttpSession;
+import javax.ws.rs.GET;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.WebApplicationException;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.Response;
+import java.io.IOException;
+import java.net.MalformedURLException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.security.Principal;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Optional;
+import java.util.Set;
+
+import static javax.ws.rs.core.MediaType.APPLICATION_JSON;
+import static javax.ws.rs.core.MediaType.APPLICATION_XML;
+import static org.apache.knox.gateway.services.GatewayServices.GATEWAY_CLUSTER_ATTRIBUTE;
 
 @Path( WebSSOResource.RESOURCE_PATH )
 public class WebSSOResource {
@@ -108,13 +112,14 @@ public class WebSSOResource {
   private String tokenType;
   private String whitelist;
   private String domainSuffix;
-  private List<String> targetAudiences = new ArrayList<>();
+  private final List<String> targetAudiences = new ArrayList<>();
   private boolean enableSession;
   private String signatureAlgorithm;
   private List<String> ssoExpectedparams = new ArrayList<>();
   private String clusterName;
   private String tokenIssuer;
   private TokenStateService tokenStateService;
+  private final FederatedOpConfigurationStore federatedOpConfigurationStore = FederatedOpConfigurationStore.getInstance(120000L);
 
   private String sameSiteValue;
 
@@ -226,6 +231,25 @@ public class WebSSOResource {
     tokenType = StringUtils.isBlank(configuredTokenType) ? JOSEObjectType.JWT.getType() : configuredTokenType;
   }
 
+  @Path("/federated/op")
+  @GET
+  public Response federatedOpLogin() {
+    final String loginSessionId = request.getParameter("fedOpSid");
+    final String opName = request.getParameter("fedOpName");
+    final Optional<FederatedOpConfiguration> federatedOpConfig = federatedOpConfigurationStore.get(loginSessionId).stream()
+            .filter(federatedOpConfiguration -> federatedOpConfiguration.getName().equals(opName))
+            .findFirst();
+    if (federatedOpConfig.isPresent()) {
+      final FederatedOpConfiguration federatedOpConfiguration = federatedOpConfig.get();
+      //keep only the selected federated OP in the cache -> we can easily get it in the AuthorizeResource.authCallback endpoint
+      federatedOpConfigurationStore.put(loginSessionId, Set.of(federatedOpConfiguration));
+      final String federatedOpAuthRedirect = KnoxIDFUtils.buildFederatedOpAuthRedirect(federatedOpConfiguration, loginSessionId);
+      return Response.seeOther(java.net.URI.create(federatedOpAuthRedirect)).build();
+    } else {
+      return KnoxIDFUtils.error("invalid_request", "Cannot load federated op config associated with login session");
+    }
+  }
+
   @GET
   @Produces({APPLICATION_JSON, APPLICATION_XML})
   public Response doGet() {
diff --git a/gateway-service-knoxtoken/src/main/java/org/apache/knox/gateway/service/knoxtoken/ClientCredentialsResource.java b/gateway-service-knoxtoken/src/main/java/org/apache/knox/gateway/service/knoxtoken/ClientCredentialsResource.java
index d23c693ee..4c17c02db 100644
--- a/gateway-service-knoxtoken/src/main/java/org/apache/knox/gateway/service/knoxtoken/ClientCredentialsResource.java
+++ b/gateway-service-knoxtoken/src/main/java/org/apache/knox/gateway/service/knoxtoken/ClientCredentialsResource.java
@@ -33,6 +33,7 @@ import javax.ws.rs.Path;
 import javax.ws.rs.Produces;
 import javax.ws.rs.core.Response;
 import java.util.HashMap;
+import java.util.Map;
 
 import static javax.ws.rs.core.MediaType.APPLICATION_JSON;
 import static javax.ws.rs.core.MediaType.APPLICATION_XML;
@@ -98,6 +99,7 @@ public class ClientCredentialsResource extends PasscodeTokenResourceBase {
             map.put(CLIENT_ID, tokenId);
             map.put(CLIENT_SECRET, passcode);
             addExpiryIfNotNever(map);
+            decorateResponseMap(map);
             String jsonResponse = JsonUtils.renderAsJsonString(map);
             return resp.responseBuilder.entity(jsonResponse).build();
         }
@@ -108,4 +110,8 @@ public class ClientCredentialsResource extends PasscodeTokenResourceBase {
             return resp.responseBuilder.build();
         }
     }
+
+    protected void decorateResponseMap(Map<String, Object> responseMap) {
+        //NOP
+    }
 }
diff --git a/gateway-service-knoxtoken/src/main/java/org/apache/knox/gateway/service/knoxtoken/TokenResource.java b/gateway-service-knoxtoken/src/main/java/org/apache/knox/gateway/service/knoxtoken/TokenResource.java
index 8da6f137b..56ab3b3f8 100644
--- a/gateway-service-knoxtoken/src/main/java/org/apache/knox/gateway/service/knoxtoken/TokenResource.java
+++ b/gateway-service-knoxtoken/src/main/java/org/apache/knox/gateway/service/knoxtoken/TokenResource.java
@@ -116,7 +116,7 @@ public class TokenResource {
   protected static final String TOKEN_TYPE = "token_type";
   protected static final String ACCESS_TOKEN = "access_token";
   protected static final String TOKEN_ID = "token_id";
-  static final String PASSCODE = "passcode";
+  public static final String PASSCODE = "passcode";
   protected static final String MANAGED_TOKEN = "managed";
   private static final String TARGET_URL = "target_url";
   private static final String ENDPOINT_PUBLIC_CERT = "endpoint_public_cert";
@@ -146,6 +146,7 @@ public class TokenResource {
   private static final String LIFESPAN_INPUT_ENABLED_TEXT = "lifespanInputEnabled";
   static final String KNOX_TOKEN_USER_LIMIT_PER_USER = TOKEN_PARAM_PREFIX + "limit.per.user";
   static final String KNOX_TOKEN_USER_LIMIT_EXCEEDED_ACTION = TOKEN_PARAM_PREFIX + "user.limit.exceeded.action";
+  private static final String KNOX_TOKEN_HARDCODED_CLAIM_MAPPINGS = TOKEN_PARAM_PREFIX + "hardcoded.claim.mappings";
   private static final String METADATA_QUERY_PARAM_PREFIX = "md_";
   private static final String TOKEN_ENABLE_DELEGATED_AUTH = TOKEN_PARAM_PREFIX + "enable.delegated.auth";
   private static final long TOKEN_TTL_DEFAULT = 30000L;
@@ -188,6 +189,7 @@ public class TokenResource {
   private Optional<Long> maxTokenLifetime = Optional.empty();
 
   private int tokenLimitPerUser;
+  private Map<String, Object> hardCodedClaimMappings;
   private boolean includeGroupsInTokenAllowed;
   private String tokenIssuer;
   private boolean enableDelegatedAuth;
@@ -365,9 +367,37 @@ public class TokenResource {
               .filter(s -> !s.isEmpty())
               .collect(Collectors.toSet());
     }
+
+    parseHardcodedClaimMappings(context.getInitParameter(KNOX_TOKEN_HARDCODED_CLAIM_MAPPINGS));
     setTokenStateServiceStatusMap();
   }
 
+  private void parseHardcodedClaimMappings(String raw) {
+    hardCodedClaimMappings = new HashMap<>();
+
+    if (raw != null && !raw.isBlank()) {
+      Arrays.stream(raw.split(";"))
+              .map(String::trim)
+              .filter(s -> !s.isEmpty())
+              .map(entry -> entry.split("=", 2))
+              .filter(kv -> kv.length == 2)
+              .forEach(kv -> {
+                String key = kv[0].trim();
+                String value = kv[1].trim();
+
+                Object mappedValue =
+                        value.contains(",")
+                                ? Arrays.stream(value.split(","))
+                                .map(String::trim)
+                                .filter(v -> !v.isEmpty())
+                                .toList()
+                                : value;
+
+                hardCodedClaimMappings.put(key, mappedValue);
+              });
+    }
+  }
+
   private String getTokenTTLAsText() {
     if (tokenTTL == -1) {
       return "Unlimited lifetime";
@@ -670,7 +700,7 @@ public class TokenResource {
     } else {
       try {
         final String revoker = SubjectUtils.getCurrentEffectivePrincipalName();
-        final String tokenId = getTokenId(token);
+        final String tokenId = TokenUtils.getTokenId(token);
         if (isKnoxSsoCookie(tokenId)) {
           errorStatus = Response.Status.FORBIDDEN;
           error = "SSO cookie (" + Tokens.getTokenIDDisplayText(tokenId) + ") cannot not be revoked.";
@@ -722,22 +752,6 @@ public class TokenResource {
     return StringUtils.isNotBlank(revoker) && (revoker.equals(tokenUserName) || revoker.equals(tokenCreatedBy));
   }
 
-  /*
-   * If the supplied 'token' conforms the UUID string representation, we consider
-   * that as the token ID; otherwise we expect that 'token' is the entire JWT and
-   * we get the token ID from it
-   */
-  private String getTokenId(String token) throws ParseException {
-    try {
-      UUID.fromString(token);
-      return token;
-    } catch (IllegalArgumentException e) {
-      //NOP: the supplied token is not a UUID, we expect the entire JWT
-    }
-    final JWTToken jwt = new JWTToken(token);
-    return TokenUtils.getTokenId(jwt);
-  }
-
   @PUT
   @Path(ENABLE_PATH)
   @Produces({APPLICATION_JSON})
@@ -845,16 +859,17 @@ public class TokenResource {
 
   protected TokenResponseContext getTokenResponse(UserContext context) {
     TokenResponseContext response = null;
+    long issueTime = System.currentTimeMillis();
     long expires = getExpiry();
     setupPublicCertPEM();
     String jku = getJku();
     try
     {
-      JWT token = getJWT(context.userName, expires, jku);
+      JWT token = getJWT(context, issueTime, expires, jku);
       if (token != null) {
         ResponseMap result = buildResponseMap(token, expires);
         String jsonResponse = JsonUtils.renderAsJsonString(result.map);
-        persistTokenDetails(result, expires, context.userName, context.createdBy);
+        persistTokenDetails(result, issueTime, expires, context.userName, context.createdBy);
 
         response = new TokenResponseContext(result, jsonResponse, Response.ok());
       } else {
@@ -940,10 +955,16 @@ public class TokenResource {
   protected static class UserContext {
     public final String userName;
     public final String createdBy;
+    private final Map<String, Object> userParams;
 
     public UserContext(String userName, String createdBy) {
+      this(userName, createdBy, Collections.emptyMap());
+    }
+
+    public UserContext(String userName, String createdBy, Map<String, Object> userParams) {
       this.userName = userName;
       this.createdBy = createdBy;
+      this.userParams = userParams;
     }
   }
 
@@ -1015,13 +1036,10 @@ public class TokenResource {
     return response;
   }
 
-  protected void persistTokenDetails(ResponseMap result, long expires, String userName, String createdBy) {
+  protected void persistTokenDetails(ResponseMap result, long issueTime, long expires, String userName, String createdBy) {
     // Optional token store service persistence
     if (tokenStateService != null) {
-      final long issueTime = System.currentTimeMillis();
-      tokenStateService.addToken(result.tokenId,
-                                 issueTime,
-              expires,
+      tokenStateService.addToken(result.tokenId, issueTime, expires,
                                  maxTokenLifetime.orElse(tokenStateService.getDefaultMaxLifetimeDuration()));
       final String comment = request.getParameter(COMMENT);
       final TokenMetadata tokenMetadata = new TokenMetadata(userName, StringUtils.isBlank(comment) ? null : comment);
@@ -1035,7 +1053,7 @@ public class TokenResource {
     }
   }
 
-  protected ResponseMap buildResponseMap(JWT token, long expires) {
+  protected ResponseMap buildResponseMap(JWT token, long expires) throws TokenServiceException {
     String accessToken = token.toString();
     String tokenId = TokenUtils.getTokenId(token);
     final boolean managedToken = tokenStateService != null;
@@ -1079,7 +1097,7 @@ public class TokenResource {
     }
   }
 
-  protected JWT getJWT(String userName, long expires, String jku) throws TokenServiceException {
+  private JWT getJWT(UserContext userContext, long issueTime, long expires, String jku) throws TokenServiceException {
     JWTokenAttributes jwtAttributes;
     JWT token;
     JWTokenAuthority ts = getGatewayServices().getService(ServiceType.TOKEN_SERVICE);
@@ -1087,8 +1105,9 @@ public class TokenResource {
     final JWTokenAttributesBuilder jwtAttributesBuilder = new JWTokenAttributesBuilder();
     jwtAttributesBuilder
         .setIssuer(tokenIssuer)
-        .setUserName(userName)
+        .setUserName(userContext.userName)
         .setAlgorithm(signatureAlgorithm)
+        .setIssueTime(issueTime)
         .setExpires(expires)
         .setManaged(managedToken)
         .setJku(jku)
@@ -1111,6 +1130,14 @@ public class TokenResource {
       handleDelegatedAuthentication(subject, jwtAttributesBuilder);
     }
 
+    if (userContext.userParams != null) {
+      hardCodedClaimMappings.putAll(userContext.userParams);
+    }
+
+    if (!hardCodedClaimMappings.isEmpty()) {
+      jwtAttributesBuilder.setCustomAttributes(hardCodedClaimMappings);
+    }
+
     jwtAttributes = jwtAttributesBuilder.build();
     token = ts.issueToken(jwtAttributes);
     return token;
diff --git a/gateway-spi/src/main/java/org/apache/knox/gateway/security/CommonTokenConstants.java b/gateway-spi/src/main/java/org/apache/knox/gateway/security/CommonTokenConstants.java
index 1537f698b..2f28b9629 100644
--- a/gateway-spi/src/main/java/org/apache/knox/gateway/security/CommonTokenConstants.java
+++ b/gateway-spi/src/main/java/org/apache/knox/gateway/security/CommonTokenConstants.java
@@ -27,4 +27,6 @@ public interface CommonTokenConstants {
 
     String CLIENT_SECRET = "client_secret";
 
+    String AUTH_CODE = "authorization_code";
+
 }
diff --git a/gateway-spi/src/main/java/org/apache/knox/gateway/services/ServiceType.java b/gateway-spi/src/main/java/org/apache/knox/gateway/services/ServiceType.java
index 5caeac0e6..21794bc21 100644
--- a/gateway-spi/src/main/java/org/apache/knox/gateway/services/ServiceType.java
+++ b/gateway-spi/src/main/java/org/apache/knox/gateway/services/ServiceType.java
@@ -40,7 +40,8 @@ public enum ServiceType {
   REMOTE_CONFIGURATION_MONITOR("RemoteConfigurationMonitor"),
   GATEWAY_STATUS_SERVICE("GatewayStatusService"),
   LDAP_SERVICE("LDAPService"),
-  LDAP_ROLES_LOOKUP_SERVICE("LDAPRoleLookupService");
+  LDAP_ROLES_LOOKUP_SERVICE("LDAPRoleLookupService"),
+  KNOXIDF_FEDERATED_IDENTITY_SERVICE("KnoxIDFFederatedIdentityService");
 
   private final String serviceTypeName;
   private final String shortName;
diff --git a/gateway-spi/src/main/java/org/apache/knox/gateway/services/knoxidf/federation/FederatedIdentity.java b/gateway-spi/src/main/java/org/apache/knox/gateway/services/knoxidf/federation/FederatedIdentity.java
new file mode 100644
index 000000000..3c025ccb8
--- /dev/null
+++ b/gateway-spi/src/main/java/org/apache/knox/gateway/services/knoxidf/federation/FederatedIdentity.java
@@ -0,0 +1,87 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. The ASF
+ * licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * <p>
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * <p>
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package org.apache.knox.gateway.services.knoxidf.federation;
+
+import java.time.Instant;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.UUID;
+
+public final class FederatedIdentity {
+
+    private final String id;
+    private final String userId;
+    private final String provider;
+    private final String externalSubject;
+    private final String externalIssuer;
+    private final Instant createdAt;
+    private final Map<String, String> attributes = new HashMap<>();
+
+    public FederatedIdentity(String userId, String provider, String externalSubject, String externalIssuer,
+                             Instant createdAt, Map<String, String> attributes) {
+        this(UUID.randomUUID().toString(), userId, provider, externalSubject, externalIssuer, createdAt, attributes);
+    }
+
+    public FederatedIdentity(String id, String userId, String provider, String externalSubject, String externalIssuer,
+                             Instant createdAt, Map<String, String> attributes) {
+        this.id = id;
+        this.userId = userId;
+        this.provider = provider;
+        this.externalSubject = externalSubject;
+        this.externalIssuer = externalIssuer;
+        this.createdAt = createdAt;
+        if (attributes != null) {
+            this.attributes.putAll(attributes);
+        }
+    }
+
+    public String getId() {
+        return id;
+    }
+
+    public String getUserId() {
+        return userId;
+    }
+
+    public String getProvider() {
+        return provider;
+    }
+
+    public String getExternalSubject() {
+        return externalSubject;
+    }
+
+    public String getExternalIssuer() {
+        return externalIssuer;
+    }
+
+    public Instant getCreatedAt() {
+        return createdAt;
+    }
+
+    public Map<String, String> getAttributes() {
+        return attributes;
+    }
+
+    public String getAttribute(String key) {
+        return attributes.get(key);
+    }
+
+    public void addAttribute(String key, String value) {
+        attributes.put(key, value);
+    }
+}
diff --git a/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/TokenMetadataType.java b/gateway-spi/src/main/java/org/apache/knox/gateway/services/knoxidf/federation/FederatedIdentityService.java
similarity index 61%
copy from gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/TokenMetadataType.java
copy to gateway-spi/src/main/java/org/apache/knox/gateway/services/knoxidf/federation/FederatedIdentityService.java
index 17e82e0af..778b589a2 100644
--- a/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/TokenMetadataType.java
+++ b/gateway-spi/src/main/java/org/apache/knox/gateway/services/knoxidf/federation/FederatedIdentityService.java
@@ -5,19 +5,30 @@
  * licenses this file to you under the Apache License, Version 2.0 (the
  * "License"); you may not use this file except in compliance with the License.
  * You may obtain a copy of the License at
- *
+ * <p>
  * http://www.apache.org/licenses/LICENSE-2.0
- *
+ * <p>
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
  * License for the specific language governing permissions and limitations under
  * the License.
  */
-package org.apache.knox.gateway.services.security.token;
+package org.apache.knox.gateway.services.knoxidf.federation;
 
-public enum TokenMetadataType {
+import org.apache.knox.gateway.services.Service;
 
-  JWT, KNOXSSO_COOKIE, CLIENT_ID, API_KEY;
+import java.util.Optional;
 
+public interface FederatedIdentityService extends Service {
+
+    void addFederatedIdentity(FederatedIdentity identity);
+
+    Optional<FederatedIdentity> findById(String identityId);
+
+    Optional<FederatedIdentity> findByProviderAndSubject(
+            String provider,
+            String externalIssuer,
+            String externalSubject);
 }
+
diff --git a/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/TokenMetadataType.java b/gateway-spi/src/main/java/org/apache/knox/gateway/services/knoxidf/federation/FederatedIdentityServiceException.java
similarity index 68%
copy from gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/TokenMetadataType.java
copy to gateway-spi/src/main/java/org/apache/knox/gateway/services/knoxidf/federation/FederatedIdentityServiceException.java
index 17e82e0af..30cbce7b2 100644
--- a/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/TokenMetadataType.java
+++ b/gateway-spi/src/main/java/org/apache/knox/gateway/services/knoxidf/federation/FederatedIdentityServiceException.java
@@ -5,19 +5,24 @@
  * licenses this file to you under the Apache License, Version 2.0 (the
  * "License"); you may not use this file except in compliance with the License.
  * You may obtain a copy of the License at
- *
+ * <p>
  * http://www.apache.org/licenses/LICENSE-2.0
- *
+ * <p>
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
  * License for the specific language governing permissions and limitations under
  * the License.
  */
-package org.apache.knox.gateway.services.security.token;
+package org.apache.knox.gateway.services.knoxidf.federation;
 
-public enum TokenMetadataType {
+public class FederatedIdentityServiceException extends RuntimeException {
 
-  JWT, KNOXSSO_COOKIE, CLIENT_ID, API_KEY;
+    public FederatedIdentityServiceException(String message) {
+        super(message);
+    }
 
+    public FederatedIdentityServiceException(String message, Throwable cause) {
+        super(message, cause);
+    }
 }
diff --git a/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/JWTokenAttributes.java b/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/JWTokenAttributes.java
index c41f983eb..40f9f4712 100644
--- a/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/JWTokenAttributes.java
+++ b/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/JWTokenAttributes.java
@@ -29,6 +29,7 @@ public class JWTokenAttributes {
   public static final String DEFAULT_TYPE = "JWT";
   private final String userName;
   private final List<String> audiences;
+  private final long issueTime;
   private final String algorithm;
   private final long expires;
   private final String signingKeystoreName;
@@ -42,22 +43,15 @@ public class JWTokenAttributes {
   private String kid;
   private final String clientId;
   private final List<Map<String, Object>> actorChain;
+  private final Map<String, Object> customAttributes;
 
-  JWTokenAttributes(String userName, List<String> audiences, String algorithm, long expires, String signingKeystoreName, String signingKeystoreAlias,
-      char[] signingKeystorePassphrase, boolean managed, String jku, String type, Set<String> groups, String kid, String issuer) {
-    this(userName, audiences, algorithm, expires, signingKeystoreName, signingKeystoreAlias, signingKeystorePassphrase, managed, jku, type, groups, kid, issuer, null);
-  }
-
-  JWTokenAttributes(String userName, List<String> audiences, String algorithm, long expires, String signingKeystoreName, String signingKeystoreAlias,
-      char[] signingKeystorePassphrase, boolean managed, String jku, String type, Set<String> groups, String kid, String issuer, String clientId) {
-    this(userName, audiences, algorithm, expires, signingKeystoreName, signingKeystoreAlias, signingKeystorePassphrase, managed, jku, type, groups, kid, issuer, clientId, null);
-  }
-
-  JWTokenAttributes(String userName, List<String> audiences, String algorithm, long expires, String signingKeystoreName, String signingKeystoreAlias,
-      char[] signingKeystorePassphrase, boolean managed, String jku, String type, Set<String> groups, String kid, String issuer, String clientId, List<Map<String, Object>> actorChain) {
+  JWTokenAttributes(String userName, List<String> audiences, String algorithm, long issueTime, long expires, String signingKeystoreName, String signingKeystoreAlias,
+      char[] signingKeystorePassphrase, boolean managed, String jku, String type, Set<String> groups, String kid, String issuer, String clientId, List<Map<String, Object>> actorChain,
+                    Map<String, Object> customAttributes) {
     this.userName = userName;
     this.audiences = audiences;
     this.algorithm = algorithm;
+    this.issueTime = issueTime;
     this.expires = expires;
     this.signingKeystoreName = signingKeystoreName;
     this.signingKeystoreAlias = signingKeystoreAlias;
@@ -70,77 +64,81 @@ public class JWTokenAttributes {
     this.issuer = issuer;
     this.clientId = clientId;
     this.actorChain = actorChain;
+    this.customAttributes = customAttributes;
   }
+    public String getUserName() {
+        return userName;
+    }
 
-  public String getUserName() {
-    return userName;
-  }
+    public List<String> getAudiences() {
+        return audiences;
+    }
 
-  public List<String> getAudiences() {
-    return audiences;
-  }
+    public String getAlgorithm() {
+        return algorithm;
+    }
 
-  public String getAlgorithm() {
-    return algorithm;
-  }
+    public long getIssueTime() {
+        return issueTime;
+    }
 
-  public long getExpires() {
-    return expires;
-  }
+    public long getExpires() {
+        return expires;
+    }
 
-  public Date getExpiresDate() {
-    return expires == -1 ? null : new Date(expires);
-  }
+    public Date getExpiresDate() {
+        return expires == -1 ? null : new Date(expires);
+    }
 
-  public String getSigningKeystoreName() {
-    return signingKeystoreName;
-  }
+    public String getSigningKeystoreName() {
+        return signingKeystoreName;
+    }
 
-  public String getSigningKeystoreAlias() {
-    return signingKeystoreAlias;
-  }
+    public String getSigningKeystoreAlias() {
+        return signingKeystoreAlias;
+    }
 
-  public char[] getSigningKeystorePassphrase() {
-    return signingKeystorePassphrase;
-  }
+    public char[] getSigningKeystorePassphrase() {
+        return signingKeystorePassphrase;
+    }
 
-  public boolean isManaged() {
-    return managed;
-  }
+    public boolean isManaged() {
+        return managed;
+    }
 
-  public URI getJkuUri() throws URISyntaxException {
-    return jku != null ? new URI(jku) : null;
-  }
+    public URI getJkuUri() throws URISyntaxException {
+        return jku != null ? new URI(jku) : null;
+    }
 
-  public String getJku(){
-    return jku;
-  }
+    public String getJku() {
+        return jku;
+    }
 
-  public void setJku(String jku) {
-    this.jku = jku;
-  }
+    public void setJku(String jku) {
+        this.jku = jku;
+    }
 
-  public String getType() {
-    return type;
-  }
+    public String getType() {
+        return type;
+    }
 
-  public Set<String> getGroups() {
-    return groups;
-  }
+    public Set<String> getGroups() {
+        return groups;
+    }
 
-  public void setKid(String kid) {
-    this.kid = kid;
-  }
+    public void setKid(String kid) {
+        this.kid = kid;
+    }
 
-  public String getKid() {
-    return kid;
-  }
+    public String getKid() {
+        return kid;
+    }
 
-  public String getIssuer() {
-    return issuer;
-  }
+    public String getIssuer() {
+        return issuer;
+    }
 
-  public String getClientId() {
+    public String getClientId() {
     return clientId;
   }
 
@@ -167,4 +165,8 @@ public class JWTokenAttributes {
   public List<Map<String, Object>> getActorChain() {
     return actorChain;
   }
+
+  public Map<String, Object> getCustomAttributes() {
+      return customAttributes;
+  }
 }
diff --git a/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/JWTokenAttributesBuilder.java b/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/JWTokenAttributesBuilder.java
index b70a84e6e..8bc70d43d 100644
--- a/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/JWTokenAttributesBuilder.java
+++ b/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/JWTokenAttributesBuilder.java
@@ -28,6 +28,7 @@ public class JWTokenAttributesBuilder {
   private String userName;
   private List<String> audiences;
   private String algorithm;
+  private long issueTime;
   private long expires;
   private String signingKeystoreName;
   private String signingKeystoreAlias;
@@ -40,6 +41,7 @@ public class JWTokenAttributesBuilder {
   private String issuer = JWTokenAttributes.DEFAULT_ISSUER;
   private String clientId;
   private List<Map<String, Object>> actorChain;
+  private Map<String, Object> customAttributes;
 
   public JWTokenAttributesBuilder setUserName(String userName) {
     this.userName = userName;
@@ -60,6 +62,11 @@ public class JWTokenAttributesBuilder {
     return this;
   }
 
+  public JWTokenAttributesBuilder setIssueTime(long issueTime) {
+    this.issueTime = issueTime;
+    return this;
+  }
+
   public JWTokenAttributesBuilder setExpires(long expires) {
     this.expires = expires;
     return this;
@@ -144,8 +151,13 @@ public class JWTokenAttributesBuilder {
     return this;
   }
 
+  public JWTokenAttributesBuilder setCustomAttributes(Map<String, Object> customAttributes) {
+    this.customAttributes = customAttributes;
+    return this;
+  }
+
   public JWTokenAttributes build() {
-    return new JWTokenAttributes(userName, (audiences == null ? new ArrayList<>() : audiences), algorithm, expires, signingKeystoreName, signingKeystoreAlias,
-        signingKeystorePassphrase, managed, jku, type, groups, kid, issuer, clientId, actorChain);
+    return new JWTokenAttributes(userName, (audiences == null ? new ArrayList<>() : audiences), algorithm, issueTime, expires, signingKeystoreName, signingKeystoreAlias,
+        signingKeystorePassphrase, managed, jku, type, groups, kid, issuer, clientId, actorChain, customAttributes);
   }
 }
diff --git a/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/TokenMetadata.java b/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/TokenMetadata.java
index 3bc7fe2cd..8df35babe 100644
--- a/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/TokenMetadata.java
+++ b/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/TokenMetadata.java
@@ -70,7 +70,6 @@ public class TokenMetadata {
   }
 
   public TokenMetadata(Map<String, String> metadataMap) {
-    this.metadataMap.clear();
     this.metadataMap.putAll(metadataMap);
   }
 
@@ -151,12 +150,17 @@ public class TokenMetadata {
 
   @JsonIgnore
   public boolean isKnoxSsoCookie() {
-    return getType() == null ? false : TokenMetadataType.KNOXSSO_COOKIE == TokenMetadataType.valueOf(getType());
+    return getType() != null && TokenMetadataType.KNOXSSO_COOKIE == TokenMetadataType.valueOf(getType());
   }
 
   @JsonIgnore
   public boolean isClientId() {
-    return getType() == null ? false : TokenMetadataType.CLIENT_ID == TokenMetadataType.valueOf(getType());
+    return getType() != null && TokenMetadataType.CLIENT_ID == TokenMetadataType.valueOf(getType());
+  }
+
+  @JsonIgnore
+  public boolean isAuthCode() {
+    return getType() != null && TokenMetadataType.AUTH_CODE == TokenMetadataType.valueOf(getType());
   }
 
   public String getType() {
diff --git a/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/TokenMetadataType.java b/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/TokenMetadataType.java
index 17e82e0af..4d0080fd5 100644
--- a/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/TokenMetadataType.java
+++ b/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/TokenMetadataType.java
@@ -18,6 +18,6 @@ package org.apache.knox.gateway.services.security.token;
 
 public enum TokenMetadataType {
 
-  JWT, KNOXSSO_COOKIE, CLIENT_ID, API_KEY;
+  JWT, KNOXSSO_COOKIE, CLIENT_ID, API_KEY, AUTH_CODE, REFRESH_TOKEN;
 
 }
diff --git a/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/TokenUtils.java b/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/TokenUtils.java
index 72fb7a25f..7805253b0 100644
--- a/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/TokenUtils.java
+++ b/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/TokenUtils.java
@@ -35,6 +35,8 @@ import java.util.Collections;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
+import java.text.ParseException;
+import java.util.UUID;
 
 public class TokenUtils {
   public static final String ATTR_CURRENT_KNOXSSO_COOKIE_TOKEN_ID = "currentKnoxSsoCookieTokenId";
@@ -53,6 +55,21 @@ public class TokenUtils {
     return token.getClaim(JWTToken.KNOX_ID_CLAIM);
   }
 
+  /**
+   * If the supplied 'token' conforms the UUID string representation, we consider
+   * that as the token ID; otherwise we expect that 'token' is the entire JWT, and
+   * we get the token ID from it
+   */
+  public static String getTokenId(String token) throws ParseException {
+    try {
+      UUID.fromString(token);
+      return token;
+    } catch (IllegalArgumentException e) {
+      //NOP: the supplied token is not a UUID, we expect the entire JWT
+    }
+    return getTokenId(new JWTToken(token));
+  }
+
   /**
    * Determine if server-managed token state is enabled for a provider, based on configuration.
    * The analysis includes checking the provider params and the gateway configuration.
diff --git a/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/impl/JWT.java b/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/impl/JWT.java
index 4cb4d151e..d756b034c 100644
--- a/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/impl/JWT.java
+++ b/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/impl/JWT.java
@@ -23,6 +23,7 @@ import com.nimbusds.jose.JOSEObjectType;
 import com.nimbusds.jose.JWSAlgorithm;
 import com.nimbusds.jose.JWSSigner;
 import com.nimbusds.jose.JWSVerifier;
+import com.nimbusds.jwt.JWTClaimsSet;
 
 public interface JWT {
 
@@ -63,6 +64,8 @@ public interface JWT {
 
   String getClaims();
 
+  JWTClaimsSet  getJWTClaimsSet();
+
   JWSAlgorithm getSignatureAlgorithm();
 
   JOSEObjectType getType();
diff --git a/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/impl/JWTToken.java b/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/impl/JWTToken.java
index b43a0b29a..78ad648f0 100644
--- a/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/impl/JWTToken.java
+++ b/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/impl/JWTToken.java
@@ -18,6 +18,7 @@ package org.apache.knox.gateway.services.security.token.impl;
 
 import java.net.URISyntaxException;
 import java.text.ParseException;
+import java.time.Instant;
 import java.util.Date;
 import java.util.Map;
 import java.util.UUID;
@@ -84,6 +85,7 @@ public class JWTToken implements JWT {
     }
     JWTClaimsSet claims;
     JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder()
+            .issueTime(Date.from(Instant.ofEpochMilli(jwtAttributes.getIssueTime())))
             .issuer(jwtAttributes.getIssuer())
             .subject(jwtAttributes.getUserName())
             .audience(jwtAttributes.getAudiences());
@@ -114,6 +116,11 @@ public class JWTToken implements JWT {
     builder.claim(KNOX_ID_CLAIM, String.valueOf(UUID.randomUUID()));
 
     builder.claim(MANAGED_TOKEN_CLAIM, String.valueOf(jwtAttributes.isManaged()));
+
+    if (jwtAttributes.getCustomAttributes() != null) {
+      jwtAttributes.getCustomAttributes().forEach(builder::claim);
+    }
+
     claims = builder.build();
 
     jwt = new SignedJWT(header, claims);
@@ -148,6 +155,16 @@ public class JWTToken implements JWT {
     return c;
   }
 
+  @Override
+  public JWTClaimsSet getJWTClaimsSet() {
+    try {
+      return jwt.getJWTClaimsSet();
+    } catch (ParseException e) {
+      log.unableToParseToken(e);
+      return null;
+    }
+  }
+
   @Override
   public String getPayload() {
     Payload payload = jwt.getPayload();
diff --git a/gateway-util-common/pom.xml b/gateway-util-common/pom.xml
index 1eec58fe9..88d9c9311 100644
--- a/gateway-util-common/pom.xml
+++ b/gateway-util-common/pom.xml
@@ -104,6 +104,23 @@
             <groupId>org.apache.httpcomponents</groupId>
             <artifactId>httpclient</artifactId>
         </dependency>
+
+        <dependency>
+            <groupId>javax.ws.rs</groupId>
+            <artifactId>javax.ws.rs-api</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.commons</groupId>
+            <artifactId>commons-text</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.github.ben-manes.caffeine</groupId>
+            <artifactId>caffeine</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.google.guava</groupId>
+            <artifactId>guava</artifactId>
+        </dependency>
         <!-- ********** ********** ********** ********** ********** ********** -->
         <!-- ********** Test Dependencies                           ********** -->
         <!-- ********** ********** ********** ********** ********** ********** -->
diff --git a/gateway-util-common/src/main/java/org/apache/knox/gateway/util/JsonUtils.java b/gateway-util-common/src/main/java/org/apache/knox/gateway/util/JsonUtils.java
index f0a8bf177..ab49f7d63 100644
--- a/gateway-util-common/src/main/java/org/apache/knox/gateway/util/JsonUtils.java
+++ b/gateway-util-common/src/main/java/org/apache/knox/gateway/util/JsonUtils.java
@@ -23,6 +23,7 @@ import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.Map;
 
+import com.fasterxml.jackson.databind.ObjectWriter;
 import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
 import org.apache.knox.gateway.i18n.GatewayUtilCommonMessages;
 import org.apache.knox.gateway.i18n.messages.MessagesFactory;
@@ -37,12 +38,16 @@ public class JsonUtils {
   private static final GatewayUtilCommonMessages LOG = MessagesFactory.get( GatewayUtilCommonMessages.class );
 
   public static String renderAsJsonString(Map<String, Object> map) {
+    return renderAsJsonString(map, false);
+  }
+
+  public static String renderAsJsonString(Map<String, Object> map, boolean pretty) {
     String json = null;
     ObjectMapper mapper = new ObjectMapper().registerModule(new JavaTimeModule());
 
     try {
-      // write JSON to a file
-      json = mapper.writeValueAsString(map);
+      final ObjectWriter writer = pretty ? mapper.writerWithDefaultPrettyPrinter() :  mapper.writer();
+      json = writer.writeValueAsString(map);
     } catch ( JsonProcessingException e ) {
       LOG.failedToSerializeMapToJSON( map, e );
     }
diff --git a/gateway-util-common/src/main/java/org/apache/knox/gateway/util/knoxidf/AuthorizeRequestMetadata.java b/gateway-util-common/src/main/java/org/apache/knox/gateway/util/knoxidf/AuthorizeRequestMetadata.java
new file mode 100644
index 000000000..239e0baf2
--- /dev/null
+++ b/gateway-util-common/src/main/java/org/apache/knox/gateway/util/knoxidf/AuthorizeRequestMetadata.java
@@ -0,0 +1,124 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. The ASF
+ * licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * <p>
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * <p>
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package org.apache.knox.gateway.util.knoxidf;
+
+import javax.ws.rs.core.Response;
+import java.util.Set;
+
+import static org.apache.knox.gateway.util.knoxidf.KnoxIDFUtils.error;
+
+public final class AuthorizeRequestMetadata {
+    private final String clientId;
+    private final String subject;
+    private final String responseType;
+    private final String redirectUri;
+    private final Set<String> requestedScopes;
+    private final String state;
+    private final String nonce;
+    private final String codeChallenge;
+    private final String codeChallengeMethod;
+
+    public AuthorizeRequestMetadata(String clientId, String subject, String responseType, String redirectUri, Set<String> requestedScopes, String state, String nonce) {
+        this(clientId, subject, responseType, redirectUri, requestedScopes, state, nonce, null, null);
+    }
+
+    public AuthorizeRequestMetadata(String clientId, String subject, String responseType, String redirectUri, Set<String> requestedScopes, String state, String nonce, String codeChallenge, String codeChallengeMethod) {
+        this.clientId = clientId;
+        this.subject = subject;
+        this.responseType = responseType;
+        this.redirectUri = redirectUri;
+        this.requestedScopes = requestedScopes;
+        this.state = state;
+        this.nonce = nonce;
+        this.codeChallenge = codeChallenge;
+        this.codeChallengeMethod = codeChallengeMethod;
+    }
+
+    public Response verify() {
+        if (responseType == null || responseType.isEmpty()) {
+            return error("invalid_request", "Missing response_type");
+        } else {
+            if (!KnoxIDFConstants.ALLOWED_RESPONSE_TYPES.contains(responseType)) {
+                return error("unsupported_response_type", "Unsupported response_type");
+            }
+
+            boolean requiresNonce = responseType.contains("id_token");
+            if (requiresNonce && (nonce == null || nonce.isEmpty())) {
+                return error("invalid_request", "Missing required parameter: nonce");
+            }
+        }
+
+        if (clientId == null || clientId.isEmpty()) {
+            return error("invalid_request", "Missing client_id");
+        }
+
+        // Verify redirect URI
+        if (redirectUri == null || redirectUri.isEmpty()) {
+            return error("invalid_request", "Missing redirect_uri");
+        }
+
+        // Verify scope(s)
+        if (requestedScopes == null || requestedScopes.isEmpty()) {
+            return error("invalid_scope", "Missing scopes");
+        } else if (!requestedScopes.contains("openid")) {
+            return error("invalid_scope", "Missing required scope: openid");
+        }
+
+        return null;
+    }
+
+    public String getClientId() {
+        return clientId;
+    }
+
+    public String getSubject() {
+        return subject;
+    }
+
+    public String getResponseType() {
+        return responseType;
+    }
+
+    public String getRedirectUri() {
+        return redirectUri;
+    }
+
+    public String getState() {
+        return state;
+    }
+
+    public String getNonce() {
+        return nonce;
+    }
+
+    public String getCodeChallenge() {
+        return codeChallenge;
+    }
+
+    public String getCodeChallengeMethod() {
+        return codeChallengeMethod;
+    }
+
+    public Set<String> getRequestedScopes() {
+        return requestedScopes;
+    }
+
+    public String getJoinedRequestedScopes() {
+        return String.join(" ", requestedScopes);
+    }
+
+}
diff --git a/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/TokenMetadataType.java b/gateway-util-common/src/main/java/org/apache/knox/gateway/util/knoxidf/AuthorizeRequestMetadataStore.java
similarity index 60%
copy from gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/TokenMetadataType.java
copy to gateway-util-common/src/main/java/org/apache/knox/gateway/util/knoxidf/AuthorizeRequestMetadataStore.java
index 17e82e0af..80f3d7110 100644
--- a/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/TokenMetadataType.java
+++ b/gateway-util-common/src/main/java/org/apache/knox/gateway/util/knoxidf/AuthorizeRequestMetadataStore.java
@@ -5,19 +5,29 @@
  * licenses this file to you under the Apache License, Version 2.0 (the
  * "License"); you may not use this file except in compliance with the License.
  * You may obtain a copy of the License at
- *
+ * <p>
  * http://www.apache.org/licenses/LICENSE-2.0
- *
+ * <p>
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
  * License for the specific language governing permissions and limitations under
  * the License.
  */
-package org.apache.knox.gateway.services.security.token;
+package org.apache.knox.gateway.util.knoxidf;
 
-public enum TokenMetadataType {
+public class AuthorizeRequestMetadataStore extends KnoxIDFArtifactStore<AuthorizeRequestMetadata>{
 
-  JWT, KNOXSSO_COOKIE, CLIENT_ID, API_KEY;
+    private static AuthorizeRequestMetadataStore instance;
 
+    private AuthorizeRequestMetadataStore(long ttl) {
+        super(ttl);
+    }
+
+    public static synchronized AuthorizeRequestMetadataStore getInstance(long ttl) {
+        if (instance == null) {
+            instance = new AuthorizeRequestMetadataStore(ttl);
+        }
+        return instance;
+    }
 }
diff --git a/gateway-util-common/src/main/java/org/apache/knox/gateway/util/knoxidf/FederatedOpConfiguration.java b/gateway-util-common/src/main/java/org/apache/knox/gateway/util/knoxidf/FederatedOpConfiguration.java
new file mode 100644
index 000000000..7ad607843
--- /dev/null
+++ b/gateway-util-common/src/main/java/org/apache/knox/gateway/util/knoxidf/FederatedOpConfiguration.java
@@ -0,0 +1,81 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. The ASF
+ * licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * <p>
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * <p>
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package org.apache.knox.gateway.util.knoxidf;
+
+import javax.servlet.ServletContext;
+
+public class FederatedOpConfiguration {
+    private final boolean enabled;
+    private final String name;
+    private final String clientId;
+    private final String clientSecret;
+    private final String tokenEndpoint;
+    private final String authorizeEndpoint;
+    private final String userInfoEndpoint;
+    private final String discoveryEndpoint;
+    private final String authorizeCallback;
+
+    public FederatedOpConfiguration(final ServletContext servletContext, final String opName) {
+        this.name = opName;
+        final String prefix = KnoxIDFConstants.FEDERATED_OP_CONFIG_PREFIX + (opName != null ? opName + "." : "");
+        this.enabled = Boolean.parseBoolean(servletContext.getInitParameter(prefix + "enabled"));
+        this.clientId = servletContext.getInitParameter(prefix + "clientId");
+        this.clientSecret = servletContext.getInitParameter(prefix + "clientSecret");
+        this.tokenEndpoint = servletContext.getInitParameter(prefix + "token.endpoint");
+        this.authorizeEndpoint = servletContext.getInitParameter(prefix + "authorize.endpoint");
+        this.authorizeCallback = servletContext.getInitParameter(prefix + "authorize.callback");
+        this.userInfoEndpoint = servletContext.getInitParameter(prefix + "userinfo.endpoint");
+        this.discoveryEndpoint = servletContext.getInitParameter(prefix + "discovery.endpoint");
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public boolean isEnabled() {
+        return enabled;
+    }
+
+    public String getClientId() {
+        return clientId;
+    }
+
+    public String getClientSecret() {
+        return clientSecret;
+    }
+
+    String getAuthorizeEndpoint() {
+        return authorizeEndpoint;
+    }
+
+    public String getAuthorizeCallback() {
+        return authorizeCallback;
+    }
+
+    public String getTokenEndpoint() {
+        return tokenEndpoint;
+    }
+
+    public String getUserInfoEndpoint() {
+        return userInfoEndpoint;
+    }
+
+    public String getDiscoveryEndpoint() {
+        return discoveryEndpoint;
+    }
+
+}
diff --git a/gateway-util-common/src/main/java/org/apache/knox/gateway/util/knoxidf/FederatedOpConfigurationFactory.java b/gateway-util-common/src/main/java/org/apache/knox/gateway/util/knoxidf/FederatedOpConfigurationFactory.java
new file mode 100644
index 000000000..f1efc3d75
--- /dev/null
+++ b/gateway-util-common/src/main/java/org/apache/knox/gateway/util/knoxidf/FederatedOpConfigurationFactory.java
@@ -0,0 +1,42 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. The ASF
+ * licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * <p>
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * <p>
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package org.apache.knox.gateway.util.knoxidf;
+
+import javax.servlet.ServletContext;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+public class FederatedOpConfigurationFactory {
+
+    public static Map<String, FederatedOpConfiguration> createFederatedOpConfiguration(final ServletContext servletContext) {
+        final String names = servletContext.getInitParameter(KnoxIDFConstants.FEDERATED_OP_CONFIG_NAMES);
+        if (names == null || names.isEmpty()) {
+            return Collections.emptyMap();
+        }
+
+        final Map<String, FederatedOpConfiguration> configs = new HashMap<>();
+        for (String name : names.split(",")) {
+            final String trimmedName = name.trim();
+            final FederatedOpConfiguration federatedOpConfiguration = new FederatedOpConfiguration(servletContext, trimmedName);
+            if (federatedOpConfiguration.isEnabled()) {
+                configs.put(trimmedName, federatedOpConfiguration);
+            }
+        }
+        return configs;
+    }
+}
diff --git a/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/TokenMetadataType.java b/gateway-util-common/src/main/java/org/apache/knox/gateway/util/knoxidf/FederatedOpConfigurationStore.java
similarity index 59%
copy from gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/TokenMetadataType.java
copy to gateway-util-common/src/main/java/org/apache/knox/gateway/util/knoxidf/FederatedOpConfigurationStore.java
index 17e82e0af..80bbb1a04 100644
--- a/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/TokenMetadataType.java
+++ b/gateway-util-common/src/main/java/org/apache/knox/gateway/util/knoxidf/FederatedOpConfigurationStore.java
@@ -5,19 +5,31 @@
  * licenses this file to you under the Apache License, Version 2.0 (the
  * "License"); you may not use this file except in compliance with the License.
  * You may obtain a copy of the License at
- *
+ * <p>
  * http://www.apache.org/licenses/LICENSE-2.0
- *
+ * <p>
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
  * License for the specific language governing permissions and limitations under
  * the License.
  */
-package org.apache.knox.gateway.services.security.token;
+package org.apache.knox.gateway.util.knoxidf;
 
-public enum TokenMetadataType {
+import java.util.Set;
 
-  JWT, KNOXSSO_COOKIE, CLIENT_ID, API_KEY;
+public class FederatedOpConfigurationStore extends KnoxIDFArtifactStore<Set<FederatedOpConfiguration>> {
 
+    private static FederatedOpConfigurationStore instance;
+
+    private FederatedOpConfigurationStore(long ttl) {
+        super(ttl);
+    }
+
+    public static synchronized FederatedOpConfigurationStore getInstance(long ttl) {
+        if (instance == null) {
+            instance = new FederatedOpConfigurationStore(ttl);
+        }
+        return instance;
+    }
 }
diff --git a/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/TokenMetadataType.java b/gateway-util-common/src/main/java/org/apache/knox/gateway/util/knoxidf/KnoxIDFArtifactStore.java
similarity index 56%
copy from gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/TokenMetadataType.java
copy to gateway-util-common/src/main/java/org/apache/knox/gateway/util/knoxidf/KnoxIDFArtifactStore.java
index 17e82e0af..dbeba8141 100644
--- a/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/TokenMetadataType.java
+++ b/gateway-util-common/src/main/java/org/apache/knox/gateway/util/knoxidf/KnoxIDFArtifactStore.java
@@ -5,19 +5,35 @@
  * licenses this file to you under the Apache License, Version 2.0 (the
  * "License"); you may not use this file except in compliance with the License.
  * You may obtain a copy of the License at
- *
+ * <p>
  * http://www.apache.org/licenses/LICENSE-2.0
- *
+ * <p>
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
  * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
  * License for the specific language governing permissions and limitations under
  * the License.
  */
-package org.apache.knox.gateway.services.security.token;
+package org.apache.knox.gateway.util.knoxidf;
 
-public enum TokenMetadataType {
+import com.github.benmanes.caffeine.cache.Cache;
+import com.github.benmanes.caffeine.cache.Caffeine;
 
-  JWT, KNOXSSO_COOKIE, CLIENT_ID, API_KEY;
+import java.util.concurrent.TimeUnit;
 
+public abstract class KnoxIDFArtifactStore<T> {
+
+    private final Cache<String, T> cache;
+
+    protected KnoxIDFArtifactStore(long ttl) {
+        this.cache = Caffeine.newBuilder().expireAfterWrite(ttl * 2, TimeUnit.MILLISECONDS).build();
+    }
+
+    public void put(String key, T value) {
+        cache.put(key, value);
+    }
+
+    public T get(String key) {
+        return cache.getIfPresent(key);
+    }
 }
diff --git a/gateway-util-common/src/main/java/org/apache/knox/gateway/util/knoxidf/KnoxIDFConstants.java b/gateway-util-common/src/main/java/org/apache/knox/gateway/util/knoxidf/KnoxIDFConstants.java
new file mode 100644
index 000000000..5573b90e2
--- /dev/null
+++ b/gateway-util-common/src/main/java/org/apache/knox/gateway/util/knoxidf/KnoxIDFConstants.java
@@ -0,0 +1,60 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. The ASF
+ * licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * <p>
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * <p>
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package org.apache.knox.gateway.util.knoxidf;
+
+import com.google.common.collect.Sets;
+
+import java.util.Set;
+
+public interface KnoxIDFConstants {
+    String BASE_RESORCE_PATH = "knoxidf/api/v1";
+    String AUTH_CODE = "authorization_code";
+    String CLIENT_ID = "client_id";
+    String REDIRECT_URI = "redirect_uri";
+    String REDIRECT_URIS = "redirect_uris";
+    String RESPONSE_TYPE = "response_type";
+    Set<String> ALLOWED_RESPONSE_TYPES = Sets.newHashSet("code", "id_token", "code id_token");
+    String SCOPE = "scope";
+    String ALLOWED_SCOPES = "allowed_scopes";
+    String OFFLINE_ACCESS_SCOPE = "offline_access";
+    Set<String> DEFAULT_SCOPES = Sets.newHashSet("openid", "profile", "email", OFFLINE_ACCESS_SCOPE);
+    String OPENID_SCOPE = SCOPE + "=openid";
+    String STATE = "state";
+    String CODE = "code";
+    String REFRESH_TOKEN = "refresh_token";
+    String REFRESH_TOKEN_TTL= "refresh.token.ttl";
+    long REFRESH_TOKEN_TTL_DEFAULT = 86400000L; // 1 day
+    String CODE_RESPONSE_TYPE = RESPONSE_TYPE + "=" + CODE;
+    String NONCE = "nonce";
+
+    String CODE_CHALLENGE = "code_challenge";
+    String CODE_CHALLENGE_METHOD = "code_challenge_method";
+    String CODE_VERIFIER = "code_verifier";
+    String PKCE_METHOD_S256 = "S256";
+    String PKCE_METHOD_PLAIN = "plain";
+
+    String TOKEN_ID_ATTRIBUTE = "X-Token-Id";
+    String SCOPE_ATTRIBUTE = "X-Token-Scope";
+
+    String FEDERATED_IDENTITY_ID = "federated_identity_id";
+    String FEDERATED_ID_TOKEN_PREFIX = "fed_id_";
+    String FEDERATED_ACCESS_TOKEN_PREFIX = "fed_access_";
+    String FEDERATED_OP_CONFIG_PREFIX = "federated.op.";
+    String FEDERATED_OP_CONFIG_NAMES = FEDERATED_OP_CONFIG_PREFIX + "names";
+
+    String TOKEN_EXCHANGE_TOPOLOGY_NAME = "token.exchange.topology.name";
+}
diff --git a/gateway-util-common/src/main/java/org/apache/knox/gateway/util/knoxidf/KnoxIDFUtils.java b/gateway-util-common/src/main/java/org/apache/knox/gateway/util/knoxidf/KnoxIDFUtils.java
new file mode 100644
index 000000000..989a17e61
--- /dev/null
+++ b/gateway-util-common/src/main/java/org/apache/knox/gateway/util/knoxidf/KnoxIDFUtils.java
@@ -0,0 +1,107 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. The ASF
+ * licenses this file to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ * <p>
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * <p>
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package org.apache.knox.gateway.util.knoxidf;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.text.StringEscapeUtils;
+import org.apache.knox.gateway.util.JsonUtils;
+
+import javax.servlet.ServletContext;
+import javax.servlet.http.HttpServletRequest;
+import javax.ws.rs.core.Response;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+
+public class KnoxIDFUtils {
+
+    private static final int CHUNK_SIZE = 255;
+
+    public static Map<String, String> splitFederatedToken(String token, boolean idToken) {
+        final String prefix = idToken ? KnoxIDFConstants.FEDERATED_ID_TOKEN_PREFIX : KnoxIDFConstants.FEDERATED_ACCESS_TOKEN_PREFIX;
+        final Map<String, String> parts = new LinkedHashMap<>();
+        int i = 0, part = 1;
+        while (i < token.length()) {
+            int end = Math.min(i + CHUNK_SIZE, token.length());
+            parts.put(prefix + part++, token.substring(i, end));
+            i = end;
+        }
+        return parts;
+    }
+
+    public static String joinFederatedToken(Map<String, String> tokenMetadataMap, boolean idToken) {
+        final String prefix = idToken ? KnoxIDFConstants.FEDERATED_ID_TOKEN_PREFIX : KnoxIDFConstants.FEDERATED_ACCESS_TOKEN_PREFIX;
+        return tokenMetadataMap.entrySet().stream()
+                .filter(e -> e.getKey().startsWith(prefix))
+                .sorted(Map.Entry.comparingByKey(
+                        Comparator.comparingInt(k -> Integer.parseInt(k.replace(prefix, "")))
+                ))
+                .map(Map.Entry::getValue)
+                .collect(Collectors.joining());
+    }
+
+    public static Response error(String error, String description) {
+        final Map<String, String> errorMap = new HashMap<>();
+        errorMap.put("error", error);
+        errorMap.put("error_description", description);
+        return Response.status(Response.Status.UNAUTHORIZED).entity(JsonUtils.renderAsJsonString(errorMap)).build();
+    }
+
+    public static String getRequestParamSafe(final HttpServletRequest request, final String key) {
+        String value = request.getParameter(key);
+        if (value == null) {
+            return "";
+        } else {
+            return StringEscapeUtils.escapeHtml4(value);
+        }
+    }
+
+    public static Set<FederatedOpConfiguration> fetchEnabledFederatedOpConfigs(final HttpServletRequest request) {
+        final ServletContext servletContext = request.getServletContext();
+        return servletContext == null ? Collections.emptySet() : new HashSet<>(FederatedOpConfigurationFactory.createFederatedOpConfiguration(servletContext).values());
+    }
+
+    public static AuthorizeRequestMetadata buildAuthRequestMetadata(final HttpServletRequest request) {
+        final String clientId = request.getParameter(KnoxIDFConstants.CLIENT_ID);
+        final String responseType = request.getParameter(KnoxIDFConstants.RESPONSE_TYPE);
+        final String redirectUri = request.getParameter(KnoxIDFConstants.REDIRECT_URI);
+        final String scope = request.getParameter(KnoxIDFConstants.SCOPE);
+        final Set<String> requestedScopes = StringUtils.isBlank(scope) ? KnoxIDFConstants.DEFAULT_SCOPES : new HashSet<>(Arrays.asList(scope.split("\\s+")));
+        final String state = request.getParameter(KnoxIDFConstants.STATE);
+        final String nonce = request.getParameter(KnoxIDFConstants.NONCE);
+        final String codeChallenge = request.getParameter(KnoxIDFConstants.CODE_CHALLENGE);
+        final String codeChallengeMethod = request.getParameter(KnoxIDFConstants.CODE_CHALLENGE_METHOD);
+        return new AuthorizeRequestMetadata(clientId, null, responseType, redirectUri, requestedScopes, state, nonce, codeChallenge, codeChallengeMethod);
+    }
+
+    public static String buildFederatedOpAuthRedirect(final FederatedOpConfiguration federatedOpConfiguration, final String federatedState) {
+        return federatedOpConfiguration.getAuthorizeEndpoint()
+                + "?" + KnoxIDFConstants.CLIENT_ID + "=" + federatedOpConfiguration.getClientId()
+                + "&" + KnoxIDFConstants.REDIRECT_URI + "=" + federatedOpConfiguration.getAuthorizeCallback()
+                + "&" + KnoxIDFConstants.CODE_RESPONSE_TYPE
+                + "&" + KnoxIDFConstants.OPENID_SCOPE
+                + "&" + KnoxIDFConstants.STATE + "=" + federatedState;
+    }
+
+}
diff --git a/pom.xml b/pom.xml
index 26a680e5c..5f40cd6ef 100644
--- a/pom.xml
+++ b/pom.xml
@@ -148,6 +148,7 @@
         <module>gateway-service-metadata</module>
         <module>gateway-service-session</module>
         <module>gateway-openapi-ui</module>
+        <module>gateway-service-knoxidf</module>
     </modules>
 
     <properties>
@@ -286,6 +287,7 @@
         <taglibs-standard.version>1.2.5</taglibs-standard.version>
         <testcontainers.version>1.15.1</testcontainers.version>
         <txw2.version>2.4.0-b180830.0438</txw2.version>
+        <uuid.generator.version>5.2.0</uuid.generator.version>
         <velocity.version>2.4.1</velocity.version>
         <woodstox-core.version>6.4.0</woodstox-core.version>
         <xmlsec.version>4.0.4</xmlsec.version>
@@ -1425,6 +1427,11 @@
                 <artifactId>knox-token-generation-ui</artifactId>
                 <version>${project.version}</version>
             </dependency>
+            <dependency>
+                <groupId>org.apache.knox</groupId>
+                <artifactId>gateway-service-knoxidf</artifactId>
+                <version>${project.version}</version>
+            </dependency>
             <dependency>
                 <groupId>org.glassfish.jersey.containers</groupId>
                 <artifactId>jersey-container-servlet-core</artifactId>
@@ -1440,6 +1447,11 @@
                 <artifactId>jersey-server</artifactId>
                 <version>${jersey.version}</version>
             </dependency>
+            <dependency>
+                <groupId>org.glassfish.jersey.core</groupId>
+                <artifactId>jersey-common</artifactId>
+                <version>${jersey.version}</version>
+            </dependency>
 
             <dependency>
                 <groupId>org.glassfish.jersey.inject</groupId>
@@ -2035,6 +2047,11 @@
                 <artifactId>woodstox-core</artifactId>
                 <version>${woodstox-core.version}</version>
             </dependency>
+            <dependency>
+                <groupId>com.fasterxml.uuid</groupId>
+                <artifactId>java-uuid-generator</artifactId>
+                <version>${uuid.generator.version}</version>
+            </dependency>
 
             <dependency>
                 <groupId>cglib</groupId>