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>