diff --git a/.github/workflows/build/Dockerfile b/.github/workflows/build/Dockerfile index 441cf1ba43..7b9f42de8d 100644 --- a/.github/workflows/build/Dockerfile +++ b/.github/workflows/build/Dockerfile @@ -38,6 +38,8 @@ ADD gateway-site.xml /knox-runtime/conf/gateway-site.xml ADD conf/topologies/knoxtoken.xml /knox-runtime/conf/topologies/knoxtoken.xml ADD conf/topologies/knoxldap.xml /knox-runtime/conf/topologies/knoxldap.xml ADD conf/topologies/remoteauth.xml /knox-runtime/conf/topologies/remoteauth.xml +ADD conf/topologies/knoxidf-ldap.xml /knox-runtime/conf/topologies/knoxidf-ldap.xml +ADD conf/topologies/knoxidf-token.xml /knox-runtime/conf/topologies/knoxidf-token.xml ADD conf/topologies/health.xml /knox-runtime/conf/topologies/health.xml diff --git a/.github/workflows/build/Dockerfile.local b/.github/workflows/build/Dockerfile.local index 73e1c9bfe8..d2ff6815da 100644 --- a/.github/workflows/build/Dockerfile.local +++ b/.github/workflows/build/Dockerfile.local @@ -60,6 +60,8 @@ ADD conf/topologies/knoxtoken.xml /knox-runtime/conf/topologies/knoxtoken.xml ADD conf/topologies/health.xml /knox-runtime/conf/topologies/health.xml ADD conf/topologies/knoxldap.xml /knox-runtime/conf/topologies/knoxldap.xml ADD conf/topologies/remoteauth.xml /knox-runtime/conf/topologies/remoteauth.xml +ADD conf/topologies/knoxidf-ldap.xml /knox-runtime/conf/topologies/knoxidf-ldap.xml +ADD 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 0000000000..b0c340f6b1 --- /dev/null +++ b/.github/workflows/build/conf/topologies/knoxidf-ldap.xml @@ -0,0 +1,71 @@ + + + + + authentication + ShiroProvider + true + + main.ldapRealm + org.apache.knox.gateway.shirorealm.KnoxLdapRealm + + + main.ldapRealm.userDnTemplate + uid={0},ou=people,dc=hadoop,dc=apache,dc=org + + + main.ldapRealm.contextFactory.url + ldap://ldap:33389 + + + main.ldapRealm.contextFactory.authenticationMechanism + simple + + + urls./knoxidf/api/v1/.well-known/openid-configuration + anon + + + urls./knoxidf/api/v1/client/register + anon + + + urls./knoxidf/api/v1/authorize/callback + anon + + + urls./knoxidf/api/v1/jwks + anon + + + urls./** + authcBasic + + + + identity-assertion + Default + true + + + + + KNOXIDF + + knoxidf.knox.token.ttl + 60000 + + + knoxidf.knox.token.limit.per.user + -1 + + + token.exchange.topology.name + knoxidf-token + + + user.params.provider.ldap.url + ldap://ldap:33389 + + + 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 0000000000..002c3813d2 --- /dev/null +++ b/.github/workflows/build/conf/topologies/knoxidf-token.xml @@ -0,0 +1,46 @@ + + + + + federation + JWTProvider + true + + knox.token.exp.server-managed + true + + + + identity-assertion + Default + true + + + + + KNOXIDF + + knoxidf.knox.token.ttl + 86400000 + + + knoxidf.knox.token.limit.per.user + -1 + + + user.params.provider.ldap.url + ldap://ldap:33389 + + + + KNOXTOKEN + + knox.token.ttl + 60000 + + + knox.token.limit.per.user + -1 + + + diff --git a/.github/workflows/compose/docker-compose.yml b/.github/workflows/compose/docker-compose.yml index 3aa1798edc..eab1382f23 100644 --- a/.github/workflows/compose/docker-compose.yml +++ b/.github/workflows/compose/docker-compose.yml @@ -25,8 +25,8 @@ services: context: ../build dockerfile: Dockerfile.local args: - knoxurl: ${knoxurl:-https://github.com/apache/knox.git} - branch: ${branch:-master} + knoxurl: ${knoxurl:-https://github.com/smolnar82/knox.git} + branch: ${branch:-knox_idf_smolnar} image: apache/knox-dev:local-${GITHUB_RUN_ID:-local}-${GITHUB_RUN_ID:-local} ldap: diff --git a/.github/workflows/publish-test-results.yml b/.github/workflows/publish-test-results.yml index 0f584f599c..621830115d 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 4f90c4a379..40f03845c4 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -82,6 +82,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 @@ -89,6 +97,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 41198b09e1..0b544b4e98 100644 --- a/.github/workflows/tests/common_utils.py +++ b/.github/workflows/tests/common_utils.py @@ -15,6 +15,8 @@ from __future__ import annotations +import base64 +import json import os import unittest from typing import Any @@ -66,3 +68,32 @@ def collect_actor_group_values( def assert_hsts_header(testcase: unittest.TestCase, response: requests.Response) -> None: 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 0000000000..93825fba5d --- /dev/null +++ b/.github/workflows/tests/test_knoxidf.py @@ -0,0 +1,207 @@ +# 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 +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"]) + +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 1af440809d..dc1ab640c2 100644 --- a/build-tools/src/main/resources/build-tools/spotbugs-filter.xml +++ b/build-tools/src/main/resources/build-tools/spotbugs-filter.xml @@ -90,4 +90,9 @@ limitations under the License. + + + + + 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 50379c68d4..b4fa53ceed 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(` +
+ Or +
+ `); + } + + ops.forEach(op => { + container.append(` +
+ 🌐 + Continue with ${op} +
+ `); + }); +}; + +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 bd90cdebb8..5151b98781 100644 --- a/gateway-applications/src/main/resources/applications/knoxauth/app/login.html +++ b/gateway-applications/src/main/resources/applications/knoxauth/app/login.html @@ -31,7 +31,7 @@ - + @@ -90,7 +90,7 @@ - +
@@ -116,5 +116,9 @@
+
+ +
+ 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 27fae58fff..1da0548c25 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 @@ -1913,4 +1913,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-discovery-cm/pom.xml b/gateway-discovery-cm/pom.xml index 8a484cb8ce..86332b9d5d 100644 --- a/gateway-discovery-cm/pom.xml +++ b/gateway-discovery-cm/pom.xml @@ -63,6 +63,10 @@ org.apache.knox gateway-util-configinjector + + org.apache.knox + gateway-util-common + com.cloudera.api.swagger cloudera-manager-api-swagger diff --git a/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/ApiClientFactory.java b/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/ApiClientFactory.java index 6ba9f90068..ce1babeb3d 100644 --- a/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/ApiClientFactory.java +++ b/gateway-discovery-cm/src/main/java/org/apache/knox/gateway/topology/discovery/cm/ApiClientFactory.java @@ -21,28 +21,26 @@ import org.apache.knox.gateway.services.security.AliasService; import org.apache.knox.gateway.services.security.AliasServiceException; import org.apache.knox.gateway.topology.discovery.ServiceDiscoveryConfig; +import org.apache.knox.gateway.util.TruststorePasswordSetter; import java.security.KeyStore; public class ApiClientFactory { private static final ClouderaManagerServiceDiscoveryMessages LOG = MessagesFactory.get(ClouderaManagerServiceDiscoveryMessages.class); - static final String TRUSTSTORE_PASSWORD_SYSTEM_PROPERTY = "javax.net.ssl.trustStorePassword"; - public static final String TRUSTSTORE_PASSWORD_ALIAS = "cm.discovery.trustStorePassword"; public static DiscoveryApiClient getApiClient(final GatewayConfig gatewayConfig, final ServiceDiscoveryConfig discoveryConfig, final AliasService aliasService, final KeyStore truststore) { + final char[] trustStorePassword; try { - final char[] trustStorePassword = aliasService.getPasswordFromAliasForGateway(TRUSTSTORE_PASSWORD_ALIAS); - if (trustStorePassword != null && trustStorePassword.length > 0) { - System.setProperty(TRUSTSTORE_PASSWORD_SYSTEM_PROPERTY, new String(trustStorePassword)); - } - return new DiscoveryApiClient(gatewayConfig, discoveryConfig, aliasService, truststore); + trustStorePassword = aliasService.getPasswordFromAliasForGateway(TruststorePasswordSetter.TRUSTSTORE_PASSWORD_ALIAS); } catch (AliasServiceException e) { LOG.clouderaManagerApiClientBuildError(e); throw new ServiceDiscoveryException("Unable to retrieve CM service discovery truststore password", e); - } finally { - System.clearProperty(TRUSTSTORE_PASSWORD_SYSTEM_PROPERTY); + } + + try (TruststorePasswordSetter ignored = new TruststorePasswordSetter(trustStorePassword)) { + return new DiscoveryApiClient(gatewayConfig, discoveryConfig, aliasService, truststore); } } } diff --git a/gateway-discovery-cm/src/test/java/org/apache/knox/gateway/topology/discovery/cm/ApiClientFactoryTest.java b/gateway-discovery-cm/src/test/java/org/apache/knox/gateway/topology/discovery/cm/ApiClientFactoryTest.java index 322166fda2..6125f47c5b 100644 --- a/gateway-discovery-cm/src/test/java/org/apache/knox/gateway/topology/discovery/cm/ApiClientFactoryTest.java +++ b/gateway-discovery-cm/src/test/java/org/apache/knox/gateway/topology/discovery/cm/ApiClientFactoryTest.java @@ -21,6 +21,7 @@ import org.apache.knox.gateway.services.security.AliasService; import org.apache.knox.gateway.services.security.AliasServiceException; import org.apache.knox.gateway.topology.discovery.ServiceDiscoveryConfig; +import org.apache.knox.gateway.util.TruststorePasswordSetter; import org.easymock.EasyMock; import org.junit.After; import org.junit.Assert; @@ -77,9 +78,9 @@ private void testGetApiClient(final boolean shouldSetSystemProperty, String trus EasyMock.expect(serviceDiscoveryConfig.getPasswordAlias()).andReturn("myCmPasswordAlias").anyTimes(); final AliasService aliasService = EasyMock.createMock(AliasService.class); if (shouldSetSystemProperty) { - EasyMock.expect(aliasService.getPasswordFromAliasForGateway(ApiClientFactory.TRUSTSTORE_PASSWORD_ALIAS)).andReturn(trustStorePassword.toCharArray()).anyTimes(); + EasyMock.expect(aliasService.getPasswordFromAliasForGateway(TruststorePasswordSetter.TRUSTSTORE_PASSWORD_ALIAS)).andReturn(trustStorePassword.toCharArray()).anyTimes(); } else { - EasyMock.expect(aliasService.getPasswordFromAliasForGateway(ApiClientFactory.TRUSTSTORE_PASSWORD_ALIAS)).andReturn(null).anyTimes(); + EasyMock.expect(aliasService.getPasswordFromAliasForGateway(TruststorePasswordSetter.TRUSTSTORE_PASSWORD_ALIAS)).andReturn(null).anyTimes(); } EasyMock.expect(aliasService.getPasswordFromAliasForGateway("myCmPasswordAlias")).andReturn("myCmPassword".toCharArray()).anyTimes(); final KeyStore trustStore = EasyMock.createMock(KeyStore.class); @@ -88,13 +89,13 @@ private void testGetApiClient(final boolean shouldSetSystemProperty, String trus ApiClientFactory.getApiClient(gatewayConfig, serviceDiscoveryConfig, aliasService, trustStore); if (shouldSetSystemProperty && StringUtils.isNotBlank(trustStorePassword)) { - Assert.assertEquals(ApiClientFactory.TRUSTSTORE_PASSWORD_SYSTEM_PROPERTY, testProps.lastSetKey); + Assert.assertEquals(TruststorePasswordSetter.TRUSTSTORE_PASSWORD_SYSTEM_PROPERTY, testProps.lastSetKey); Assert.assertEquals(trustStorePassword, testProps.lastSetValue); - Assert.assertEquals(ApiClientFactory.TRUSTSTORE_PASSWORD_SYSTEM_PROPERTY, testProps.lastRemovedKey); + Assert.assertEquals(TruststorePasswordSetter.TRUSTSTORE_PASSWORD_SYSTEM_PROPERTY, testProps.lastRemovedKey); } else { Assert.assertNull(testProps.lastSetKey); Assert.assertNull(testProps.lastSetValue); - Assert.assertEquals(ApiClientFactory.TRUSTSTORE_PASSWORD_SYSTEM_PROPERTY, testProps.lastRemovedKey); + Assert.assertEquals(TruststorePasswordSetter.TRUSTSTORE_PASSWORD_SYSTEM_PROPERTY, testProps.lastRemovedKey); } } diff --git a/gateway-discovery-cm/src/test/java/org/apache/knox/gateway/topology/discovery/cm/monitor/PollingConfigurationAnalyzerTest.java b/gateway-discovery-cm/src/test/java/org/apache/knox/gateway/topology/discovery/cm/monitor/PollingConfigurationAnalyzerTest.java index 5d734027b9..9d6dbd95bd 100644 --- a/gateway-discovery-cm/src/test/java/org/apache/knox/gateway/topology/discovery/cm/monitor/PollingConfigurationAnalyzerTest.java +++ b/gateway-discovery-cm/src/test/java/org/apache/knox/gateway/topology/discovery/cm/monitor/PollingConfigurationAnalyzerTest.java @@ -32,9 +32,9 @@ import org.apache.knox.gateway.services.topology.impl.GatewayStatusService; import org.apache.knox.gateway.topology.ClusterConfigurationMonitorService; import org.apache.knox.gateway.topology.discovery.ServiceDiscoveryConfig; -import org.apache.knox.gateway.topology.discovery.cm.ApiClientFactory; import org.apache.knox.gateway.topology.discovery.cm.model.hdfs.NameNodeServiceModelGenerator; import org.apache.knox.gateway.topology.discovery.cm.model.hive.HiveOnTezServiceModelGenerator; +import org.apache.knox.gateway.util.TruststorePasswordSetter; import org.easymock.EasyMock; import org.junit.After; import org.junit.Test; @@ -363,7 +363,7 @@ public void testClusterConfigMonitorTerminationForNoLongerReferencedClusters() t EasyMock.replay(ts, ccms, gatewayStatusService, gws); AliasService aliasService = EasyMock.createNiceMock(AliasService.class); - EasyMock.expect(aliasService.getPasswordFromAliasForGateway(ApiClientFactory.TRUSTSTORE_PASSWORD_ALIAS)).andReturn(null).anyTimes(); + EasyMock.expect(aliasService.getPasswordFromAliasForGateway(TruststorePasswordSetter.TRUSTSTORE_PASSWORD_ALIAS)).andReturn(null).anyTimes(); EasyMock.replay(aliasService); try { @@ -540,7 +540,7 @@ private TestablePollingConfigAnalyzer buildPollingConfigAnalyzer(final String ad EasyMock.replay(configCache); AliasService aliasService = EasyMock.createNiceMock(AliasService.class); - EasyMock.expect(aliasService.getPasswordFromAliasForGateway(ApiClientFactory.TRUSTSTORE_PASSWORD_ALIAS)).andReturn(null).anyTimes(); + EasyMock.expect(aliasService.getPasswordFromAliasForGateway(TruststorePasswordSetter.TRUSTSTORE_PASSWORD_ALIAS)).andReturn(null).anyTimes(); EasyMock.replay(aliasService); if (isKnoxGatewayReady) { diff --git a/gateway-docker/src/main/resources/docker/Dockerfile b/gateway-docker/src/main/resources/docker/Dockerfile index 052ac3d12e..7121de75d5 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 " diff --git a/gateway-docker/src/main/resources/docker/gateway-entrypoint.sh b/gateway-docker/src/main/resources/docker/gateway-entrypoint.sh index 015dbe2fac..2cb0277a3d 100755 --- a/gateway-docker/src/main/resources/docker/gateway-entrypoint.sh +++ b/gateway-docker/src/main/resources/docker/gateway-entrypoint.sh @@ -42,10 +42,10 @@ set -o pipefail ## Helper function used to import certs into truststore ## Function takes cert file as argument +## At this time ALIAS_PASSPHRASE is already initialized importMultipleCerts() { FILE=$1 local import_failed=0 - ALIAS_PASSPHRASE=$(/bin/cat "${KEYSTORE_PASSWORD_FILE}") # number of certs in the PEM file CERTS=$(/bin/grep 'END CERTIFICATE' "$FILE"| /usr/bin/wc -l) # For every cert in the PEM file, extract it and import into the JKS keystore @@ -139,10 +139,11 @@ fi if [[ -n ${KEYSTORE_PASSWORD_FILE} ]] && [[ -f ${KEYSTORE_PASSWORD_FILE} ]] then - echo "Using provided keystore password file" + echo "Setting ALIAS_PASSPHRASE from provided keystore password file: ${KEYSTORE_PASSWORD_FILE}" ALIAS_PASSPHRASE=$(/bin/cat "${KEYSTORE_PASSWORD_FILE}" 2> /dev/null) else # If keystore password is not provided use master secret as alias passphrase + echo "Setting ALIAS_PASSPHRASE to MASTER_SECRET" ALIAS_PASSPHRASE="${MASTER_SECRET}" fi 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 31b3df4112..ffa5d0868e 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 @@ -22,6 +22,7 @@ import org.apache.knox.gateway.i18n.messages.MessagesFactory; import org.apache.knox.gateway.provider.federation.jwt.JWTMessages; import org.apache.knox.gateway.security.PrimaryPrincipal; +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.services.security.token.impl.JWTToken; @@ -29,6 +30,7 @@ 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; @@ -48,10 +50,11 @@ 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 { @@ -188,7 +191,8 @@ public void doFilter(ServletRequest request, ServletResponse response, FilterCha try { JWT token = new JWTToken(tokenValue); if (validateToken((HttpServletRequest) request, (HttpServletResponse) response, chain, token)) { - Subject subject = createSubjectFromToken(token); + final Subject subject = createSubjectFromToken(token); + addKnoxIDFAttributes(request, token); continueWithEstablishedSecurityContext(subject, (HttpServletRequest) request, (HttpServletResponse) response, chain); } } catch (ParseException | UnknownTokenException ex) { @@ -210,7 +214,8 @@ public void doFilter(ServletRequest request, ServletResponse response, FilterCha } 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); @@ -224,6 +229,14 @@ public void doFilter(ServletRequest request, ServletResponse response, FilterCha } } + 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); @@ -322,8 +335,8 @@ private Pair getTokenFromRequestBody(ServletRequest request) 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 a8e7b8f8de..7558f5a2ed 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.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 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 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 @@ protected String constructLoginURL(HttpServletRequest request) { delimiter = "&"; } + final Set 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 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 90c056f2ea..d96b2442ad 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.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 f0f14b365d..7501dbe4b4 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 void init(FilterConfig filterConfig) throws ServletException { @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 99600f8746..cfee258c34 100644 --- a/gateway-release/home/conf/topologies/knoxsso.xml +++ b/gateway-release/home/conf/topologies/knoxsso.xml @@ -73,6 +73,10 @@ main.ldapRealm.contextFactory.authenticationMechanism simple + + urls./api/v1/websso/federated/op + anon + urls./** authcBasic diff --git a/gateway-release/home/conf/users.ldif b/gateway-release/home/conf/users.ldif index 986704dc40..c148b6865b 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 c8b7c74d42..391c1a25ba 100644 --- a/gateway-release/pom.xml +++ b/gateway-release/pom.xml @@ -524,5 +524,9 @@ org.apache.knox gateway-service-restcatalog + + org.apache.knox + gateway-service-knoxidf + 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 2e2482aa1d..139e51862b 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 7a7afadd13..a9d544fe85 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 2009872782..3f627bdac8 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 String descriptorsTableSql() { 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-server/src/main/java/org/apache/knox/gateway/database/KnoxDatabase.java b/gateway-server/src/main/java/org/apache/knox/gateway/database/KnoxDatabase.java new file mode 100644 index 0000000000..261b368998 --- /dev/null +++ b/gateway-server/src/main/java/org/apache/knox/gateway/database/KnoxDatabase.java @@ -0,0 +1,36 @@ +/* + * 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. + */ +package org.apache.knox.gateway.database; + +import org.apache.knox.gateway.services.token.impl.TokenStateDatabase; + +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 dfe4a4ea90..564f793007 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 @@ private static void initialize( 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 06ce95d93a..cdbafc7bbb 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 @@ -90,6 +90,8 @@ public void init(GatewayConfig config, Map options) throws Servic ldapService.init(config, options); addService(ServiceType.LDAP_SERVICE, ldapService); } + + 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 0000000000..f78f96af6d --- /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 + *

+ * 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. + */ +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 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 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 0000000000..8ce9e550e9 --- /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 + *

+ * 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. + */ +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 findById(String identityId) { + return Optional.empty(); + } + + @Override + public Optional findByProviderAndSubject(String provider, String externalIssuer, String externalSubject) { + return Optional.empty(); + } + + @Override + public void init(GatewayConfig config, Map 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 0000000000..649bf30388 --- /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 + *

+ * 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. + */ +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 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 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 0000000000..d6f820a1d5 --- /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 + *

+ * 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. + */ +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 0000000000..8e26bdc1d5 --- /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 + *

+ * 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. + */ +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 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 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 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 dbc89d6950..c7579a4805 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 @@ 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 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 4c015979f9..c48af4a587 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,8 +33,6 @@ 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 \ No newline at end of file +org.apache.knox.gateway.services.factory.TokenServiceFactory \ No newline at end of file diff --git a/gateway-server/src/main/resources/createKnoxIDFFederatedIdentityAttributesTable.sql b/gateway-server/src/main/resources/createKnoxIDFFederatedIdentityAttributesTable.sql new file mode 100644 index 0000000000..239cb5df1a --- /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 0000000000..13ae9ab113 --- /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 0000000000..7ed07b1b05 --- /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 0000000000..acaf1c3404 --- /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 0000000000..7152d4c71d --- /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 0000000000..0dc42c1f72 --- /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 b27358c7cb..1a5483c537 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 @@ -66,7 +66,8 @@ public void testAddStartAndStop() throws ServiceLifecycleException { ServiceType.CONCURRENT_SESSION_VERIFIER, ServiceType.REMOTE_CONFIGURATION_MONITOR, ServiceType.GATEWAY_STATUS_SERVICE, - ServiceType.LDAP_SERVICE + ServiceType.LDAP_SERVICE, + ServiceType.KNOXIDF_FEDERATED_IDENTITY_SERVICE }; assertNotEquals(ServiceType.values(), orderedServiceTypes); diff --git a/gateway-service-knoxidf/pom.xml b/gateway-service-knoxidf/pom.xml new file mode 100644 index 0000000000..48296b435d --- /dev/null +++ b/gateway-service-knoxidf/pom.xml @@ -0,0 +1,115 @@ + + + + 4.0.0 + + org.apache.knox + gateway + 3.0.0-SNAPSHOT + + + gateway-service-knoxidf + gateway-service-knoxidf + + + + org.apache.knox + gateway-i18n + + + org.apache.knox + gateway-spi + + + org.apache.knox + gateway-provider-jersey + + + org.apache.knox + gateway-util-common + + + org.apache.knox + gateway-service-knoxtoken + + + + javax.annotation + javax.annotation-api + + + javax.ws.rs + javax.ws.rs-api + + + javax.servlet + javax.servlet-api + + + + com.google.guava + guava + + + commons-io + commons-io + + + com.fasterxml.jackson.core + jackson-core + + + com.fasterxml.jackson.core + jackson-databind + + + com.fasterxml.uuid + java-uuid-generator + + + com.nimbusds + nimbus-jose-jwt + + + com.github.ben-manes.caffeine + caffeine + + + org.apache.commons + commons-lang3 + + + org.apache.commons + commons-text + + + org.apache.httpcomponents + httpclient + + + org.apache.httpcomponents + httpcore + + + org.glassfish.jersey.core + jersey-common + + + 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 0000000000..6200edc835 --- /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 + *

+ * 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. + */ +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 scopes = new HashSet<>(Arrays.asList(scope.split("\\s+"))); + + try (PrintWriter out = response.getWriter()) { + out.println(""); + out.println("Consent Required"); + out.println(""); + out.println(""); + out.println("

"); + out.println("

Application Consent Required

"); + out.printf(Locale.US, "

The application %s is requesting access to your account.

%n", clientId); + + if (!scopes.isEmpty()) { + out.println("

This application will be able to:

"); + out.println("
    "); + for (String s : scopes) { + out.printf(Locale.US, "
  • %s
  • %n", describeScope(s)); + } + out.println("
"); + } + + out.println("
"); + out.printf(Locale.US, "%n", state); + out.println("
"); + out.println(""); + out.println(""); + out.println("
"); + out.println("
"); + out.println("
"); + out.println(""); + out.println(""); + } + } + + 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 0000000000..a49369ee4e --- /dev/null +++ b/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/AuthorizeResource.java @@ -0,0 +1,389 @@ +/* + * 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. + */ +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.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.KnoxIDFConstants.OFFLINE_ACCESS_SCOPE; +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 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(); + } + + public Response authorize() { + try { + return authorize( + request.getParameter("response_type"), + request.getParameter("client_id"), + request.getParameter("redirect_uri"), + request.getParameter("scope"), + request.getParameter("state"), + request.getParameter("nonce")); + } catch (Exception e) { + return error("server_error", e.getMessage()); + } + } + + public Response authorize(String responseType, + String clientId, + String redirectUri, + String scope, + String state, + String nonce) throws Exception { + final String subject = SubjectUtils.getCurrentEffectivePrincipalName(); + final Set 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); + 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 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 federatedTokens) throws Exception { + final Response tokenResponse = getAuthenticationToken(); + if (tokenResponse.getStatus() == Response.Status.OK.getStatusCode()) { + final Map 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) throws UnsupportedEncodingException { + final String redirectLocation = authorizeRequestMetadata.getRedirectUri() + + "?code=" + URLEncoder.encode(code, UTF_8) + + "&state=" + URLEncoder.encode(authorizeRequestMetadata.getState(), 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 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()); + } + + @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 federatedTokens) throws Exception { + final Map 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("userName", 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 (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 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 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 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 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 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 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 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 0000000000..def54ad2fe --- /dev/null +++ b/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/DiscoveryResource.java @@ -0,0 +1,74 @@ +/* + * 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. + */ +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; + +@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 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"}); + return Response.ok(JsonUtils.renderAsJsonString(config)).build(); + } + +} diff --git a/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/JwksResource.java b/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/JwksResource.java new file mode 100644 index 0000000000..47f802b04d --- /dev/null +++ b/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/JwksResource.java @@ -0,0 +1,39 @@ +/* + * 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. + */ +package org.apache.knox.gateway.service.knoxidf; + +import org.apache.knox.gateway.service.knoxtoken.JWKSResource; + +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/LdapUserParamsProvider.java b/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/LdapUserParamsProvider.java new file mode 100644 index 0000000000..0bd567c3fd --- /dev/null +++ b/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/LdapUserParamsProvider.java @@ -0,0 +1,165 @@ +/* + * 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. + */ +package org.apache.knox.gateway.service.knoxidf; + +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 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 { + + // === Hardcoded LDAP config for now === + private static final String BASE_DN = "dc=hadoop,dc=apache,dc=org"; + private static final String USER_DN_TEMPLATE = "uid=%s,ou=people," + BASE_DN; + private static final String SYSTEM_USER = "uid=admin,ou=people," + BASE_DN; + private static final String SYSTEM_PASSWORD = "admin-password"; + + private static final String[] ATTRIBUTES = {"cn", "sn", "givenName", "mail"}; + + private final String ldapUrl; + + LdapUserParamsProvider(String ldapUrl) { + this.ldapUrl = ldapUrl; + } + + @Override + public Map getParamsFor(String subjectName, String scope) { + Map userParams = new HashMap<>(); + if ("anonymous".equalsIgnoreCase(subjectName)) { + return userParams; + } + + Set requestedClaims = OIDCScope.claimsForScopes(scope); + + LdapContext ctx = null; + try { + ctx = createSystemContext(); + + String userDn = String.format(Locale.US, USER_DN_TEMPLATE, subjectName); + + SearchControls controls = new SearchControls(); + controls.setSearchScope(SearchControls.OBJECT_SCOPE); + controls.setReturningAttributes(ATTRIBUTES); + + NamingEnumeration 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 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 fetchRoles(LdapContext ctx, String userDn) throws Exception { + List roles = new ArrayList<>(); + + SearchControls groupControls = new SearchControls(); + groupControls.setSearchScope(SearchControls.ONELEVEL_SCOPE); + groupControls.setReturningAttributes(new String[]{"cn", "member"}); + + String groupsBase = "ou=groups," + BASE_DN; + NamingEnumeration 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 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, SYSTEM_USER); + env.put(Context.SECURITY_CREDENTIALS, SYSTEM_PASSWORD); + 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-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 0000000000..d673f3f454 --- /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 + *

+ * 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. + */ +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 claims; + + OIDCScope(Set claims) { + this.claims = Collections.unmodifiableSet(new HashSet<>(claims)); + } + + public Set getClaims() { + return claims; + } + + public static Set claimsForScopes(String scopeString) { + Set 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 0000000000..05675e4917 --- /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 + *

+ * 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. + */ +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 redirectUris; + private List 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 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 0000000000..5507a17675 --- /dev/null +++ b/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/TokenResource.java @@ -0,0 +1,406 @@ +/* + * 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. + */ +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.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.text.ParseException; +import java.util.HashMap; +import java.util.Map; + +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.OFFLINE_ACCESS_SCOPE; +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 = new LdapUserParamsProvider(servletContext.getInitParameter("user.params.provider.ldap.url")); + 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 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 thw 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 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); + } + } + } catch (UnknownTokenException e) { + throw new AuthTokenValidationError("Unknown auth_code"); + } + } + + 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 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 0000000000..e5bcb01e5a --- /dev/null +++ b/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/UserInfoResource.java @@ -0,0 +1,139 @@ +/* + * 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. + */ +package org.apache.knox.gateway.service.knoxidf; + + +import org.apache.commons.lang3.StringUtils; +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 = new LdapUserParamsProvider(servletContext.getInitParameter("user.params.provider.ldap.url")); + 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 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 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-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/UserParamsProvider.java b/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/UserParamsProvider.java new file mode 100644 index 0000000000..04085e48d8 --- /dev/null +++ b/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/UserParamsProvider.java @@ -0,0 +1,30 @@ +/* + * 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. + */ +package org.apache.knox.gateway.service.knoxidf; + +import java.util.Map; + +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 getParamsFor(String subjectName, String scope); +} diff --git a/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/deploy/KnoxIDFServiceDeploymentContributor.java b/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/deploy/KnoxIDFServiceDeploymentContributor.java new file mode 100644 index 0000000000..cfbb4647cb --- /dev/null +++ b/gateway-service-knoxidf/src/main/java/org/apache/knox/gateway/service/knoxidf/deploy/KnoxIDFServiceDeploymentContributor.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 + *

+ * 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. + */ +package org.apache.knox.gateway.service.knoxidf.deploy; + +import org.apache.knox.gateway.jersey.JerseyServiceDeploymentContributorBase; + +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-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 0000000000..49fc687f12 --- /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 16e6762403..5da9f343eb 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.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 targetAudiences = new ArrayList<>(); + private final List targetAudiences = new ArrayList<>(); private boolean enableSession; private String signatureAlgorithm; private List 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 @@ private void handleCookieSetup() { 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 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 d23c693ee5..4c17c02db9 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.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 Response getAuthenticationToken() { 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 Response getAuthenticationToken() { return resp.responseBuilder.build(); } } + + protected void decorateResponseMap(Map 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 6fec10e0f2..9dd26ba444 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 @@ -115,7 +115,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"; @@ -145,6 +145,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 long TOKEN_TTL_DEFAULT = 30000L; static final String TOKEN_API_PATH = "knoxtoken/api/v1"; @@ -186,6 +187,7 @@ public class TokenResource { private Optional maxTokenLifetime = Optional.empty(); private int tokenLimitPerUser; + private Map hardCodedClaimMappings; private boolean includeGroupsInTokenAllowed; private String tokenIssuer; @@ -359,9 +361,37 @@ public void init() throws AliasServiceException, ServiceLifecycleException, KeyL .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"; @@ -664,7 +694,7 @@ public Response revoke(String token) { } 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."; @@ -716,22 +746,6 @@ private boolean triesToRevokeOwnToken(String tokenId, String revoker) throws Unk 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}) @@ -839,16 +853,17 @@ protected Response getAuthenticationToken() { 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 { @@ -934,10 +949,16 @@ protected UserContext buildUserContext(HttpServletRequest request) { protected static class UserContext { public final String userName; public final String createdBy; + private final Map userParams; public UserContext(String userName, String createdBy) { + this(userName, createdBy, Collections.emptyMap()); + } + + public UserContext(String userName, String createdBy, Map userParams) { this.userName = userName; this.createdBy = createdBy; + this.userParams = userParams; } } @@ -1009,13 +1030,10 @@ protected Response enforceClientCertIfRequired() { 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); @@ -1029,7 +1047,7 @@ protected void persistTokenDetails(ResponseMap result, long expires, String user } } - 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; @@ -1073,7 +1091,7 @@ public ResponseMap(String accessToken, String tokenId, Map map, } } - 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); @@ -1081,8 +1099,9 @@ protected JWT getJWT(String userName, long expires, String jku) throws TokenServ final JWTokenAttributesBuilder jwtAttributesBuilder = new JWTokenAttributesBuilder(); jwtAttributesBuilder .setIssuer(tokenIssuer) - .setUserName(userName) + .setUserName(userContext.userName) .setAlgorithm(signatureAlgorithm) + .setIssueTime(issueTime) .setExpires(expires) .setManaged(managedToken) .setJku(jku) @@ -1102,6 +1121,14 @@ protected JWT getJWT(String userName, long expires, String jku) throws TokenServ } } + 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-service-metadata/src/main/java/org/apache/knox/gateway/service/metadata/KnoxMetadataResource.java b/gateway-service-metadata/src/main/java/org/apache/knox/gateway/service/metadata/KnoxMetadataResource.java index a01d5a554c..75eb88baa4 100644 --- a/gateway-service-metadata/src/main/java/org/apache/knox/gateway/service/metadata/KnoxMetadataResource.java +++ b/gateway-service-metadata/src/main/java/org/apache/knox/gateway/service/metadata/KnoxMetadataResource.java @@ -69,6 +69,7 @@ import org.apache.knox.gateway.topology.Service; import org.apache.knox.gateway.topology.Topology; import org.apache.knox.gateway.util.JsonUtils; +import org.apache.knox.gateway.util.TruststorePasswordSetter; import org.apache.knox.gateway.util.X509CertificateUtil; import com.kstruct.gethostname4j.Hostname; @@ -173,11 +174,16 @@ private Response generateFailureFileDownloadResponse(Status status, String error private Certificate[] getPublicCertificates() { try { - return X509CertificateUtil.fetchPublicCertsFromServer(request.getRequestURL().toString(), true, null); + final GatewayServices gatewayServices = (GatewayServices) request.getServletContext().getAttribute(GatewayServices.GATEWAY_SERVICES_ATTRIBUTE); + if (gatewayServices != null) { + final AliasService aliasService = gatewayServices.getService(ServiceType.ALIAS_SERVICE); + char[] trustStorePassword = aliasService.getPasswordFromAliasForGateway(TruststorePasswordSetter.TRUSTSTORE_PASSWORD_ALIAS); + return X509CertificateUtil.fetchPublicCertsFromServer(request.getRequestURL().toString(), trustStorePassword, true, null); + } } catch (Exception e) { LOG.failedToFetchPublicCert(e.getMessage(), e); - return null; } + return null; } private void generateCertificatePem(Certificate[] certificateChain, GatewayConfig gatewayConfig) { diff --git a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/KnoxSh.java b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/KnoxSh.java index ec169b51d7..a9bce5f9db 100644 --- a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/KnoxSh.java +++ b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/KnoxSh.java @@ -181,7 +181,8 @@ class KnoxBuildTrustStore extends Command { public void execute() throws Exception { String result = GATEWAY_CERT_NOT_EXPORTED; try { - final X509Certificate[] gatewayServerPublicCerts = X509CertificateUtil.fetchPublicCertsFromServer(gateway, false, out); + final X509Certificate[] gatewayServerPublicCerts = X509CertificateUtil.fetchPublicCertsFromServer( + gateway, ClientTrustStoreHelper.getClientTrustStoreFilePassword().toCharArray(), false, out); if (gatewayServerPublicCerts != null) { final File trustStoreFile = ClientTrustStoreHelper.getClientTrustStoreFile(); final String trustStorePassword = ClientTrustStoreHelper.getClientTrustStoreFilePassword(); 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 1537f698b2..2f28b96293 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 d96db8d495..5fbcba3ab5 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 @@ -39,7 +39,8 @@ public enum ServiceType { CONCURRENT_SESSION_VERIFIER("ConcurrentSessionVerifier"), REMOTE_CONFIGURATION_MONITOR("RemoteConfigurationMonitor"), GATEWAY_STATUS_SERVICE("GatewayStatusService"), - LDAP_SERVICE("LDAPService"); + LDAP_SERVICE("LDAPService"), + 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 0000000000..3c025ccb82 --- /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 + *

+ * 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. + */ +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 attributes = new HashMap<>(); + + public FederatedIdentity(String userId, String provider, String externalSubject, String externalIssuer, + Instant createdAt, Map 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 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 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/knoxidf/federation/FederatedIdentityService.java b/gateway-spi/src/main/java/org/apache/knox/gateway/services/knoxidf/federation/FederatedIdentityService.java new file mode 100644 index 0000000000..778b589a24 --- /dev/null +++ b/gateway-spi/src/main/java/org/apache/knox/gateway/services/knoxidf/federation/FederatedIdentityService.java @@ -0,0 +1,34 @@ +/* + * 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. + */ +package org.apache.knox.gateway.services.knoxidf.federation; + +import org.apache.knox.gateway.services.Service; + +import java.util.Optional; + +public interface FederatedIdentityService extends Service { + + void addFederatedIdentity(FederatedIdentity identity); + + Optional findById(String identityId); + + Optional findByProviderAndSubject( + String provider, + String externalIssuer, + String externalSubject); +} + diff --git a/gateway-spi/src/main/java/org/apache/knox/gateway/services/knoxidf/federation/FederatedIdentityServiceException.java b/gateway-spi/src/main/java/org/apache/knox/gateway/services/knoxidf/federation/FederatedIdentityServiceException.java new file mode 100644 index 0000000000..30cbce7b24 --- /dev/null +++ b/gateway-spi/src/main/java/org/apache/knox/gateway/services/knoxidf/federation/FederatedIdentityServiceException.java @@ -0,0 +1,28 @@ +/* + * 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. + */ +package org.apache.knox.gateway.services.knoxidf.federation; + +public class FederatedIdentityServiceException extends RuntimeException { + + 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 2df7cf53b3..f5719ccc9d 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 @@ -21,118 +21,128 @@ import java.net.URISyntaxException; import java.util.Date; import java.util.List; +import java.util.Map; import java.util.Set; public class JWTokenAttributes { - public static final String DEFAULT_ISSUER = "KNOXSSO"; - public static final String DEFAULT_TYPE = "JWT"; - private final String userName; - private final List audiences; - private final String algorithm; - private final long expires; - private final String signingKeystoreName; - private final String signingKeystoreAlias; - private final char[] signingKeystorePassphrase; - private final boolean managed; - private String jku; - private final String type; - private final Set groups; - private final String issuer; - private String kid; - private final String clientId; - - JWTokenAttributes(String userName, List audiences, String algorithm, long expires, String signingKeystoreName, String signingKeystoreAlias, - char[] signingKeystorePassphrase, boolean managed, String jku, String type, Set groups, String kid, String issuer) { - this(userName, audiences, algorithm, expires, signingKeystoreName, signingKeystoreAlias, signingKeystorePassphrase, managed, jku, type, groups, kid, issuer, null); - } - - JWTokenAttributes(String userName, List audiences, String algorithm, long expires, String signingKeystoreName, String signingKeystoreAlias, - char[] signingKeystorePassphrase, boolean managed, String jku, String type, Set groups, String kid, String issuer, String clientId) { - this.userName = userName; - this.audiences = audiences; - this.algorithm = algorithm; - this.expires = expires; - this.signingKeystoreName = signingKeystoreName; - this.signingKeystoreAlias = signingKeystoreAlias; - this.signingKeystorePassphrase = signingKeystorePassphrase; - this.managed = managed; - this.jku = jku; - this.type = type; - this.groups = groups; - this.kid = kid; - this.issuer = issuer; - this.clientId = clientId; - } - - public String getUserName() { - return userName; - } - - public List getAudiences() { - return audiences; - } - - public String getAlgorithm() { - return algorithm; - } - - public long getExpires() { - return expires; - } - - public Date getExpiresDate() { - return expires == -1 ? null : new Date(expires); - } - - public String getSigningKeystoreName() { - return signingKeystoreName; - } - - public String getSigningKeystoreAlias() { - return signingKeystoreAlias; - } - - public char[] getSigningKeystorePassphrase() { - return signingKeystorePassphrase; - } - - public boolean isManaged() { - return managed; - } - - public URI getJkuUri() throws URISyntaxException { - return jku != null ? new URI(jku) : null; - } - - public String getJku(){ - return jku; - } - - public void setJku(String jku) { - this.jku = jku; - } - - public String getType() { - return type; - } - - public Set getGroups() { - return groups; - } - - public void setKid(String kid) { - this.kid = kid; - } - - public String getKid() { - return kid; - } - - public String getIssuer() { - return issuer; - } - - public String getClientId() { - return clientId; - } + public static final String DEFAULT_ISSUER = "KNOXSSO"; + public static final String DEFAULT_TYPE = "JWT"; + private final String userName; + private final List audiences; + private final String algorithm; + private final long issueTime; + private final long expires; + private final String signingKeystoreName; + private final String signingKeystoreAlias; + private final char[] signingKeystorePassphrase; + private final boolean managed; + private String jku; + private final String type; + private final Set groups; + private final String issuer; + private String kid; + private final String clientId; + private final Map customAttributes; + + JWTokenAttributes(String userName, List audiences, String algorithm, long issueTime, long expires, String signingKeystoreName, String signingKeystoreAlias, + char[] signingKeystorePassphrase, boolean managed, String jku, String type, Set groups, String kid, String issuer, String clientId, + Map customAttributes) { + this.userName = userName; + this.audiences = audiences; + this.algorithm = algorithm; + this.issueTime = issueTime; + this.expires = expires; + this.signingKeystoreName = signingKeystoreName; + this.signingKeystoreAlias = signingKeystoreAlias; + this.signingKeystorePassphrase = signingKeystorePassphrase; + this.managed = managed; + this.jku = jku; + this.type = type; + this.groups = groups; + this.kid = kid; + this.issuer = issuer; + this.clientId = clientId; + this.customAttributes = customAttributes; + } + + public String getUserName() { + return userName; + } + + public List getAudiences() { + return audiences; + } + + public String getAlgorithm() { + return algorithm; + } + + public long getIssueTime() { + return issueTime; + } + + public long getExpires() { + return expires; + } + + public Date getExpiresDate() { + return expires == -1 ? null : new Date(expires); + } + + public String getSigningKeystoreName() { + return signingKeystoreName; + } + + public String getSigningKeystoreAlias() { + return signingKeystoreAlias; + } + + public char[] getSigningKeystorePassphrase() { + return signingKeystorePassphrase; + } + + public boolean isManaged() { + return managed; + } + + public URI getJkuUri() throws URISyntaxException { + return jku != null ? new URI(jku) : null; + } + + public String getJku() { + return jku; + } + + public void setJku(String jku) { + this.jku = jku; + } + + public String getType() { + return type; + } + + public Set getGroups() { + return groups; + } + + public void setKid(String kid) { + this.kid = kid; + } + + public String getKid() { + return kid; + } + + public String getIssuer() { + return issuer; + } + + + public String getClientId() { + return clientId; + } + + public Map 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 91f4acddf7..af60ae8901 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 @@ -20,6 +20,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Map; import java.util.Set; public class JWTokenAttributesBuilder { @@ -27,6 +28,7 @@ public class JWTokenAttributesBuilder { private String userName; private List audiences; private String algorithm; + private long issueTime; private long expires; private String signingKeystoreName; private String signingKeystoreAlias; @@ -38,6 +40,7 @@ public class JWTokenAttributesBuilder { private String kid; private String issuer = JWTokenAttributes.DEFAULT_ISSUER; private String clientId; + private Map customAttributes; public JWTokenAttributesBuilder setUserName(String userName) { this.userName = userName; @@ -58,6 +61,11 @@ public JWTokenAttributesBuilder setAlgorithm(String algorithm) { return this; } + public JWTokenAttributesBuilder setIssueTime(long issueTime) { + this.issueTime = issueTime; + return this; + } + public JWTokenAttributesBuilder setExpires(long expires) { this.expires = expires; return this; @@ -113,8 +121,13 @@ public JWTokenAttributesBuilder setClientId(String clientId) { return this; } + public JWTokenAttributesBuilder setCustomAttributes(Map 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); + return new JWTokenAttributes(userName, (audiences == null ? new ArrayList<>() : audiences), algorithm, issueTime, expires, signingKeystoreName, signingKeystoreAlias, + signingKeystorePassphrase, managed, jku, type, groups, kid, issuer, clientId, 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 3bc7fe2cda..8df35babe5 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 @@ private void saveMetadata(String key, String value) { } public TokenMetadata(Map metadataMap) { - this.metadataMap.clear(); this.metadataMap.putAll(metadataMap); } @@ -151,12 +150,17 @@ public void markKnoxSsoCookie() { @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 17e82e0af5..4d0080fd57 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 @@ 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 e9620a4258..73d84f96b9 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 @@ -30,7 +30,9 @@ import javax.servlet.FilterConfig; import javax.servlet.ServletContext; import java.security.interfaces.RSAPublicKey; +import java.text.ParseException; import java.util.LinkedHashMap; +import java.util.UUID; public class TokenUtils { public static final String ATTR_CURRENT_KNOXSSO_COOKIE_TOKEN_ID = "currentKnoxSsoCookieTokenId"; @@ -49,6 +51,21 @@ public static String getTokenId(final JWT token) { 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 e416e63aaa..d4935c6e58 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.JWSAlgorithm; import com.nimbusds.jose.JWSSigner; import com.nimbusds.jose.JWSVerifier; +import com.nimbusds.jwt.JWTClaimsSet; public interface JWT { @@ -61,6 +62,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 22d8dc16ff..a0c01cd27c 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 @@ import java.net.URISyntaxException; import java.text.ParseException; +import java.time.Instant; import java.util.Date; import java.util.UUID; @@ -81,6 +82,7 @@ public JWTToken(JWTokenAttributes jwtAttributes) { } JWTClaimsSet claims; JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder() + .issueTime(Date.from(Instant.ofEpochMilli(jwtAttributes.getIssueTime()))) .issuer(jwtAttributes.getIssuer()) .subject(jwtAttributes.getUserName()) .audience(jwtAttributes.getAudiences()); @@ -104,6 +106,11 @@ public JWTToken(JWTokenAttributes jwtAttributes) { 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); @@ -138,6 +145,16 @@ public String getClaims() { 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 1eec58fe97..88d9c93114 100644 --- a/gateway-util-common/pom.xml +++ b/gateway-util-common/pom.xml @@ -104,6 +104,23 @@ org.apache.httpcomponents httpclient + + + javax.ws.rs + javax.ws.rs-api + + + org.apache.commons + commons-text + + + com.github.ben-manes.caffeine + caffeine + + + com.google.guava + guava + 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 f0a8bf177b..ab49f7d636 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.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 map) { + return renderAsJsonString(map, false); + } + + public static String renderAsJsonString(Map 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/TruststorePasswordSetter.java b/gateway-util-common/src/main/java/org/apache/knox/gateway/util/TruststorePasswordSetter.java new file mode 100644 index 0000000000..9de37e6179 --- /dev/null +++ b/gateway-util-common/src/main/java/org/apache/knox/gateway/util/TruststorePasswordSetter.java @@ -0,0 +1,37 @@ +/* + * 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. + */ +package org.apache.knox.gateway.util; + +/** + * A utility class that sets and clears the javax.net.ssl.trustStorePassword system property. + */ +public class TruststorePasswordSetter implements AutoCloseable { + public static final String TRUSTSTORE_PASSWORD_SYSTEM_PROPERTY = "javax.net.ssl.trustStorePassword"; + public static final String TRUSTSTORE_PASSWORD_ALIAS = "cm.discovery.trustStorePassword"; + + public TruststorePasswordSetter(char[] password) { + if (password != null && password.length > 0) { + System.setProperty(TRUSTSTORE_PASSWORD_SYSTEM_PROPERTY, new String(password)); + } + } + + @Override + public void close() { + System.clearProperty(TRUSTSTORE_PASSWORD_SYSTEM_PROPERTY); + } +} diff --git a/gateway-util-common/src/main/java/org/apache/knox/gateway/util/X509CertificateUtil.java b/gateway-util-common/src/main/java/org/apache/knox/gateway/util/X509CertificateUtil.java index 93cfb0d3ae..8d0574d209 100644 --- a/gateway-util-common/src/main/java/org/apache/knox/gateway/util/X509CertificateUtil.java +++ b/gateway-util-common/src/main/java/org/apache/knox/gateway/util/X509CertificateUtil.java @@ -232,11 +232,14 @@ public static boolean isSelfSignedCertificate(Certificate certificate) { } } - public static X509Certificate[] fetchPublicCertsFromServer(String serverUrl, boolean forceReturnCert, PrintStream out) throws Exception { - final TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); - trustManagerFactory.init((KeyStore) null); - final X509TrustManager defaultTrustManager = (X509TrustManager) trustManagerFactory.getTrustManagers()[0]; - final CertificateChainAwareTrustManager trustManagerWithCertificateChain = new CertificateChainAwareTrustManager(defaultTrustManager); + public static X509Certificate[] fetchPublicCertsFromServer(String serverUrl, char[] trustStorePassword, boolean forceReturnCert, PrintStream out) throws Exception { + CertificateChainAwareTrustManager trustManagerWithCertificateChain; + try (TruststorePasswordSetter ignored = new TruststorePasswordSetter(trustStorePassword)) { + final TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + trustManagerFactory.init((KeyStore) null); + final X509TrustManager defaultTrustManager = (X509TrustManager) trustManagerFactory.getTrustManagers()[0]; + trustManagerWithCertificateChain = new CertificateChainAwareTrustManager(defaultTrustManager); + } final SSLContext sslContext = SSLContext.getInstance("TLS"); sslContext.init(null, new TrustManager[] { trustManagerWithCertificateChain }, null); 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 0000000000..9042b2ea97 --- /dev/null +++ b/gateway-util-common/src/main/java/org/apache/knox/gateway/util/knoxidf/AuthorizeRequestMetadata.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 + *

+ * 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. + */ +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 requestedScopes; + private final String state; + private final String nonce; + + public AuthorizeRequestMetadata(String clientId, String subject, String responseType, String redirectUri, Set requestedScopes, String state, String nonce) { + this.clientId = clientId; + this.subject = subject; + this.responseType = responseType; + this.redirectUri = redirectUri; + this.requestedScopes = requestedScopes; + this.state = state; + this.nonce = nonce; + } + + 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 Set getRequestedScopes() { + return requestedScopes; + } + + public String getJoinedRequestedScopes() { + return String.join(" ", requestedScopes); + } + +} diff --git a/gateway-util-common/src/main/java/org/apache/knox/gateway/util/knoxidf/AuthorizeRequestMetadataStore.java b/gateway-util-common/src/main/java/org/apache/knox/gateway/util/knoxidf/AuthorizeRequestMetadataStore.java new file mode 100644 index 0000000000..80f3d7110f --- /dev/null +++ b/gateway-util-common/src/main/java/org/apache/knox/gateway/util/knoxidf/AuthorizeRequestMetadataStore.java @@ -0,0 +1,33 @@ +/* + * 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. + */ +package org.apache.knox.gateway.util.knoxidf; + +public class AuthorizeRequestMetadataStore extends KnoxIDFArtifactStore{ + + 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 0000000000..7ad6078433 --- /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 + *

+ * 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. + */ +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 0000000000..f1efc3d75b --- /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 + *

+ * 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. + */ +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 createFederatedOpConfiguration(final ServletContext servletContext) { + final String names = servletContext.getInitParameter(KnoxIDFConstants.FEDERATED_OP_CONFIG_NAMES); + if (names == null || names.isEmpty()) { + return Collections.emptyMap(); + } + + final Map 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-util-common/src/main/java/org/apache/knox/gateway/util/knoxidf/FederatedOpConfigurationStore.java b/gateway-util-common/src/main/java/org/apache/knox/gateway/util/knoxidf/FederatedOpConfigurationStore.java new file mode 100644 index 0000000000..80bbb1a04f --- /dev/null +++ b/gateway-util-common/src/main/java/org/apache/knox/gateway/util/knoxidf/FederatedOpConfigurationStore.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 + *

+ * 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. + */ +package org.apache.knox.gateway.util.knoxidf; + +import java.util.Set; + +public class FederatedOpConfigurationStore extends KnoxIDFArtifactStore> { + + 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-util-common/src/main/java/org/apache/knox/gateway/util/knoxidf/KnoxIDFArtifactStore.java b/gateway-util-common/src/main/java/org/apache/knox/gateway/util/knoxidf/KnoxIDFArtifactStore.java new file mode 100644 index 0000000000..dbeba8141d --- /dev/null +++ b/gateway-util-common/src/main/java/org/apache/knox/gateway/util/knoxidf/KnoxIDFArtifactStore.java @@ -0,0 +1,39 @@ +/* + * 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. + */ +package org.apache.knox.gateway.util.knoxidf; + +import com.github.benmanes.caffeine.cache.Cache; +import com.github.benmanes.caffeine.cache.Caffeine; + +import java.util.concurrent.TimeUnit; + +public abstract class KnoxIDFArtifactStore { + + private final Cache 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 0000000000..6526a54e3d --- /dev/null +++ b/gateway-util-common/src/main/java/org/apache/knox/gateway/util/knoxidf/KnoxIDFConstants.java @@ -0,0 +1,49 @@ +/* + * 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. + */ +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 RESPONSE_TYPE = "response_type"; + Set ALLOWED_RESPONSE_TYPES = Sets.newHashSet("code", "id_token", "code id_token"); + String SCOPE = "scope"; + String OFFLINE_ACCESS_SCOPE = "offline_access"; + Set 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 TOKEN_ID_ATTRIBUTE = "X-Token-Id"; + String SCOPE_ATTRIBUTE = "X-Token-Scope"; + + 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"; +} 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 0000000000..432bf7a195 --- /dev/null +++ b/gateway-util-common/src/main/java/org/apache/knox/gateway/util/knoxidf/KnoxIDFUtils.java @@ -0,0 +1,105 @@ +/* + * 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. + */ +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 splitFederatedToken(String token, boolean idToken) { + final String prefix = idToken ? KnoxIDFConstants.FEDERATED_ID_TOKEN_PREFIX : KnoxIDFConstants.FEDERATED_ACCESS_TOKEN_PREFIX; + final Map 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 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 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 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 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); + return new AuthorizeRequestMetadata(clientId, null, responseType, redirectUri, requestedScopes, state, nonce); + } + + 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/gateway-util-common/src/test/java/org/apache/knox/gateway/util/TruststorePasswordSetterTest.java b/gateway-util-common/src/test/java/org/apache/knox/gateway/util/TruststorePasswordSetterTest.java new file mode 100644 index 0000000000..3a43b6ec93 --- /dev/null +++ b/gateway-util-common/src/test/java/org/apache/knox/gateway/util/TruststorePasswordSetterTest.java @@ -0,0 +1,71 @@ +/* + * 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. + */ +package org.apache.knox.gateway.util; + +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +public class TruststorePasswordSetterTest { + private String originalPassword; + + @Before + public void setUp() { + originalPassword = System.getProperty(TruststorePasswordSetter.TRUSTSTORE_PASSWORD_SYSTEM_PROPERTY); + System.clearProperty(TruststorePasswordSetter.TRUSTSTORE_PASSWORD_SYSTEM_PROPERTY); + } + + @After + public void tearDown() { + if (originalPassword != null) { + System.setProperty(TruststorePasswordSetter.TRUSTSTORE_PASSWORD_SYSTEM_PROPERTY, originalPassword); + } else { + System.clearProperty(TruststorePasswordSetter.TRUSTSTORE_PASSWORD_SYSTEM_PROPERTY); + } + } + + @Test + public void testSetPassword() { + final String password = "test-password"; + try (TruststorePasswordSetter setter = new TruststorePasswordSetter(password.toCharArray())) { + Assert.assertEquals("The system property should be set to the provided password.", + password, System.getProperty(TruststorePasswordSetter.TRUSTSTORE_PASSWORD_SYSTEM_PROPERTY)); + } + Assert.assertNull("The system property should be cleared after closing the setter.", + System.getProperty(TruststorePasswordSetter.TRUSTSTORE_PASSWORD_SYSTEM_PROPERTY)); + } + + @Test + public void testNullPassword() { + try (TruststorePasswordSetter setter = new TruststorePasswordSetter(null)) { + Assert.assertNull("The system property should not be set if the password is null.", + System.getProperty(TruststorePasswordSetter.TRUSTSTORE_PASSWORD_SYSTEM_PROPERTY)); + } + Assert.assertNull(System.getProperty(TruststorePasswordSetter.TRUSTSTORE_PASSWORD_SYSTEM_PROPERTY)); + } + + @Test + public void testEmptyPassword() { + try (TruststorePasswordSetter setter = new TruststorePasswordSetter(new char[0])) { + Assert.assertNull("The system property should not be set if the password array is empty.", + System.getProperty(TruststorePasswordSetter.TRUSTSTORE_PASSWORD_SYSTEM_PROPERTY)); + } + Assert.assertNull(System.getProperty(TruststorePasswordSetter.TRUSTSTORE_PASSWORD_SYSTEM_PROPERTY)); + } +} diff --git a/gateway-util-common/src/test/java/org/apache/knox/gateway/util/X509CertificateUtilTest.java b/gateway-util-common/src/test/java/org/apache/knox/gateway/util/X509CertificateUtilTest.java new file mode 100644 index 0000000000..06365f575e --- /dev/null +++ b/gateway-util-common/src/test/java/org/apache/knox/gateway/util/X509CertificateUtilTest.java @@ -0,0 +1,94 @@ +/* + * 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. + */ +package org.apache.knox.gateway.util; + +import org.junit.Assert; +import org.junit.Test; + +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLServerSocket; +import javax.net.ssl.SSLServerSocketFactory; +import javax.net.ssl.SSLSocket; +import java.io.IOException; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.KeyStore; +import java.security.SecureRandom; +import java.security.cert.X509Certificate; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; + +public class X509CertificateUtilTest { + + @Test + public void testFetchPublicCertsFromServer() throws Exception { + // 1. Generate a self-signed certificate + KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA"); + keyPairGenerator.initialize(2048); + KeyPair keyPair = keyPairGenerator.generateKeyPair(); + X509Certificate cert = X509CertificateUtil.generateCertificate("CN=localhost", keyPair, 30, "SHA256withRSA"); + + // 2. Set up a KeyStore with the certificate + KeyStore keyStore = KeyStore.getInstance("JKS"); + keyStore.load(null, null); + keyStore.setKeyEntry("alias", keyPair.getPrivate(), "password".toCharArray(), new java.security.cert.Certificate[]{cert}); + + KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); + kmf.init(keyStore, "password".toCharArray()); + + SSLContext serverSslContext = SSLContext.getInstance("TLS"); + serverSslContext.init(kmf.getKeyManagers(), null, new SecureRandom()); + + SSLServerSocketFactory ssf = serverSslContext.getServerSocketFactory(); + try (SSLServerSocket serverSocket = (SSLServerSocket) ssf.createServerSocket(0)) { + int port = serverSocket.getLocalPort(); + + ExecutorService executor = Executors.newSingleThreadExecutor(); + Future serverFuture = executor.submit(new Callable() { + @Override + public Void call() throws Exception { + try (SSLSocket clientSocket = (SSLSocket) serverSocket.accept()) { + clientSocket.startHandshake(); + } catch (IOException e) { + // Expected if client closes connection early + } + return null; + } + }); + + // 3. Fetch the certificate from the server + try { + String serverUrl = "https://localhost:" + port; + X509Certificate[] certs = X509CertificateUtil.fetchPublicCertsFromServer(serverUrl, null, true, null); + + // 4. Verify + Assert.assertNotNull(certs); + Assert.assertTrue(certs.length > 0); + Assert.assertEquals(cert.getSubjectX500Principal(), certs[0].getSubjectX500Principal()); + Assert.assertEquals(cert.getPublicKey(), certs[0].getPublicKey()); + } finally { + serverFuture.get(5, TimeUnit.SECONDS); + executor.shutdown(); + } + } + } +} diff --git a/pom.xml b/pom.xml index a2626889a6..6bab9a87d3 100644 --- a/pom.xml +++ b/pom.xml @@ -153,6 +153,7 @@ knox-token-management-ui knox-webshell-ui knox-token-generation-ui + gateway-service-knoxidf @@ -291,6 +292,7 @@ 1.2.5 1.15.1 2.4.0-b180830.0438 + 5.2.0 2.4.1 6.4.0 4.0.4 @@ -1411,6 +1413,11 @@ knox-token-generation-ui ${project.version} + + org.apache.knox + gateway-service-knoxidf + ${project.version} + org.glassfish.jersey.containers jersey-container-servlet-core @@ -1426,6 +1433,11 @@ jersey-server ${jersey.version} + + org.glassfish.jersey.core + jersey-common + ${jersey.version} + org.glassfish.jersey.inject @@ -2021,6 +2033,11 @@ woodstox-core ${woodstox-core.version} + + com.fasterxml.uuid + java-uuid-generator + ${uuid.generator.version} + cglib