This is an automated email from the ASF dual-hosted git repository. smolnar82 pushed a commit to branch knox_idf in repository https://gitbox.apache.org/repos/asf/knox.git
commit 9a1d04a2b54ba17644dad2001a0b59e32ea75bb2 Author: Sandor Molnar <[email protected]> AuthorDate: Tue Aug 11 19:09:21 2026 +0200 KNOX-3414: add opt-in Keycloak federation E2E test and activate federated-identity persistence Adds an opt-in end-to-end test that stands up a real Keycloak as the external OpenID Provider and drives the full broker flow through Knox (register client, /authorize -> SSOCookie redirect -> Keycloak login -> callback -> token exchange). It is layered in via docker-compose.knoxidf-federation.yml and is NOT part of the default test run (the base compose ignores test_knoxidf_federation.py). Surfacing that path exposed a production bug baked into the OIDC-provider squash: federated-identity persistence never activated. The service factories decided whether to use the JDBC-backed store by calling TopologyService.getTopologies() at gateway-service-init time, but topologies are not loaded yet at that point, so the no-op EmptyFederatedIdentityService was chosen in every deployment and the token exchange failed with "Federated identity not found". Fixes: - Move isKnoxIdfEnabledInAnyTopology into AbstractServiceFactory and add a topology-directory disk-scan fallback for when getTopologies() is still empty at init time; match both KNOXIDF and KNOXIDF_ADMIN roles. TrustedOidcIssuerServiceFactory now delegates to the shared helper. - Add a self-provisioning DerbyDBFederatedIdentityService (mirrors DerbyDBTokenStateService) and 3-way backend auto-selection: explicit impl wins; otherwise an operator-configured external DB -> JDBC, else the embedded Derby default so federation works out of the box with no extra infrastructure. - SQL DDL: the create-table runner executes a single statement per file, so fold the federated-identity UNIQUE constraint inline and drop trailing semicolons. - Validate federated OP id_tokens with a JWS type verifier that accepts typ=JWT and an absent typ (Keycloak and most OPs set typ=JWT; the shared token authority rejects any typ'd token when no verifier is supplied). - getPrefix() now returns "knoxidf." (trailing dot) so knoxidf.knox.token.* topology params map onto the base KNOXTOKEN params; add the same override to AuthorizeResource so its callback-minted tokens honor the per-user limit/ttl configured on the topology. Covered by FederatedIdentityServiceFactoryTest, DerbyDBFederatedIdentityServiceTest, and the 3 federation E2E tests (all green); default KnoxIDF E2E and the knoxidf/ knoxtoken JUnit suites remain green. Co-Authored-By: Claude Opus 4.8 <[email protected]> --- .github/workflows/build/Dockerfile | 2 + .../build/conf/topologies/knoxidf-sso.xml | 120 +++++++++++ .../workflows/build/conf/topologies/knoxsso.xml | 105 +++++++++ .../compose/docker-compose.knoxidf-federation.yml | 73 +++++++ .github/workflows/compose/docker-compose.yml | 2 +- .github/workflows/compose/keycloak/realm.json | 49 +++++ .github/workflows/tests/test_knoxidf_federation.py | 234 +++++++++++++++++++++ .gitignore | 1 + .../org/apache/knox/gateway/GatewayMessages.java | 6 + .../services/factory/AbstractServiceFactory.java | 79 +++++++ .../factory/FederatedIdentityServiceFactory.java | 83 +++++--- .../factory/TrustedOidcIssuerServiceFactory.java | 26 +-- .../DerbyDBFederatedIdentityService.java | 101 +++++++++ ...eateKnoxIDFFederatedIdentityAttributesTable.sql | 2 +- ...noxIDFFederatedIdentityAttributesTableDerby.sql | 2 +- ...oxIDFFederatedIdentityAttributesTableOracle.sql | 2 +- .../createKnoxIDFFederatedIdentityTable.sql | 7 +- .../createKnoxIDFFederatedIdentityTableDerby.sql | 7 +- .../createKnoxIDFFederatedIdentityTableOracle.sql | 7 +- .../FederatedIdentityServiceFactoryTest.java | 211 +++++++++++++++++++ .../DerbyDBFederatedIdentityServiceTest.java | 108 ++++++++++ .../gateway/service/knoxidf/AuthorizeResource.java | 17 +- .../gateway/service/knoxidf/TokenResource.java | 2 +- 23 files changed, 1175 insertions(+), 71 deletions(-) diff --git a/.github/workflows/build/Dockerfile b/.github/workflows/build/Dockerfile index 5c0407661..3934c5d11 100644 --- a/.github/workflows/build/Dockerfile +++ b/.github/workflows/build/Dockerfile @@ -46,6 +46,8 @@ ADD .github/workflows/build/conf/topologies/remoteauth.xml /knox-runtime/conf/to ADD .github/workflows/build/conf/topologies/k8sauth.xml /knox-runtime/conf/topologies/k8sauth.xml ADD .github/workflows/build/conf/topologies/knoxidf-ldap.xml /knox-runtime/conf/topologies/knoxidf-ldap.xml ADD .github/workflows/build/conf/topologies/knoxidf-token.xml /knox-runtime/conf/topologies/knoxidf-token.xml +ADD .github/workflows/build/conf/topologies/knoxsso.xml /knox-runtime/conf/topologies/knoxsso.xml +ADD .github/workflows/build/conf/topologies/knoxidf-sso.xml /knox-runtime/conf/topologies/knoxidf-sso.xml RUN chown -R gateway /knox-runtime/ diff --git a/.github/workflows/build/conf/topologies/knoxidf-sso.xml b/.github/workflows/build/conf/topologies/knoxidf-sso.xml new file mode 100644 index 000000000..596db8284 --- /dev/null +++ b/.github/workflows/build/conf/topologies/knoxidf-sso.xml @@ -0,0 +1,120 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + 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. +--> +<!-- + CI-only KnoxIDF topology exercised by the opt-in federation E2E test + (test_knoxidf_federation.py). Unlike knoxidf-ldap.xml (which fronts /authorize with + LDAP Basic auth), this topology fronts /authorize with an SSOCookieProvider. An + unauthenticated /authorize is redirected to the knoxsso login front-end; because a + federated OP (Keycloak) is configured below, the SSOCookieFederationFilter records the + request and offers the OP as a login option, brokering the user out to Keycloak and back + through /authorize/callback. + + The federated.op.* settings are KNOXIDF service params so they are exposed as + ServletContext init-params, which is where both AuthorizeResource and the + SSOCookieFederationFilter read them (same webapp, shared ServletContext). +--> +<topology> + <gateway> + <provider> + <role>federation</role> + <name>SSOCookieProvider</name> + <enabled>true</enabled> + <param> + <name>sso.authentication.provider.url</name> + <value>https://knox:8443/gateway/knoxsso/api/v1/websso</value> + </param> + <param> + <name>sso.unauthenticated.path.list</name> + <value>/knoxidf/api/v1/authorize/callback;/knoxidf/api/v1/jwks;/knoxidf/api/v1/.well-known/openid-configuration;/knoxidf/api/v1/client/register</value> + </param> + </provider> + <provider> + <role>identity-assertion</role> + <name>Default</name> + <enabled>true</enabled> + </provider> + </gateway> + + <service> + <role>KNOXIDF</role> + <param> + <name>knoxidf.knox.token.ttl</name> + <value>60000</value> + </param> + <param> + <name>knoxidf.knox.token.limit.per.user</name> + <value>-1</value> + </param> + <param> + <name>knoxidf.client.registration.anonymous.allowed</name> + <value>true</value> + </param> + <param> + <name>knoxidf.auto.consent.enabled</name> + <value>true</value> + </param> + <param> + <name>token.exchange.topology.name</name> + <value>knoxidf-token</value> + </param> + <param> + <name>federated.op.names</name> + <value>keycloak</value> + </param> + <param> + <name>federated.op.keycloak.enabled</name> + <value>true</value> + </param> + <param> + <name>federated.op.keycloak.clientId</name> + <value>knox-client</value> + </param> + <param> + <name>federated.op.keycloak.clientSecret</name> + <value>knox-client-secret</value> + </param> + <param> + <name>federated.op.keycloak.authorize.endpoint</name> + <value>http://keycloak:8080/realms/knox/protocol/openid-connect/auth</value> + </param> + <param> + <name>federated.op.keycloak.authorize.callback</name> + <value>https://knox:8443/gateway/knoxidf-sso/knoxidf/api/v1/authorize/callback</value> + </param> + <param> + <name>federated.op.keycloak.token.endpoint</name> + <value>http://keycloak:8080/realms/knox/protocol/openid-connect/token</value> + </param> + <param> + <name>federated.op.keycloak.jwks.endpoint</name> + <value>http://keycloak:8080/realms/knox/protocol/openid-connect/certs</value> + </param> + <param> + <name>federated.op.keycloak.issuer</name> + <value>http://keycloak:8080/realms/knox</value> + </param> + <param> + <name>federated.op.keycloak.userinfo.endpoint</name> + <value>http://keycloak:8080/realms/knox/protocol/openid-connect/userinfo</value> + </param> + <param> + <name>federated.op.keycloak.signature.algorithm</name> + <value>RS256</value> + </param> + </service> +</topology> diff --git a/.github/workflows/build/conf/topologies/knoxsso.xml b/.github/workflows/build/conf/topologies/knoxsso.xml new file mode 100644 index 000000000..39c9cea6f --- /dev/null +++ b/.github/workflows/build/conf/topologies/knoxsso.xml @@ -0,0 +1,105 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + 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. +--> +<!-- + CI-only KnoxSSO topology used by the opt-in KnoxIDF federation E2E test + (test_knoxidf_federation.py). It is the authentication front-end that the + knoxidf-sso topology's SSOCookieProvider redirects unauthenticated /authorize + requests to. The /api/v1/websso/federated/op endpoint (WebSSOResource.federatedOpLogin) + must be reachable anonymously so the browser can be bounced on to the external OP + (Keycloak) without first logging in locally. + + This overwrites the knoxsso.xml shipped in the release tarball (the ADD in the + Dockerfile wins) so the LDAP endpoint matches the CI LDAP (ldaps://localhost:33390) + and the redirect whitelist accepts the dot-less "knox" compose hostname. +--> +<topology> + <gateway> + <provider> + <role>authentication</role> + <name>ShiroProvider</name> + <enabled>true</enabled> + <param> + <name>sessionTimeout</name> + <value>30</value> + </param> + <param> + <name>redirectToUrl</name> + <value>/${GATEWAY_PATH}/knoxsso/knoxauth/login.html</value> + </param> + <param> + <name>restrictedCookies</name> + <value>rememberme,WWW-Authenticate</value> + </param> + <param> + <name>main.ldapRealm</name> + <value>org.apache.knox.gateway.shirorealm.KnoxLdapRealm</value> + </param> + <param> + <name>main.ldapRealm.userDnTemplate</name> + <value>uid={0},ou=people,dc=hadoop,dc=apache,dc=org</value> + </param> + <param> + <name>main.ldapRealm.contextFactory.url</name> + <value>ldaps://localhost:33390</value> + </param> + <param> + <name>main.ldapRealm.authenticationCachingEnabled</name> + <value>false</value> + </param> + <param> + <name>main.ldapRealm.contextFactory.authenticationMechanism</name> + <value>simple</value> + </param> + <param> + <name>urls./api/v1/websso/federated/op</name> + <value>anon</value> + </param> + <param> + <name>urls./**</name> + <value>authcBasic</value> + </param> + </provider> + + <provider> + <role>identity-assertion</role> + <name>Default</name> + <enabled>true</enabled> + </provider> + </gateway> + + <application> + <name>knoxauth</name> + </application> + + <service> + <role>KNOXSSO</role> + <param> + <name>knoxsso.token.ttl</name> + <value>86400000</value> + </param> + <param> + <name>knox.token.exp.server-managed</name> + <value>false</value> + </param> + <param> + <name>knoxsso.redirect.whitelist.regex</name> + <value>^https?://knox:[0-9]+/gateway/.*$</value> + </param> + </service> + +</topology> diff --git a/.github/workflows/compose/docker-compose.knoxidf-federation.yml b/.github/workflows/compose/docker-compose.knoxidf-federation.yml new file mode 100644 index 000000000..0b2b4676d --- /dev/null +++ b/.github/workflows/compose/docker-compose.knoxidf-federation.yml @@ -0,0 +1,73 @@ +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with this +# work for additional information regarding copyright ownership. The ASF +# licenses this file to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# <p> +# http://www.apache.org/licenses/LICENSE-2.0 +# <p> +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Opt-in override that layers a real Keycloak (external OpenID Provider) onto the base +# compose stack and runs ONLY the KnoxIDF federation E2E test. It is intentionally not part +# of the default test run (which ignores test_knoxidf_federation.py) because it pulls the +# Keycloak image and adds minutes of startup. +# +# Usage: +# cd .github/workflows/compose +# IMAGE_TAG=knoxidf docker compose \ +# -f docker-compose.yml -f docker-compose.knoxidf-federation.yml \ +# up --build --abort-on-container-exit --exit-code-from tests + +services: + keycloak: + image: quay.io/keycloak/keycloak:26.2.4 + command: + - start-dev + - --import-realm + environment: + - KC_BOOTSTRAP_ADMIN_USERNAME=admin + - KC_BOOTSTRAP_ADMIN_PASSWORD=admin + - KC_HOSTNAME_STRICT=false + - KC_HTTP_ENABLED=true + - KC_HEALTH_ENABLED=true + volumes: + - ./keycloak/realm.json:/opt/keycloak/data/import/realm.json:ro + healthcheck: + # Keycloak 25+ serves health on the management port (9000). The image has no curl, so + # use bash's /dev/tcp per Keycloak's own recommendation. + test: + - CMD-SHELL + - > + exec 3<>/dev/tcp/localhost/9000; + echo -e 'GET /health/ready HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n' >&3; + cat <&3 | grep -q 'UP' + interval: 10s + timeout: 5s + retries: 30 + start_period: 60s + + knox: + depends_on: + keycloak: + condition: service_healthy + + tests: + environment: + - KNOX_GATEWAY_URL=https://knox:8443/ + - KEYCLOAK_URL=http://keycloak:8080 + command: > + bash -c "pip install -r requirements.txt + && echo 'Waiting for knox...' + && sleep 30 + && pytest test_knoxidf_federation.py --junitxml=test-results-federation.xml" + depends_on: + knox: + condition: service_started + keycloak: + condition: service_healthy diff --git a/.github/workflows/compose/docker-compose.yml b/.github/workflows/compose/docker-compose.yml index 727bca263..3757f8837 100644 --- a/.github/workflows/compose/docker-compose.yml +++ b/.github/workflows/compose/docker-compose.yml @@ -114,7 +114,7 @@ services: && pylint *.py && echo 'Waiting for knox...' && sleep 30 - && pytest --ignore=test_single_eku_mtls.py --ignore=test_single_eku_no_mtls.py --junitxml=test-results.xml" + && pytest --ignore=test_single_eku_mtls.py --ignore=test_single_eku_no_mtls.py --ignore=test_knoxidf_federation.py --junitxml=test-results.xml" depends_on: - knox diff --git a/.github/workflows/compose/keycloak/realm.json b/.github/workflows/compose/keycloak/realm.json new file mode 100644 index 000000000..bf6f57b87 --- /dev/null +++ b/.github/workflows/compose/keycloak/realm.json @@ -0,0 +1,49 @@ +{ + "realm": "knox", + "enabled": true, + "sslRequired": "none", + "registrationAllowed": false, + "loginWithEmailAllowed": true, + "clients": [ + { + "clientId": "knox-client", + "enabled": true, + "protocol": "openid-connect", + "publicClient": false, + "secret": "knox-client-secret", + "standardFlowEnabled": true, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "consentRequired": false, + "fullScopeAllowed": true, + "redirectUris": [ + "https://knox:8443/gateway/knoxidf-sso/knoxidf/api/v1/authorize/callback" + ], + "webOrigins": [ + "+" + ], + "defaultClientScopes": [ + "profile", + "email" + ], + "optionalClientScopes": [] + } + ], + "users": [ + { + "username": "alice", + "enabled": true, + "emailVerified": true, + "email": "[email protected]", + "firstName": "Alice", + "lastName": "Example", + "credentials": [ + { + "type": "password", + "value": "alice-password", + "temporary": false + } + ] + } + ] +} diff --git a/.github/workflows/tests/test_knoxidf_federation.py b/.github/workflows/tests/test_knoxidf_federation.py new file mode 100644 index 000000000..2806e0856 --- /dev/null +++ b/.github/workflows/tests/test_knoxidf_federation.py @@ -0,0 +1,234 @@ +# 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. + +"""End-to-end test of the KnoxIDF federation path: Knox brokering login to Keycloak. + +This is opt-in and NOT part of the default test run (the base docker-compose ignores it). +It runs only under the docker-compose.knoxidf-federation.yml override, which stands up a +real Keycloak as the external OpenID Provider. See that override file for how to run it. + +Flow exercised (all hops carried on one requests.Session so cookies persist): + 1. register an OIDC client on the knoxidf-sso topology (anonymous registration), + 2. GET /authorize -> SSOCookieProvider redirects to knoxsso with a federated-OP session id, + 3. GET /api/v1/websso/federated/op -> redirect to Keycloak's authorize endpoint, + 4. submit alice's credentials to Keycloak's login form -> redirect to the Knox callback, + 5. Knox validates the OP id_token, resumes /authorize, and redirects with a Knox code, + 6. exchange the Knox code at the token endpoint for Knox access/id/refresh tokens. +""" + +import os +import unittest +import uuid +from html.parser import HTMLParser +from urllib.parse import urljoin, urlparse, parse_qs + +import requests + +from common_utils import gateway_base_url, get_token_claim, KNOX_REQUEST_TIMEOUT + +# Client-side (relying-party) details. The redirect_uri is a loopback http URL, which the +# registration redirect-URI policy permits (RFC 8252). The client state is echoed back to +# the client redirect and is distinct from the federated-OP login session id. +CLIENT_REDIRECT_URI = "http://localhost/callback" +CLIENT_STATE = "knox_fed_client_state" + +# The seeded Keycloak realm user (see compose/keycloak/realm.json). +KC_USERNAME = "alice" +KC_PASSWORD = "alice-password" +KC_EMAIL = "[email protected]" + + +class _LoginFormParser(HTMLParser): + """Scrapes the first HTML <form>: its action plus every input's name/value.""" + + def __init__(self): + super().__init__() + self.action = None + self.inputs = {} + self._in_form = False + + def handle_starttag(self, tag, attrs): + """Record the form action and any inputs inside the first form.""" + attributes = dict(attrs) + if tag == "form" and self.action is None: + self.action = attributes.get("action") + self._in_form = True + elif tag == "input" and self._in_form: + name = attributes.get("name") + if name: + self.inputs[name] = attributes.get("value", "") + + def handle_endtag(self, tag): + """Stop collecting inputs once the first form closes.""" + if tag == "form": + self._in_form = False + + +class TestKnoxIDFFederation(unittest.TestCase): + """Federation broker tests: Knox delegating authentication to Keycloak.""" + + def setUp(self): + self.base_url = gateway_base_url() + self.knoxidf_sso_url = f"{self.base_url}gateway/knoxidf-sso/" + self.knoxsso_url = f"{self.base_url}gateway/knoxsso/" + self.knoxidf_token_url = f"{self.base_url}gateway/knoxidf-token/" + self.keycloak_url = os.environ.get("KEYCLOAK_URL", "http://keycloak:8080") + + @staticmethod + def _new_session(): + """A cookie-carrying session that tolerates Knox's self-signed TLS.""" + session = requests.Session() + session.verify = False + return session + + def _register_client(self, session): + """Register a confidential client and return (client_id, client_secret).""" + url = f"{self.knoxidf_sso_url}knoxidf/api/v1/client/register" + payload = { + "redirect_uris": CLIENT_REDIRECT_URI, + "allowed_scopes": "openid,profile,email,offline_access", + } + response = session.post(url, data=payload, timeout=KNOX_REQUEST_TIMEOUT) + self.assertEqual(response.status_code, 200, response.text) + body = response.json() + return body["client_id"], body["client_secret"] + + def _start_authorize(self, session, client_id): + """Kick off /authorize; return the federated-OP login session id from the redirect.""" + url = f"{self.knoxidf_sso_url}knoxidf/api/v1/authorize" + params = { + "response_type": "code", + "client_id": client_id, + "redirect_uri": CLIENT_REDIRECT_URI, + "scope": "openid offline_access", + "state": CLIENT_STATE, + } + response = session.get(url, params=params, allow_redirects=False, + timeout=KNOX_REQUEST_TIMEOUT) + self.assertIn(response.status_code, (302, 303, 307), response.text) + location = response.headers.get("Location") + self.assertIsNotNone(location, "SSOCookieProvider did not issue a login redirect") + query = parse_qs(urlparse(location).query) + self.assertIn("federatedOpLoginSession", query, location) + self.assertIn("keycloak", query.get("federatedOpNames", [""])[0]) + return query["federatedOpLoginSession"][0] + + def _kickoff_federated_op(self, session, login_session_id): + """Select the Keycloak OP; return the Keycloak authorize URL Knox redirects to.""" + url = f"{self.knoxsso_url}api/v1/websso/federated/op" + params = {"fedOpSid": login_session_id, "fedOpName": "keycloak"} + response = session.get(url, params=params, allow_redirects=False, + timeout=KNOX_REQUEST_TIMEOUT) + self.assertIn(response.status_code, (302, 303, 307), response.text) + location = response.headers.get("Location") + self.assertIsNotNone(location) + self.assertTrue(location.startswith(self.keycloak_url), location) + return location + + def _keycloak_login(self, session, keycloak_authorize_url): + """Submit alice's credentials to Keycloak; return the Knox callback URL it redirects to.""" + page = session.get(keycloak_authorize_url, allow_redirects=True, + timeout=KNOX_REQUEST_TIMEOUT) + self.assertEqual(page.status_code, 200, "Keycloak did not render a login page") + parser = _LoginFormParser() + parser.feed(page.text) + self.assertIsNotNone(parser.action, "No login form found on the Keycloak page") + form = dict(parser.inputs) + form["username"] = KC_USERNAME + form["password"] = KC_PASSWORD + action = urljoin(page.url, parser.action) + submitted = session.post(action, data=form, allow_redirects=False, + timeout=KNOX_REQUEST_TIMEOUT) + self.assertIn(submitted.status_code, (302, 303), submitted.text) + location = submitted.headers.get("Location") + self.assertIsNotNone(location, "Keycloak did not redirect back after login") + self.assertTrue(location.startswith(self.base_url), location) + return location + + def _knox_callback(self, session, callback_url): + """Follow the OP callback into Knox; return the Knox authorization code.""" + response = session.get(callback_url, allow_redirects=False, + timeout=KNOX_REQUEST_TIMEOUT) + self.assertIn(response.status_code, (302, 303), response.text) + location = response.headers.get("Location") + self.assertIsNotNone(location) + self.assertTrue(location.startswith(CLIENT_REDIRECT_URI), location) + query = parse_qs(urlparse(location).query) + self.assertIn("code", query, location) + self.assertEqual(query.get("state", [""])[0], CLIENT_STATE) + return query["code"][0] + + def _exchange_code(self, session, client_id, client_secret, code): + """Exchange a Knox authorization code for the Knox token set.""" + url = f"{self.knoxidf_token_url}knoxidf/api/v1/token" + payload = { + "grant_type": "authorization_code", + "code": code, + "redirect_uri": CLIENT_REDIRECT_URI, + "client_id": client_id, + "client_secret": client_secret, + } + response = session.post(url, data=payload, timeout=KNOX_REQUEST_TIMEOUT) + self.assertEqual(response.status_code, 200, response.text) + return response.json() + + def _run_full_flow(self): + """Drive the whole broker flow on a fresh session; return the Knox token set.""" + session = self._new_session() + client_id, client_secret = self._register_client(session) + login_session_id = self._start_authorize(session, client_id) + keycloak_authorize_url = self._kickoff_federated_op(session, login_session_id) + callback_url = self._keycloak_login(session, keycloak_authorize_url) + code = self._knox_callback(session, callback_url) + return self._exchange_code(session, client_id, client_secret, code) + + def test_federation_returns_all_tokens(self): + """The brokered flow yields access, id, and refresh tokens with a usable token type.""" + tokens = self._run_full_flow() + self.assertIn("access_token", tokens) + self.assertIn("id_token", tokens) + self.assertIn("refresh_token", tokens) + self.assertEqual(tokens.get("token_type", "").lower(), "bearer") + self.assertGreater(int(tokens.get("expires_in", 0)), 0) + + def test_id_token_carries_federated_claims(self): + """The Knox id_token records the OP provenance and a standard email claim.""" + tokens = self._run_full_flow() + id_token = tokens["id_token"] + + self.assertEqual(get_token_claim(id_token, "federated_idp"), "KEYCLOAK") + self.assertEqual( + get_token_claim(id_token, "federated_iss"), + f"{self.keycloak_url}/realms/knox", + ) + self.assertTrue(get_token_claim(id_token, "federated_sub")) + self.assertEqual(get_token_claim(id_token, "email"), KC_EMAIL) + + # The Knox subject is a deterministic UUIDv5 derived from the OP issuer+subject. + subject = get_token_claim(id_token, "sub") + self.assertEqual(uuid.UUID(subject).version, 5) + + def test_same_keycloak_user_maps_to_stable_knox_subject(self): + """Two independent logins by the same OP user resolve to one persisted Knox subject.""" + first = self._run_full_flow() + second = self._run_full_flow() + first_sub = get_token_claim(first["id_token"], "sub") + second_sub = get_token_claim(second["id_token"], "sub") + self.assertTrue(first_sub) + self.assertEqual(first_sub, second_sub) + + +if __name__ == "__main__": + unittest.main() diff --git a/.gitignore b/.gitignore index 17236cdcd..f28ecbe32 100644 --- a/.gitignore +++ b/.gitignore @@ -43,6 +43,7 @@ velocity.log # Workflow rules .github/workflows/compose/logs/* .github/workflows/tests/test-results.xml +.github/workflows/tests/test-results-federation.xml # other IDEs and editors diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/GatewayMessages.java b/gateway-server/src/main/java/org/apache/knox/gateway/GatewayMessages.java index d4639b011..a71fbf5b3 100644 --- a/gateway-server/src/main/java/org/apache/knox/gateway/GatewayMessages.java +++ b/gateway-server/src/main/java/org/apache/knox/gateway/GatewayMessages.java @@ -738,6 +738,12 @@ public interface GatewayMessages { @Message(level = MessageLevel.ERROR, text = "Error while initializing {0}: {1}") void errorInitializingService(String implementation, String error, @StackTrace(level = MessageLevel.DEBUG) Exception e); + @Message(level = MessageLevel.DEBUG, text = "Failed to list topology directory {0} while detecting KnoxIDF: {1}") + void failedToListTopologyDirForKnoxIdfDetection(String topologyDir, String error, @StackTrace(level = MessageLevel.DEBUG) Exception e); + + @Message(level = MessageLevel.DEBUG, text = "Failed to read topology file {0} while detecting KnoxIDF: {1}") + void failedToReadTopologyFileForKnoxIdfDetection(String topologyFile, String error, @StackTrace(level = MessageLevel.DEBUG) Exception e); + @Message(level = MessageLevel.ERROR, text = "Unable to complete service discovery for cluster {0} topology = {1}.") void failedToDiscoverClusterServices(String clusterName, String topologyName, diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/services/factory/AbstractServiceFactory.java b/gateway-server/src/main/java/org/apache/knox/gateway/services/factory/AbstractServiceFactory.java index 4b60fa434..2049c72bb 100644 --- a/gateway-server/src/main/java/org/apache/knox/gateway/services/factory/AbstractServiceFactory.java +++ b/gateway-server/src/main/java/org/apache/knox/gateway/services/factory/AbstractServiceFactory.java @@ -17,9 +17,16 @@ */ package org.apache.knox.gateway.services.factory; +import java.io.IOException; import java.lang.reflect.InvocationTargetException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; import java.util.Collection; +import java.util.Locale; import java.util.Map; +import java.util.stream.Stream; import org.apache.commons.lang3.StringUtils; import org.apache.knox.gateway.GatewayMessages; @@ -33,6 +40,8 @@ import org.apache.knox.gateway.services.ServiceType; import org.apache.knox.gateway.services.security.AliasService; import org.apache.knox.gateway.services.security.KeystoreService; import org.apache.knox.gateway.services.security.MasterService; +import org.apache.knox.gateway.services.topology.TopologyService; +import org.apache.knox.gateway.topology.Topology; public abstract class AbstractServiceFactory implements ServiceFactory { @@ -40,6 +49,10 @@ public abstract class AbstractServiceFactory implements ServiceFactory { private static final String IMPLEMENTATION_PARAM_NAME = "impl"; private static final String EMPTY_DEFAULT_IMPLEMENTATION = ""; + /** Topology service roles that enable the KnoxIDF-backed service implementations. */ + private static final String KNOXIDF_ROLE = "KNOXIDF"; + private static final String KNOXIDF_ADMIN_ROLE = "KNOXIDF_ADMIN"; + @Override public Service create(GatewayServices gatewayServices, ServiceType serviceType, GatewayConfig gatewayConfig, Map<String, String> options) throws ServiceLifecycleException { return create(gatewayServices, serviceType, gatewayConfig, options, getImplementation(gatewayConfig)); @@ -105,6 +118,72 @@ public abstract class AbstractServiceFactory implements ServiceFactory { LOG.usingServiceImplementation(isEmptyDefaultImplementation(implementation) ? "default" : implementation, serviceType.getServiceTypeName()); } + /** + * Returns {@code true} if any topology enables KnoxIDF (a service with role {@code KNOXIDF} or + * {@code KNOXIDF_ADMIN}). + * <p> + * Service factories run during {@code DefaultGatewayServices.init}, before the topology monitor + * has loaded any topologies, so {@link TopologyService#getTopologies()} is typically empty at + * this point. To detect KnoxIDF anyway we fall back to scanning the on-disk topology directory + * for the enabling role. The in-memory check is kept first so a caller that runs after topologies + * are loaded still works. Known limitation: descriptor-generated {@code .topology} files that are + * not yet materialised on disk at init time are not seen (still strictly better than relying on + * the empty in-memory map alone). + */ + protected boolean isKnoxIdfEnabledInAnyTopology(GatewayServices gatewayServices, GatewayConfig gatewayConfig) { + final TopologyService topologyService = gatewayServices.getService(ServiceType.TOPOLOGY_SERVICE); + if (topologyService != null) { + for (Topology topology : topologyService.getTopologies()) { + if (topology.getServices().stream().anyMatch(service -> isKnoxIdfRole(service.getRole()))) { + return true; + } + } + } + return isKnoxIdfEnabledOnDisk(gatewayConfig); + } + + private static boolean isKnoxIdfRole(String role) { + return KNOXIDF_ROLE.equals(role) || KNOXIDF_ADMIN_ROLE.equals(role); + } + + private static boolean isKnoxIdfEnabledOnDisk(GatewayConfig gatewayConfig) { + if (gatewayConfig == null) { + return false; + } + final String topologyDir = gatewayConfig.getGatewayTopologyDir(); + if (StringUtils.isBlank(topologyDir)) { + return false; + } + final Path dir = Paths.get(topologyDir); + if (!Files.isDirectory(dir)) { + return false; + } + try (Stream<Path> files = Files.list(dir)) { + return files.filter(AbstractServiceFactory::isTopologyFile).anyMatch(AbstractServiceFactory::topologyFileEnablesKnoxIdf); + } catch (IOException e) { + LOG.failedToListTopologyDirForKnoxIdfDetection(topologyDir, e.getMessage(), e); + return false; + } + } + + private static boolean isTopologyFile(Path path) { + if (!Files.isRegularFile(path)) { + return false; + } + final String name = path.getFileName().toString().toLowerCase(Locale.ROOT); + return name.endsWith(".xml") || name.endsWith(".topology"); + } + + private static boolean topologyFileEnablesKnoxIdf(Path path) { + try { + final String content = new String(Files.readAllBytes(path), StandardCharsets.UTF_8); + return content.contains("<role>" + KNOXIDF_ROLE + "</role>") || content.contains("<role>" + KNOXIDF_ADMIN_ROLE + "</role>"); + } catch (IOException e) { + LOG.failedToReadTopologyFileForKnoxIdfDetection(path.toString(), e.getMessage(), e); + return false; + } + } + // abstract methods protected abstract Service createService(GatewayServices gatewayServices, ServiceType serviceType, GatewayConfig gatewayConfig, Map<String, String> options, 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 index 7ffe009fa..fbfeaa6d0 100644 --- 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 @@ -18,16 +18,16 @@ 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.database.DatabaseType; 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.DerbyDBFederatedIdentityService; import org.apache.knox.gateway.services.knoxidf.federation.EmptyFederatedIdentityService; 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; @@ -43,46 +43,71 @@ public class FederatedIdentityServiceFactory extends AbstractServiceFactory { 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(); - } + // No explicit impl configured: auto-select a persistence backend when KnoxIDF is deployed. + // Otherwise honor the configured impl (very likely a prod JDBC store). + if (isEmptyDefaultImplementation(implementationToUse) && isKnoxIdfEnabledInAnyTopology(gatewayServices, gatewayConfig)) { + implementationToUse = chooseAutoImplementation(gatewayConfig); } FederatedIdentityService service = null; if (shouldCreateService(implementationToUse)) { if (matchesImplementation(implementationToUse, EmptyFederatedIdentityService.class, true)) { service = new EmptyFederatedIdentityService(); + } else if (matchesImplementation(implementationToUse, DerbyDBFederatedIdentityService.class)) { + service = createDerbyService(gatewayServices, gatewayConfig, options); } 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 EmptyFederatedIdentityService(); - } - } catch (Exception e) { - throw new ServiceLifecycleException("Error while creating Federated Identity Service: " + e, e); - } + service = createJdbcService(gatewayServices, gatewayConfig, options); } 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; - } - } + /** + * Chooses the auto-enabled implementation when KnoxIDF is deployed with no explicit impl: an + * operator-configured external database wins (very likely a prod JDBC store), otherwise a + * self-provisioning embedded Derby store (the {@code none}/{@code derbydb} default) so + * federation works out of the box without any extra infrastructure. + */ + String chooseAutoImplementation(GatewayConfig gatewayConfig) { + return isExternalDatabaseConfigured(gatewayConfig) + ? JdbcFederatedIdentityService.class.getName() + : DerbyDBFederatedIdentityService.class.getName(); + } + + private boolean isExternalDatabaseConfigured(GatewayConfig gatewayConfig) { + final String databaseType = gatewayConfig.getDatabaseType(); + try { + return DatabaseType.fromString(databaseType) != DatabaseType.DERBY; + } catch (IllegalArgumentException e) { + // "none" (the default) or any unrecognized value: no real external DB -> use Derby. + return false; + } + } + + private FederatedIdentityService createDerbyService(GatewayServices gatewayServices, GatewayConfig gatewayConfig, Map<String, String> options) { + try { + final DerbyDBFederatedIdentityService derbyService = new DerbyDBFederatedIdentityService(); + derbyService.setAliasService(getAliasService(gatewayServices)); + derbyService.setMasterService(getMasterService(gatewayServices)); + derbyService.init(gatewayConfig, options); + return derbyService; + } catch (ServiceLifecycleException e) { + LOG.errorInitializingService(DerbyDBFederatedIdentityService.class.getName(), e.getMessage(), e); + return new EmptyFederatedIdentityService(); + } + } + + private FederatedIdentityService createJdbcService(GatewayServices gatewayServices, GatewayConfig gatewayConfig, Map<String, String> options) { + try { + final JdbcFederatedIdentityService jdbcService = new JdbcFederatedIdentityService(); + jdbcService.setAliasService(getAliasService(gatewayServices)); + jdbcService.init(gatewayConfig, options); + return jdbcService; + } catch (ServiceLifecycleException e) { + LOG.errorInitializingService(JdbcFederatedIdentityService.class.getName(), e.getMessage(), e); + return new EmptyFederatedIdentityService(); } - return false; } @Override @@ -92,6 +117,6 @@ public class FederatedIdentityServiceFactory extends AbstractServiceFactory { @Override protected Collection<String> getKnownImplementations() { - return List.of(DEFAULT_IMPLEMENTATION, JdbcFederatedIdentityService.class.getName()); + return List.of(DEFAULT_IMPLEMENTATION, JdbcFederatedIdentityService.class.getName(), DerbyDBFederatedIdentityService.class.getName()); } } diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/services/factory/TrustedOidcIssuerServiceFactory.java b/gateway-server/src/main/java/org/apache/knox/gateway/services/factory/TrustedOidcIssuerServiceFactory.java index daa8d11a4..3f520f094 100644 --- a/gateway-server/src/main/java/org/apache/knox/gateway/services/factory/TrustedOidcIssuerServiceFactory.java +++ b/gateway-server/src/main/java/org/apache/knox/gateway/services/factory/TrustedOidcIssuerServiceFactory.java @@ -26,8 +26,6 @@ import org.apache.knox.gateway.services.ServiceType; import org.apache.knox.gateway.services.knoxidf.trustedoidcissuer.EmptyTrustedOidcIssuerService; import org.apache.knox.gateway.services.knoxidf.trustedoidcissuer.JdbcTrustedOidcIssuerService; import org.apache.knox.gateway.services.knoxidf.trustedoidcissuer.TrustedOidcIssuerService; -import org.apache.knox.gateway.services.topology.TopologyService; -import org.apache.knox.gateway.topology.Topology; import java.util.Collection; import java.util.List; @@ -44,10 +42,8 @@ public class TrustedOidcIssuerServiceFactory extends AbstractServiceFactory { throws ServiceLifecycleException { String implementationToUse = implementation; - if (isEmptyDefaultImplementation(implementationToUse)) { - if (isKnoxIdfEnabledInAnyTopology(gatewayServices)) { - implementationToUse = JdbcTrustedOidcIssuerService.class.getName(); - } + if (isEmptyDefaultImplementation(implementationToUse) && isKnoxIdfEnabledInAnyTopology(gatewayServices, gatewayConfig)) { + implementationToUse = JdbcTrustedOidcIssuerService.class.getName(); } TrustedOidcIssuerService service = null; @@ -75,24 +71,6 @@ public class TrustedOidcIssuerServiceFactory extends AbstractServiceFactory { return service; } - /** - * Returns true if any deployed topology contains a service with role {@code KNOXIDF} - * or {@code KNOXIDF_ADMIN}. The trusted issuer registry is activated by either role - * because the admin API ({@code KNOXIDF_ADMIN}) also needs to persist registrations. - */ - 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( - s -> "KNOXIDF".equals(s.getRole()) || "KNOXIDF_ADMIN".equals(s.getRole()))) { - return true; - } - } - } - return false; - } - @Override protected ServiceType getServiceType() { return ServiceType.TRUSTED_OIDC_ISSUER_SERVICE; diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/services/knoxidf/federation/DerbyDBFederatedIdentityService.java b/gateway-server/src/main/java/org/apache/knox/gateway/services/knoxidf/federation/DerbyDBFederatedIdentityService.java new file mode 100644 index 000000000..aa4bec22c --- /dev/null +++ b/gateway-server/src/main/java/org/apache/knox/gateway/services/knoxidf/federation/DerbyDBFederatedIdentityService.java @@ -0,0 +1,101 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * <p> + * http://www.apache.org/licenses/LICENSE-2.0 + * <p> + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package org.apache.knox.gateway.services.knoxidf.federation; + +import static org.apache.knox.gateway.config.impl.GatewayConfigImpl.GATEWAY_DATABASE_NAME; +import static org.apache.knox.gateway.config.impl.GatewayConfigImpl.GATEWAY_DATABASE_TYPE; +import static org.apache.knox.gateway.database.AbstractDataSourceFactory.DATABASE_PASSWORD_ALIAS_NAME; +import static org.apache.knox.gateway.database.AbstractDataSourceFactory.DATABASE_USER_ALIAS_NAME; +import static org.apache.knox.gateway.database.DatabaseType.DERBY; +import static org.apache.knox.gateway.services.security.AliasService.NO_CLUSTER_NAME; + +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import org.apache.hadoop.conf.Configuration; +import org.apache.knox.gateway.config.GatewayConfig; +import org.apache.knox.gateway.services.ServiceLifecycleException; +import org.apache.knox.gateway.services.security.MasterService; +import org.apache.knox.gateway.services.token.impl.DerbyDBTokenStateService; +import org.apache.knox.gateway.shell.jdbc.derby.DerbyDatabase; + +/** + * A self-provisioning, embedded-Derby backed {@link FederatedIdentityService}. This is the + * auto-enabled default when KnoxIDF is deployed without an operator-configured external database, + * mirroring how {@link DerbyDBTokenStateService} is the default token-state service. + * <p> + * It reuses the single embedded Derby database that the token-state service already provisions + * under {@code ${securityDir}/tokens} (the {@code ;create=true} JDBC URL is idempotent, so + * connecting to an already-booted database simply connects), sets the shared {@link GatewayConfig} + * to point at it, ensures the connection user/password aliases exist, and then delegates all + * persistence to {@link JdbcFederatedIdentityService} (which builds the + * {@link FederatedIdentityDatabase} and self-creates its tables). + */ +public class DerbyDBFederatedIdentityService extends JdbcFederatedIdentityService { + + private DerbyDatabase derbyDatabase; + private Path derbyDatabaseFolder; + private MasterService masterService; + + public void setMasterService(MasterService masterService) { + this.masterService = masterService; + } + + @Override + public void init(GatewayConfig config, Map<String, String> options) throws ServiceLifecycleException { + try { + derbyDatabaseFolder = Paths.get(config.getGatewaySecurityDir(), DerbyDBTokenStateService.DB_NAME); + startDerby(); + ((Configuration) config).set(GATEWAY_DATABASE_TYPE, DERBY.type()); + ((Configuration) config).set(GATEWAY_DATABASE_NAME, derbyDatabaseFolder.toString()); + getAliasService().addAliasForCluster(NO_CLUSTER_NAME, DATABASE_USER_ALIAS_NAME, getDatabaseUserName()); + getAliasService().addAliasForCluster(NO_CLUSTER_NAME, DATABASE_PASSWORD_ALIAS_NAME, getDatabasePassword()); + super.init(config, options); + } catch (Exception e) { + throw new ServiceLifecycleException("Error while initiating DerbyDBFederatedIdentityService: " + e, e); + } + } + + private void startDerby() throws Exception { + derbyDatabase = new DerbyDatabase(derbyDatabaseFolder.toString()); + derbyDatabase.create(); + TimeUnit.SECONDS.sleep(1); // give a bit of time for the server to start + } + + private String getDatabasePassword() throws Exception { + final char[] dbPasswordAliasValue = getAliasService().getPasswordFromAliasForGateway(DATABASE_PASSWORD_ALIAS_NAME); + return dbPasswordAliasValue != null ? new String(dbPasswordAliasValue) : new String(masterService.getMasterSecret()); + } + + private String getDatabaseUserName() throws Exception { + final char[] dbUserAliasValue = getAliasService().getPasswordFromAliasForGateway(DATABASE_USER_ALIAS_NAME); + return dbUserAliasValue != null ? new String(dbUserAliasValue) : DerbyDBTokenStateService.DEFAULT_TOKEN_DB_USER_NAME; + } + + @Override + public void stop() throws ServiceLifecycleException { + try { + if (derbyDatabase != null) { + derbyDatabase.shutdown(); + } + } catch (Exception e) { + throw new ServiceLifecycleException("Error while shutting down Derby Database", e); + } + } +} diff --git a/gateway-server/src/main/resources/createKnoxIDFFederatedIdentityAttributesTable.sql b/gateway-server/src/main/resources/createKnoxIDFFederatedIdentityAttributesTable.sql index 239cb5df1..c74cf756e 100644 --- a/gateway-server/src/main/resources/createKnoxIDFFederatedIdentityAttributesTable.sql +++ b/gateway-server/src/main/resources/createKnoxIDFFederatedIdentityAttributesTable.sql @@ -18,4 +18,4 @@ CREATE TABLE FEDERATED_IDENTITY_ATTR ( 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 +) \ No newline at end of file diff --git a/gateway-server/src/main/resources/createKnoxIDFFederatedIdentityAttributesTableDerby.sql b/gateway-server/src/main/resources/createKnoxIDFFederatedIdentityAttributesTableDerby.sql index 90c70ff0f..60e1e247b 100644 --- a/gateway-server/src/main/resources/createKnoxIDFFederatedIdentityAttributesTableDerby.sql +++ b/gateway-server/src/main/resources/createKnoxIDFFederatedIdentityAttributesTableDerby.sql @@ -19,4 +19,4 @@ CREATE TABLE FEDERATED_IDENTITY_ATTR ( 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 +) \ No newline at end of file diff --git a/gateway-server/src/main/resources/createKnoxIDFFederatedIdentityAttributesTableOracle.sql b/gateway-server/src/main/resources/createKnoxIDFFederatedIdentityAttributesTableOracle.sql index 7ed07b1b0..9d89349aa 100644 --- a/gateway-server/src/main/resources/createKnoxIDFFederatedIdentityAttributesTableOracle.sql +++ b/gateway-server/src/main/resources/createKnoxIDFFederatedIdentityAttributesTableOracle.sql @@ -19,4 +19,4 @@ CREATE TABLE FEDERATED_IDENTITY_ATTR ( 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 +) \ No newline at end of file diff --git a/gateway-server/src/main/resources/createKnoxIDFFederatedIdentityTable.sql b/gateway-server/src/main/resources/createKnoxIDFFederatedIdentityTable.sql index acaf1c340..dd2b163a7 100644 --- a/gateway-server/src/main/resources/createKnoxIDFFederatedIdentityTable.sql +++ b/gateway-server/src/main/resources/createKnoxIDFFederatedIdentityTable.sql @@ -19,7 +19,6 @@ CREATE TABLE FEDERATED_IDENTITY ( 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 + created_at TIMESTAMP NOT NULL, + CONSTRAINT UX_FED_IDENTITY UNIQUE (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 index acaf1c340..dd2b163a7 100644 --- a/gateway-server/src/main/resources/createKnoxIDFFederatedIdentityTableDerby.sql +++ b/gateway-server/src/main/resources/createKnoxIDFFederatedIdentityTableDerby.sql @@ -19,7 +19,6 @@ CREATE TABLE FEDERATED_IDENTITY ( 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 + created_at TIMESTAMP NOT NULL, + CONSTRAINT UX_FED_IDENTITY UNIQUE (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 index 0dc42c1f7..922d7b95a 100644 --- a/gateway-server/src/main/resources/createKnoxIDFFederatedIdentityTableOracle.sql +++ b/gateway-server/src/main/resources/createKnoxIDFFederatedIdentityTableOracle.sql @@ -19,7 +19,6 @@ CREATE TABLE FEDERATED_IDENTITY ( 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 + created_at TIMESTAMP NOT NULL, + CONSTRAINT UX_FED_IDENTITY UNIQUE (provider, external_issuer, external_subject) +) \ No newline at end of file diff --git a/gateway-server/src/test/java/org/apache/knox/gateway/services/factory/FederatedIdentityServiceFactoryTest.java b/gateway-server/src/test/java/org/apache/knox/gateway/services/factory/FederatedIdentityServiceFactoryTest.java new file mode 100644 index 000000000..996aae6cb --- /dev/null +++ b/gateway-server/src/test/java/org/apache/knox/gateway/services/factory/FederatedIdentityServiceFactoryTest.java @@ -0,0 +1,211 @@ +/* + * 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 static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import org.apache.commons.io.FileUtils; +import org.apache.knox.gateway.config.impl.GatewayConfigImpl; +import org.apache.knox.gateway.database.DatabaseType; +import org.apache.knox.gateway.services.GatewayServices; +import org.apache.knox.gateway.services.Service; +import org.apache.knox.gateway.services.ServiceType; +import org.apache.knox.gateway.services.knoxidf.federation.DerbyDBFederatedIdentityService; +import org.apache.knox.gateway.services.knoxidf.federation.EmptyFederatedIdentityService; +import org.apache.knox.gateway.services.knoxidf.federation.JdbcFederatedIdentityService; +import org.apache.knox.gateway.services.security.AliasService; +import org.apache.knox.gateway.services.security.MasterService; +import org.apache.knox.gateway.services.topology.TopologyService; +import org.apache.knox.gateway.topology.Topology; +import org.apache.knox.test.TestUtils; +import org.easymock.EasyMock; +import org.junit.After; +import org.junit.Test; + +public class FederatedIdentityServiceFactoryTest { + + private final FederatedIdentityServiceFactory serviceFactory = new FederatedIdentityServiceFactory(); + private final Map<String, String> options = new HashMap<>(); + private File tempDir; + private Service createdService; + + @After + public void tearDown() throws Exception { + if (createdService != null) { + createdService.stop(); + } + if (tempDir != null) { + FileUtils.forceDelete(tempDir); + } + } + + @Test + public void shouldChooseDerbyWhenNoDatabaseConfigured() { + final GatewayConfigImpl config = EasyMock.createNiceMock(GatewayConfigImpl.class); + EasyMock.expect(config.getDatabaseType()).andReturn("none").anyTimes(); + EasyMock.replay(config); + assertEquals(DerbyDBFederatedIdentityService.class.getName(), serviceFactory.chooseAutoImplementation(config)); + } + + @Test + public void shouldChooseDerbyWhenDatabaseTypeIsDerby() { + final GatewayConfigImpl config = EasyMock.createNiceMock(GatewayConfigImpl.class); + EasyMock.expect(config.getDatabaseType()).andReturn(DatabaseType.DERBY.type()).anyTimes(); + EasyMock.replay(config); + assertEquals(DerbyDBFederatedIdentityService.class.getName(), serviceFactory.chooseAutoImplementation(config)); + } + + @Test + public void shouldChooseJdbcWhenExternalDatabaseConfigured() { + final GatewayConfigImpl config = EasyMock.createNiceMock(GatewayConfigImpl.class); + EasyMock.expect(config.getDatabaseType()).andReturn(DatabaseType.POSTGRESQL.type()).anyTimes(); + EasyMock.replay(config); + assertEquals(JdbcFederatedIdentityService.class.getName(), serviceFactory.chooseAutoImplementation(config)); + } + + @Test + public void shouldDetectKnoxIdfFromInMemoryTopology() { + final GatewayServices gatewayServices = servicesWithTopology(topologyWithRole("KNOXIDF")); + final GatewayConfigImpl config = EasyMock.createNiceMock(GatewayConfigImpl.class); + EasyMock.replay(config); + assertTrue(serviceFactory.isKnoxIdfEnabledInAnyTopology(gatewayServices, config)); + } + + @Test + public void shouldDetectKnoxIdfAdminFromInMemoryTopology() { + final GatewayServices gatewayServices = servicesWithTopology(topologyWithRole("KNOXIDF_ADMIN")); + final GatewayConfigImpl config = EasyMock.createNiceMock(GatewayConfigImpl.class); + EasyMock.replay(config); + assertTrue(serviceFactory.isKnoxIdfEnabledInAnyTopology(gatewayServices, config)); + } + + @Test + public void shouldDetectKnoxIdfViaDiskScanWhenNoTopologiesLoadedYet() throws IOException { + // Mirrors the real init-time timing: the topology monitor has not loaded topologies yet, so the + // in-memory list is empty, but the topology XML already exists on disk. + tempDir = TestUtils.createTempDir(this.getClass().getName()); + writeTopologyFile("knoxidf-sso.xml", "<topology><service><role>KNOXIDF</role></service></topology>"); + + final GatewayServices gatewayServices = servicesWithTopology(/* no in-memory topologies */); + final GatewayConfigImpl config = EasyMock.createNiceMock(GatewayConfigImpl.class); + EasyMock.expect(config.getGatewayTopologyDir()).andReturn(tempDir.getAbsolutePath()).anyTimes(); + EasyMock.replay(config); + + assertTrue(serviceFactory.isKnoxIdfEnabledInAnyTopology(gatewayServices, config)); + } + + @Test + public void shouldNotDetectKnoxIdfWhenAbsentFromMemoryAndDisk() throws IOException { + tempDir = TestUtils.createTempDir(this.getClass().getName()); + writeTopologyFile("sandbox.xml", "<topology><service><role>KNOXSSO</role></service></topology>"); + + final GatewayServices gatewayServices = servicesWithTopology(topologyWithRole("KNOXSSO")); + final GatewayConfigImpl config = EasyMock.createNiceMock(GatewayConfigImpl.class); + EasyMock.expect(config.getGatewayTopologyDir()).andReturn(tempDir.getAbsolutePath()).anyTimes(); + EasyMock.replay(config); + + assertFalse(serviceFactory.isKnoxIdfEnabledInAnyTopology(gatewayServices, config)); + } + + @Test + public void shouldHonorExplicitEmptyImplEvenWhenKnoxIdfIsDeployed() throws Exception { + final GatewayServices gatewayServices = servicesWithTopology(topologyWithRole("KNOXIDF")); + final GatewayConfigImpl config = EasyMock.createNiceMock(GatewayConfigImpl.class); + EasyMock.replay(config); + + createdService = serviceFactory.create(gatewayServices, ServiceType.KNOXIDF_FEDERATED_IDENTITY_SERVICE, config, options, + EmptyFederatedIdentityService.class.getName()); + assertTrue(createdService instanceof EmptyFederatedIdentityService); + } + + @Test + public void shouldSelectEmptyWhenKnoxIdfNotDeployed() throws Exception { + final GatewayServices gatewayServices = servicesWithTopology(/* no topologies */); + final GatewayConfigImpl config = EasyMock.createNiceMock(GatewayConfigImpl.class); + // No topology dir and no in-memory topology -> KnoxIDF not enabled -> Empty. + EasyMock.replay(config); + + createdService = serviceFactory.create(gatewayServices, ServiceType.KNOXIDF_FEDERATED_IDENTITY_SERVICE, config, options, ""); + assertTrue(createdService instanceof EmptyFederatedIdentityService); + } + + @Test + public void shouldAutoSelectDerbyServiceWhenKnoxIdfDeployedWithoutExternalDatabase() throws Exception { + tempDir = TestUtils.createTempDir(this.getClass().getName()); + final String masterSecret = "M4st3RSecret!"; + final MasterService masterService = EasyMock.createNiceMock(MasterService.class); + EasyMock.expect(masterService.getMasterSecret()).andReturn(masterSecret.toCharArray()).anyTimes(); + EasyMock.replay(masterService); + final AliasService aliasService = EasyMock.createNiceMock(AliasService.class); + EasyMock.replay(aliasService); + + final GatewayServices gatewayServices = EasyMock.createNiceMock(GatewayServices.class); + final TopologyService topologyService = EasyMock.createNiceMock(TopologyService.class); + EasyMock.expect(topologyService.getTopologies()).andReturn(Collections.singletonList(topologyWithRole("KNOXIDF"))).anyTimes(); + EasyMock.replay(topologyService); + EasyMock.expect(gatewayServices.getService(ServiceType.TOPOLOGY_SERVICE)).andReturn(topologyService).anyTimes(); + EasyMock.expect(gatewayServices.getService(ServiceType.ALIAS_SERVICE)).andReturn(aliasService).anyTimes(); + EasyMock.expect(gatewayServices.getService(ServiceType.MASTER_SERVICE)).andReturn(masterService).anyTimes(); + EasyMock.replay(gatewayServices); + + final GatewayConfigImpl config = EasyMock.createNiceMock(GatewayConfigImpl.class); + EasyMock.expect(config.getDatabaseType()).andReturn(DatabaseType.DERBY.type()).anyTimes(); + EasyMock.expect(config.getGatewaySecurityDir()).andReturn(tempDir.getAbsolutePath()).anyTimes(); + EasyMock.expect(config.getDatabaseName()).andReturn(Paths.get(tempDir.getAbsolutePath(), "tokens").toString()).anyTimes(); + EasyMock.replay(config); + + createdService = serviceFactory.create(gatewayServices, ServiceType.KNOXIDF_FEDERATED_IDENTITY_SERVICE, config, options, ""); + assertTrue("Expected a self-provisioning Derby-backed federated identity service, got " + + createdService.getClass().getName(), createdService instanceof DerbyDBFederatedIdentityService); + } + + private Topology topologyWithRole(String role) { + final Topology topology = new Topology(); + topology.setName("topology-" + role); + final org.apache.knox.gateway.topology.Service service = new org.apache.knox.gateway.topology.Service(); + service.setRole(role); + topology.addService(service); + return topology; + } + + private GatewayServices servicesWithTopology(Topology... topologies) { + final TopologyService topologyService = EasyMock.createNiceMock(TopologyService.class); + EasyMock.expect(topologyService.getTopologies()).andReturn(Arrays.asList(topologies)).anyTimes(); + EasyMock.replay(topologyService); + final GatewayServices gatewayServices = EasyMock.createNiceMock(GatewayServices.class); + EasyMock.expect(gatewayServices.getService(ServiceType.TOPOLOGY_SERVICE)).andReturn(topologyService).anyTimes(); + EasyMock.replay(gatewayServices); + return gatewayServices; + } + + private void writeTopologyFile(String name, String content) throws IOException { + Files.write(Paths.get(tempDir.getAbsolutePath(), name), content.getBytes(StandardCharsets.UTF_8)); + } +} diff --git a/gateway-server/src/test/java/org/apache/knox/gateway/services/knoxidf/federation/DerbyDBFederatedIdentityServiceTest.java b/gateway-server/src/test/java/org/apache/knox/gateway/services/knoxidf/federation/DerbyDBFederatedIdentityServiceTest.java new file mode 100644 index 000000000..267dd28a1 --- /dev/null +++ b/gateway-server/src/test/java/org/apache/knox/gateway/services/knoxidf/federation/DerbyDBFederatedIdentityServiceTest.java @@ -0,0 +1,108 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * <p> + * http://www.apache.org/licenses/LICENSE-2.0 + * <p> + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package org.apache.knox.gateway.services.knoxidf.federation; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Paths; +import java.time.Instant; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +import org.apache.commons.io.FileUtils; +import org.apache.knox.gateway.config.impl.GatewayConfigImpl; +import org.apache.knox.gateway.database.DatabaseType; +import org.apache.knox.gateway.services.security.AliasService; +import org.apache.knox.gateway.services.security.MasterService; +import org.apache.knox.test.TestUtils; +import org.easymock.EasyMock; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +/** + * Verifies that {@link DerbyDBFederatedIdentityService} self-provisions an embedded Derby database + * and round-trips a federated identity through it. + */ +public class DerbyDBFederatedIdentityServiceTest { + + private File securityDir; + private DerbyDBFederatedIdentityService service; + + @Before + public void setUp() throws IOException { + securityDir = TestUtils.createTempDir(this.getClass().getName()); + } + + @After + public void tearDown() throws Exception { + if (service != null) { + service.stop(); + } + if (securityDir != null) { + FileUtils.forceDelete(securityDir); + } + } + + @Test + public void shouldRoundTripAFederatedIdentityOnEmbeddedDerby() throws Exception { + final String masterSecret = "M4st3RSecret!"; + final MasterService masterService = EasyMock.createNiceMock(MasterService.class); + EasyMock.expect(masterService.getMasterSecret()).andReturn(masterSecret.toCharArray()).anyTimes(); + EasyMock.replay(masterService); + + final AliasService aliasService = EasyMock.createNiceMock(AliasService.class); + EasyMock.replay(aliasService); + + final GatewayConfigImpl config = EasyMock.createNiceMock(GatewayConfigImpl.class); + EasyMock.expect(config.getGatewaySecurityDir()).andReturn(securityDir.getAbsolutePath()).anyTimes(); + EasyMock.expect(config.getDatabaseType()).andReturn(DatabaseType.DERBY.type()).anyTimes(); + EasyMock.expect(config.getDatabaseName()).andReturn(Paths.get(securityDir.getAbsolutePath(), "tokens").toString()).anyTimes(); + EasyMock.replay(config); + + service = new DerbyDBFederatedIdentityService(); + service.setAliasService(aliasService); + service.setMasterService(masterService); + service.init(config, Collections.emptyMap()); + + final Map<String, String> attributes = new HashMap<>(); + attributes.put("email", "[email protected]"); + final FederatedIdentity identity = new FederatedIdentity("knox-user-1", "KEYCLOAK", "external-subject-1", + "https://issuer.example.com/realms/knox", Instant.now(), attributes); + service.addFederatedIdentity(identity); + + final Optional<FederatedIdentity> byId = service.findById(identity.getId()); + assertTrue("Expected the identity to be found by id", byId.isPresent()); + assertEquals("KEYCLOAK", byId.get().getProvider()); + assertEquals("[email protected]", byId.get().getAttribute("email")); + + final Optional<FederatedIdentity> byProviderAndSubject = service.findByProviderAndSubject( + "KEYCLOAK", "https://issuer.example.com/realms/knox", "external-subject-1"); + assertTrue("Expected the identity to be found by provider/issuer/subject", byProviderAndSubject.isPresent()); + assertEquals(identity.getId(), byProviderAndSubject.get().getId()); + + final Optional<FederatedIdentity> missing = service.findByProviderAndSubject( + "KEYCLOAK", "https://issuer.example.com/realms/knox", "no-such-subject"); + assertFalse("Did not expect an identity for an unknown subject", missing.isPresent()); + } +} 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 index b27c7f9d8..8cff9a805 100644 --- 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 @@ -18,7 +18,11 @@ package org.apache.knox.gateway.service.knoxidf; import com.fasterxml.uuid.Generators; import com.fasterxml.uuid.impl.NameBasedGenerator; +import com.nimbusds.jose.JOSEObjectType; import com.nimbusds.jose.KeyLengthException; +import com.nimbusds.jose.proc.DefaultJOSEObjectTypeVerifier; +import com.nimbusds.jose.proc.JOSEObjectTypeVerifier; +import com.nimbusds.jose.proc.SecurityContext; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.tuple.Pair; import org.apache.http.NameValuePair; @@ -126,6 +130,11 @@ public class AuthorizeResource extends PasscodeTokenResourceBase { private FederatedIdentityService federatedIdentityService; private boolean autoConsentEnabled; + @Override + public String getPrefix() { + return "knoxidf."; + } + @PostConstruct @Override public void init() throws ServletException, AliasServiceException, ServiceLifecycleException, KeyLengthException { @@ -544,8 +553,14 @@ public class AuthorizeResource extends PasscodeTokenResourceBase { try { final JWTokenAuthority authority = getGatewayServices().getService(ServiceType.TOKEN_SERVICE); + // A non-null JWS type verifier is required: federated OP id_tokens carry a "typ" header + // (Keycloak and most OPs set typ=JWT), and the shared token authority rejects any typ'd + // token outright when no verifier is supplied. Accept "JWT" and a missing typ (typ is + // optional per RFC 7519) so we interoperate with the range of conformant OPs. + final JOSEObjectTypeVerifier<SecurityContext> typeVerifier = + new DefaultJOSEObjectTypeVerifier<>(new HashSet<>(Arrays.asList(JOSEObjectType.JWT, null))); // Verifies the signature against the OP's JWKS and checks exp/nbf. - if (!authority.verifyToken(idToken, Collections.singleton(new URI(jwksEndpoint)), opConfig.getSignatureAlgorithm(), null)) { + if (!authority.verifyToken(idToken, Collections.singleton(new URI(jwksEndpoint)), opConfig.getSignatureAlgorithm(), typeVerifier)) { return error("invalid_request", "Federated id_token signature or expiry verification failed"); } } catch (URISyntaxException e) { 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 index 513b275c3..1d0980f56 100644 --- 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 @@ -103,7 +103,7 @@ public class TokenResource extends PasscodeTokenResourceBase { @Override public String getPrefix() { - return "knoxidf"; + return "knoxidf."; } @PostConstruct
