Skip to content

Knox as an OAuth 2.0 / OpenID Connect Provider (KnoxIDF) - #1351

Merged
smolnar82 merged 49 commits into
masterfrom
knox_idf
Aug 14, 2026
Merged

Knox as an OAuth 2.0 / OpenID Connect Provider (KnoxIDF)#1351
smolnar82 merged 49 commits into
masterfrom
knox_idf

Conversation

@smolnar82

@smolnar82 smolnar82 commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

What changes were proposed in this pull request?

This PR introduces KnoxIDF, turning Apache Knox into an OAuth 2.0 / OpenID Connect Provider (OP) in its own right, while retaining Knox's existing role as a federation client. Historically Knox has only ever been a relying party (delegating auth to CAS/SAML/OIDC via pac4j); with KnoxIDF, Knox can now issue its own signed access and ID tokens, and optionally broker login to external OPs (Keycloak, Okta, Azure AD, Auth0, …) and re-issue Knox-signed tokens. Downstream services integrate with Knox exactly as they would with any standard OIDC provider.

📖 This change is documented in full in a new documentation book under knox-site/docs/knoxidf/ (start at index.md). Reviewers and future readers should treat the book as the authoritative reference — it covers the architecture, every endpoint, every config parameter, the security model, the federation broker flow, operations, and worked downstream integrations. This description is a summary; the book has the detail.

Highlights

KnoxIDF is a new Knox service (role KNOXIDF, with an admin counterpart KNOXIDF_ADMIN) that attaches to any topology and exposes standard OIDC endpoints:

  • Discovery (.well-known/openid-configuration), authorize, token, userinfo, jwks, and dynamic client registration.
  • Client Credentials flow (machine-to-machine).
  • Authorization Code + PKCE flow — PKCE (S256) for public clients, client_secret for confidential clients.
  • Refresh tokens with rotation.
  • One-time consent per (user, client, scopes).
  • Optional federation: broker login to one or more external OIDC Providers, validate their id_token, and re-issue Knox tokens; stable federated subject via UUIDv5.
  • Attribute enrichment: hard-coded ID-token claims plus pluggable user-parameter providers (e.g. LDAP).
  • Persistence of federated identity data (ID-token–derived data only — no access/refresh tokens or secrets at rest).

Main areas touched

  • New module gateway-service-knoxidf — the OIDC endpoint resources (AuthorizeResource, TokenResource, UserInfoResource, DiscoveryResource, JwksResource, RegistrationResource, consent servlet), audit, user-param providers, and deployment contributors.
  • gateway-server — new services and JDBC-backed stores: TrustedOidcIssuerService, FederatedIdentityService (with Derby/JDBC/empty implementations and service factories), OIDC discovery helper, DB schema/DDL for trusted issuers and federated identity, and gateway-site config wiring.
  • gateway-spi / gateway-util-common — federation and trusted-issuer SPI types, KnoxIDF* utility/constants/artifact-store classes, JWTokenAttributes extensions, new audit Action/ResourceType values.
  • Federation filters (gateway-provider-security-jwt)JWTFederationFilter / AbstractJWTFilter extended for dynamic JWKS and the iss attribute on token-exchange (KNOX-3405/3408), plus SSOCookieProvider support for the federated login page.
  • KnoxSSO / knoxtokenWebSSOResource, TokenResource, JWKSResource updated to publish multiple JWKs and select the verification key by kid; knoxauth login page gains the "Continue with " federated sign-in option.
  • Documentation — the KnoxIDF book (index, getting started, endpoints, configuration, security, federation, operations, integrations) plus mkdocs.yml nav and architecture/screenshot assets.

This branch also rolls up the incremental KnoxIDF sub-tasks that landed along the way (KNOX-3355, KNOX-3368, KNOX-3390, KNOX-3396, KNOX-3405, KNOX-3408) and a subsequent security-hardening pass (single-use auth codes, constant-time client-secret checks, fail-closed secret/alias resolution, redirect-URI normalization, OIDC nonce binding, refresh-token client auth).

Background: this work was originally proposed as KIP-18 — Knox as OIDC Provider. The implementation has since evolved; where the KIP and the book differ, the book reflects the current code and is authoritative.

How was this patch tested?

Unit tests. Extensive JUnit coverage was added across the new and modified modules, including:

  • Endpoint resources: authorize (redirect-URI matching, federated nonce/subject, success redirect, client-secret resolution), token (auth-code replay/single-use, client auth, refresh-token client auth and rotation), userinfo (invalid-token → 401), discovery (metadata + PKCE methods), registration (anonymous guard, redirect-URI policy), consent key derivation, and audit.
  • Services/stores: TrustedOidcIssuerService (JDBC/Derby/empty + schema), FederatedIdentityService (Derby/JDBC + factory), OIDCDiscoveryHelper, DefaultTokenAuthorityService multi-JWK selection, token-state services.
  • Federation filters: JWTFederationFilter token-exchange (dynamic JWKS, iss), OAuth-flows federation, SSO cookie provider.

Docker-based integration tests. New end-to-end suites run against a real Knox distribution under Docker Compose:

  • test_knoxidf.py — exercises the KnoxIDF OIDC endpoints (discovery, client registration, Client Credentials and Authorization Code + PKCE flows, token/userinfo/JWKS) against the knoxidf-ldap, knoxidf-sso, and knoxidf-token CI topologies.
  • test_knoxidf_federation.py — the federation case: stands up a real Keycloak as an external OpenID Provider (realm imported from compose/keycloak/realm.json) and drives the full broker flow — Knox delegates login to Keycloak, validates the OP id_token, and re-issues Knox tokens.

Manual testing with Apache Polaris. I verified real downstream consumption of Knox-issued tokens by integrating KnoxIDF with Apache Polaris, documented under Integrations in the book:

  • Polaris (Client Credentials) — KnoxIDF replaces Keycloak as Polaris' external OIDC provider; Polaris fetches Knox discovery + JWKS, validates the token, and maps claims to a principal/roles across Polaris' internal / external / mixed realms (machine-to-machine).
  • Polaris Console (Authorization Code + PKCE) — the Polaris Console SPA signs in a human via Authorization Code + PKCE as a public (secret-less) client, obtains a Knox token in the browser, and calls the Polaris API with it, including the cross-origin/CORS wiring.

Integration Tests

New integration tests were added under .github/workflows/tests:

  • test_knoxidf.py — KnoxIDF OIDC endpoint / flow coverage (runs on every PR).
  • test_knoxidf_federation.py — full Keycloak-broker federation E2E (opt-in, see below).

Supporting CI assets: new topologies (knoxidf-ldap.xml, knoxidf-sso.xml, knoxidf-token.xml, knoxsso.xml), docker-compose.knoxidf-federation.yml, and the Keycloak realm import.

Opt-in test suites (PR labels)

Some integration suites are expensive and are not run on every PR. Add the corresponding label to this PR to run them:

Label Runs
test-federation KnoxIDF federation E2E (test_knoxidf_federation.py). Stands up a real Keycloak as an external OpenID Provider and drives the full broker flow. Adds a few minutes (image pull + realm import).
skip-tests Skips the entire Docker Compose test job.

When to add the label: these labels only take effect on runs triggered by opening the PR, pushing a commit, or reopening the PR — the workflow does not run on a label change. Add the test-federation label before opening the PR (or before your next push) to exercise the federation suite.

UI changes

The KnoxAuth login page (knoxauth) gains an optional "Continue with <OP>" federated sign-in button (shown below an "Or" separator) when a topology fronts /authorize with an SSO cookie provider and has one or more federated OPs enabled, plus a one-time consent page for the Authorization Code flow. See the Federation and Endpoints → consent page chapters for screenshots.

smolnar82 and others added 30 commits August 13, 2026 18:01
* KnoxIDF - Initial commit
* KnoxIDF - multi OP support
* KnoxIDF - make token endpoint configurable during discovery
* KnoxIDF - Code cleanup and bug fixes
* KnoxIDF - Multi OP enablement improvements and code adoption to Larry's recent changes
* KnoxIDF - Add REFRESH_TOKEN support
* KnoxIDF - Automatically enable JdbcFederatedIdentityService when KnoxIDF is present in any topology
* KnoxIDF - Added Docker-based integration tests
* KnoxIDF: configurable user params provider (only LDAP for now)
* KnoxIDF: add support for auth code flow with PKCE
* KnoxIDF: fix an issue with the empty user params provider implementation
* KnoxIDF: Refactor Docker build to use local Maven artifacts and unify CI/Dev workflows
* KNOX-3355 - Add TrustedOidcIssuerService schema and interface
Co-authored-by: Harrison <hsheinblatt@cloudera.com>
…Authz (#1337)

Remove redundant admin URL paths, still allowing separate ACLs for different
knox idf admin APIs using PathAclAuthz.

Co-authored-by: Harrison <hsheinblatt@cloudera.com>
…change (#1339)

* KNOX-3408 - Regression tests for subject handling in JWTFederationFilter.handleTokenExchange and
TokenExchangePrincipal handling in AbstractIdentityAssertionFilter#continueChainAsPrincipal handling

Only unit tests are added for existing functionality.

* KNOX-3408 - Allow no actor token in JWTFederationFilter.handleTokenExchange.

---------

Co-authored-by: Harrison <hsheinblatt@cloudera.com>
…hangeHandlerTest; drop redundant JWTFederationFilterHandleTokenExchangeTest
Fixes a batch of security and correctness findings from reviewing the
squashed "Knox as OIDC Provider" feature. Reviewed and tested together.

Tier 1 (critical):
- 1.1 Authenticate the client on the auth-code token exchange. The
  JWTFederationFilter Bearer path forwards to the token endpoint without
  checking client_secret, so a stolen code could be redeemed by anyone
  holding some valid Knox JWT. The endpoint now independently binds
  redemption to the client: PKCE code_verifier (S256) when a challenge was
  stored, else a constant-time client_secret check.
- 1.2 Validate the federated id_token (signature via the OP JWKS, expected
  issuer, audience, exp/nbf) before trusting any claim; fail closed when
  jwks.endpoint/issuer are not configured.
- 1.3 Add knoxidf.client.registration.anonymous.allowed (default false):
  dynamic client registration refuses anonymous callers unless explicitly
  enabled. Sample knoxidf-ldap topology opts in to preserve open reg.
- 1.4 Stop leaking custom claims across users: build a per-request copy of
  the claim map instead of mutating the shared singleton field.

Tier 2 (high):
- 2.1 Default issueTime to now in JWTokenAttributesBuilder so every token
  gets a correct iat (fixes iat=1970 on KnoxSSO cookies/assertions).
- 2.2 Set SCOPE_ATTRIBUTE to the scope value (was a double getClaim -> null).
- 2.3 Match redirect_uri on parsed URI components with a path/host boundary
  (fixes wildcard open-redirect via startsWith).
- 2.5 Require S256 PKCE; reject plain.
- 2.6 Use the Knox truststore for federated OP HTTP calls.
- 2.7 Treat auto_consent as a server-side policy, not a client bypass.
- 2.8 Escape username in LDAP DN/filter (Rdn.escapeValue).
- 2.9 Require and validate state on the authorize flow.
- 2.10 Null-guard the federated callback / registration and return 4xx.

Tests: TokenResourceClientAuthTest, RegistrationResourceAnonymousGuardTest,
FederatedOpConfigurationTest, JWTTokenTest iat assertion.

Deferred to follow-up PRs: 1.5 (secrets at rest via AliasService),
2.4 + 2.11 (atomic single-use auth-code consume + schema migration across
dialects; the auth-code replay window remains until 2.4 lands), Tier 3/4.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…nce (1.5, 2.4, 2.11)

Three KnoxIDF security/correctness hardening findings from the OIDC-provider
review, landed together.

1.5 - Secrets at rest
- Federated OP client_secret is now resolvable via AliasService. New optional
  topology param federated.op.<name>.clientSecret.alias
  (FederatedOpConfiguration). AuthorizeResource.resolveClientSecret prefers the
  alias (getPasswordFromAliasForCluster, cluster from GATEWAY_CLUSTER_ATTRIBUTE
  with NO_CLUSTER_NAME fallback); fails closed if a configured alias is
  unresolvable rather than leaking the plaintext param. Plaintext clientSecret
  retained only as a fallback when no alias is set.
- Federated OP access token is no longer persisted at rest: decorateAuthCodeToken
  stores only the FEDERATED_IDENTITY_ID pointer. Removed the now-dead
  split/join/chunk token helpers and FEDERATED_*_TOKEN_PREFIX constants.

2.4 - Single-use authorization codes (close the replay window)
- SPI TokenStateService: new default consumeToken(String) - atomic single-use
  consume; exactly one concurrent caller receives true, an absent token is false
  (never throws).
- DefaultTokenStateService: overrides consumeToken with a true atomic claim
  (tokenExpirations.remove(id) != null), evicting the remaining per-token state.
- JDBCTokenStateService: overrides consumeToken using the primary-key DELETE as
  the atomic arbiter, evicts the in-memory cache, and fails closed on SQLException
  (the inherited removeToken swallows it and would falsely report a win). Derby
  inherits this.
- idf TokenResource: validateAuthCode now returns the metadata it already reads;
  handleAuthorizationCodeFlow validates, then atomically consumes the code BEFORE
  issuing any token, stashing the metadata in a per-request attribute consumed by
  the issuance steps (getAuthCodeMetadata). The revoke-in-finally is removed. A
  code that fails validation is deliberately NOT consumed, so replaying with bad
  params cannot burn a victim's still-valid code.

2.11 - Persistence hardening
- Consent key reshaped to fit KNOX_TOKEN_METADATA.md_name VARCHAR(32):
  AuthorizeResource.consentMetadataKey(subject) = "consent_" +
  first-20-hex(SHA-256(subject)) (28 chars), used by both hasConsent and
  markConsentAccepted so read and write agree. No schema change to md_name.
- Derby DDL parity: added the missing NOT NULL constraints to the federated
  identity and attribute tables (the UNIQUE index was already present).
- TOCTOU: JdbcFederatedIdentityService.addFederatedIdentity now inserts and
  catches instead of check-then-insert; a unique-constraint violation
  (SQLIntegrityConstraintViolationException or SQLState class 23, walked up the
  cause chain) is treated as a benign already-exists. The unique index is the
  arbiter.
- Transaction boundary: FederatedIdentityDatabase.addFederatedIdentity writes the
  core row and attribute rows on a single connection with autocommit off, then
  commits (rollback on failure) so an identity is never persisted without its
  attributes.
- Double-checked locking: added the missing inner recheck in
  JdbcFederatedIdentityService.init; corrected its misleading exception message.
- SELECT * replaced with the explicit column list in the by-provider/issuer/subject
  query; KnoxDatabase resolves DDL via getClass().getClassLoader() so each subclass
  loads its own create*.sql.

Tests
- DefaultTokenStateServiceTest: +3 consumeToken tests (single-use, state removed,
  unknown id).
- New TokenResourceAuthCodeReplayTest: a losing (already-consumed) redemption
  yields invalid_grant with no issuance; a winning redemption issues exactly once.
- New ConsentMetadataKeyTest: key fits VARCHAR(32) for realistic subjects,
  deterministic, distinct subjects -> distinct keys.
- FederatedOpConfigurationTest: +2 (clientSecret alias read / absent by default).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Tier 3 (hardening):
- Registration redirect_uris require HTTPS (plain HTTP only for loopback, RFC 8252)
- XSS: escape unknown scopes in AuthConsentServlet; rebuild knoxauth.js OP links
  via the DOM API instead of raw HTML / inline onclick interpolation
- URL-encode clientId and federated-OP redirect params in AuthorizeResource / KnoxIDFUtils
- Single-use consent/federation state: add KnoxIDFArtifactStore.remove() and invalidate
  state after use in authCallback and consentAccepted
- Reject disabled refresh tokens (isEnabled check) in TokenResource
- Remove hardcoded LDAP "admin-password" fallback; fail fast without a configured alias
- DiscoveryResource: literal String.replace instead of regex replaceAll for topology name
- error() maps OAuth error codes to correct HTTP status per RFC 6749 5.2 (no longer always 401)
- RedirectToUrlFilter: treat a blank fedOpSid the same as absent

Tier 4 (polish):
- Fix public-API typos: EmptyFederatedIdentityService, BASE_RESOURCE_PATH
- Make ALLOWED_RESPONSE_TYPES / DEFAULT_SCOPES immutable; copy at mutating call sites
- Drop nonce from the UserInfo response (id_token only)
- Document the KnoxIDFArtifactStore ttl*2 grace window

Tests: KnoxIDFUtilsErrorStatusTest, RegistrationRedirectUriPolicyTest, KnoxIDFArtifactStoreTest
(+18); TokenResourceAuthCodeReplayTest updated 401->400. All affected modules green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ated-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 <noreply@anthropic.com>
… finding H1)

validateRefreshTokenGrant matched only the client_id carried on the refresh
token and never proved client identity. Because the JWTFederationFilter Bearer
path forwards a request to the token endpoint without checking client_secret,
matching client_id alone would let anyone holding a stolen refresh token redeem
and rotate it. Add a constant-time client_secret check (isValidClientSecret,
MAC over tokenId+issueTime+userName+rawPasscode) after the client_id match,
mirroring the client authentication the authorization_code grant already does.

Covered by TokenResourceRefreshTokenClientAuthTest (valid secret passes;
missing / wrong / bound-to-a-different-client secret rejected).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…iew finding H2)

The federated broker leg sent no nonce to the external OP and never checked one
on the callback, so a signature-valid id_token minted for a different (or
attacker-initiated) authorization request could be injected/replayed at the
callback. Add the OIDC nonce binding (OIDC Core 3.1.2.1):

- New JVM-singleton FederatedNonceStore (mirrors FederatedOpConfigurationStore),
  single-use, keyed by the federated login-session id (== the state echoed by
  the OP).
- WebSSOResource.federatedOpLogin mints a per-request nonce, stashes it, and
  passes it to the redirect builder.
- KnoxIDFUtils.buildFederatedOpAuthRedirect now appends &nonce=<value>
  (percent-encoded).
- AuthorizeResource.authCallback retrieves and single-use-removes the expected
  nonce and, only after the id_token's signature/issuer/audience are verified,
  requires its nonce claim to match (verifyFederatedNonce). Missing expected
  nonce or a mismatch fails the flow.

Covered by AuthorizeResourceFederatedNonceTest (match passes; mismatch, missing
claim, and missing expected nonce all rejected).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ew finding M1)

handleRefreshToken revoked the presented refresh token with revokeToken() only
after validation, then minted the replacement. On DefaultTokenStateService
revokeToken is check-then-act, so two concurrent redemptions of the same refresh
token could both pass validation and both mint a new access/refresh pair. (The
JDBC/Derby path is already atomic via a PK DELETE; this is the in-memory gap.)

Replace revokeToken with the atomic consumeToken as a single-use claim performed
BEFORE issuance: exactly one concurrent caller wins and rotates, the losers get
invalid_grant and mint nothing. Mirrors the consume-before-issue guard already on
the authorization_code grant (finding 2.4).

Covered by TokenResourceRefreshTokenRotationTest (a lost consume yields
invalid_grant with no issuance).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…eview finding M2)

The federated callback derives the Knox subject and the federated-identity
primary key from the OP id_token's sub claim, and the identity tables declare
external_subject NOT NULL. A broken or hostile OP that returns a verified
id_token with no sub therefore drove a NOT NULL INSERT failure -> HTTP 500 on
every callback through that OP (a targeted DoS).

sub is REQUIRED by OIDC Core 2. Enforce its presence on the (already
signature/issuer/audience-verified) id_token via requireFederatedSubject and
return invalid_request when it is absent/blank, before any persistence happens.
The iss claim is already covered: the configured expectedIssuer is non-blank, so
a blank issuer fails the existing issuer-mismatch check.

Covered by AuthorizeResourceFederatedSubjectTest.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…finding M3)

matchesRedirectUri did a startsWith on the un-normalized requested path in the
wildcard branch, so a registration of https://app.example/callback/* matched
https://app.example/callback/../admin (path "/callback/../admin" starts with
"/callback") and the authorization code was delivered to /admin -- a same-host
open redirect. The exact, non-wildcard branch was already safe.

Normalize both the base and requested paths (URI.normalize()) before the prefix
compare so "/callback/../admin" collapses to "/admin" and no longer matches the
"/callback" prefix. Genuine sub-paths under the wildcard prefix still match.

Covered by AuthorizeResourceRedirectUriMatchTest.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…esolvable (review finding M4)

resolveClientSecret returned null when a configured client-secret alias could not
be resolved. That null flowed into the token-request form as a
BasicNameValuePair, which the URL encoder serialized to a literal
client_secret=null and sent to the OP -- the opposite of fail-closed: instead of
refusing, Knox made a back-channel call with a bogus secret.

Make it truly fail closed: requireResolvedAliasSecret throws
ClientSecretResolutionException when a declared alias resolves to nothing (null
or empty), and authCallback catches it and returns a clear server_error before
any HTTP request to the OP. The plaintext clientSecret fallback (no alias
configured) is unchanged for backward compatibility.

Covered by AuthorizeResourceClientSecretResolutionTest.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…Service.init (review finding M5)

init() tested initialized only at the outer (pre-lock) check and was missing the
inner re-check under the lock that JdbcFederatedIdentityService has. A startup
race where two threads both observed initialized==false could therefore both
enter the critical section sequentially and initialize twice, the second run
overwriting the already-built database and discoveryHelper references.

Add the inner if (!initialized.get()) recheck inside the lock so a thread that
blocked while another was initializing becomes a no-op. Mirrors the sibling
JdbcFederatedIdentityService.

Covered by JdbcTrustedOidcIssuerServiceTest#testConcurrentInitDoesNotReinitialize,
which deterministically drives the race via the service's own init lock queue and
asserts the database reference is not rebuilt once initialized.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…arer token (review finding M6)

getTokenMetadata throws UnknownTokenException for an expired/revoked/unknown
token, and doGet rethrew it as an unmapped RuntimeException -> HTTP 500. RFC 6750
requires a protected resource to answer such a token with 401 and a
WWW-Authenticate: Bearer error="invalid_token" challenge.

Catch UnknownTokenException in getUserInfo and return the RFC 6750 challenge via
the new invalidToken() helper. Also handle a token that references a
now-missing federated identity the same way (the token can no longer be
honored) instead of throwing. doGet no longer wraps-and-rethrows.

Covered by UserInfoResourceInvalidTokenTest.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… a query (review finding M7)

redirectToAuthSuccess appended "?code=...&state=..." unconditionally. When the
registered redirect_uri already carried a query string (e.g.
https://app.example/cb?ui=dark), the result had two "?" separators, so the
client parsed neither code nor state and the authorization-code flow silently
failed for those clients.

Extract buildSuccessRedirect, which uses "&" when the redirect_uri already
contains a query string and "?" otherwise, preserving any pre-existing params.

Covered by AuthorizeResourceSuccessRedirectTest.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…pported (review finding M8)

The discovery document listed ["plain","S256"] for
code_challenge_methods_supported, but AuthorizeResource rejects every
code_challenge_method other than S256. A client that trusted discovery and used
plain PKCE was then rejected at /authorize.

Drop "plain" so discovery matches enforcement.

Covered by DiscoveryResourcePkceMethodsTest.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… (J2)

Add audit-log completeness for the KnoxIDF authorization server. Every
security-relevant decision on the Authorize, Token, Registration and
UserInfo endpoints is now recorded through Knox's existing Auditor/
AuditService framework with a consistent action/outcome/resource shape:

- AuthorizeResource: authorization request rejected, authorization code
  issued / issuance failed, consent granted / denied / invalid-state, and
  the federated-OP callback outcome (single try/finally over all exits).
- TokenResource: authorization_code and refresh_token grant outcomes
  (issued/rotated vs. replayed/validation-failed) and unsupported grant.
- RegistrationResource: client-registration outcome incl. anonymous-denied
  and invalid_request reasons.
- UserInfoResource: /userinfo access outcome incl. invalid_token and
  unknown-federated-identity reasons.

Emission is centralized in a new KnoxIDFAudit holder so all records share
one Auditor, a single masking rule and a consistent shape. Credentials and
full tokens are NEVER logged: token ids/JWTs pass through mask() (prefix/
suffix only) and client_secret/code_verifier/raw refresh tokens are never
recorded. The Auditor field is package-private/non-final for test injection,
mirroring TrustedOidcIssuersResource.

Covered by KnoxIDFAuditTest: a representative SUCCESS (rotated refresh
grant) and FAILURE (unsupported grant) assert the emitted record, plus the
mask() never-leaks invariant. Full knoxidf suite: 88/88 green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
smolnar82 and others added 6 commits August 13, 2026 18:01
…tedOidcIssuerService default

JDBCUtils.tableExists passed the table name verbatim for uppercase-storing
engines (Derby), so a lowercase constant like federated_identity never matched
Derby's uppercased FEDERATED_IDENTITY metadata. On restart tableExists returned
false, createTableIfNotExists re-ran the CREATE, and Derby failed with
"Table/View 'FEDERATED_IDENTITY' already exists". Uppercase-constant callers
(KNOX_TOKENS, KNOX_PROVIDERS, TRUSTED_OIDC_ISSUERS) accidentally worked. Extract
a normalizeIdentifier() helper that upper-cases for uppercase-storing engines and
lower-cases for lowercase-storing ones.

Also add DerbyDBTrustedOidcIssuerService, a self-provisioning embedded-Derby
default mirroring DerbyDBFederatedIdentityService, and wire
TrustedOidcIssuerServiceFactory to auto-select Derby vs JDBC based on external
DB config instead of always using JDBC.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- JWTFederationFilter: add AuthCode TokenType; forward authorization_code
  grant to the token endpoint with an anonymous subject so public PKCE
  clients (no client_secret) are not rejected
- TokenResource: gate auth-code handling behind isAuthCodeFlow() and fall
  back to super.doPost() for other grants (no separate KNOXTOKEN needed)
- RegistrationResource: add knoxidf.custom.loopback.hosts allowlist for
  plain-HTTP redirect_uri hosts beyond localhost/127.0.0.1/::1
- DiscoveryResource: append /register to advertised registration_endpoint
- TokenResourceV2: make RESOURCE_PATH public
- Tests for AuthCode pass-through and the loopback-host policy
- Docs: Polaris and Polaris Console integration guides; ignore built site/

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
buildResponseMap gated id_token + refresh-token issuance behind
isAuthCodeFlow() only, but the refresh_token grant also flows through it,
so token rotation stopped returning a new refresh_token. Run that block
for both authorization_code and refresh_token grants, still excluding
client_credentials.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
doPost() now delegates unrecognized grant types to super.doPost() (so the
KnoxIDF endpoint doubles as the Knox-token endpoint; no separate KNOXTOKEN
service). Unknown grants are therefore no longer rejected with an
unsupported_grant_type audit. Repoint the representative FAILURE-audit test
at a still-existing failure path (a rejected refresh-token grant).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Convert testEmptyResolvedSecretFailsClosed to @test(expected=...) since its
catch block makes no assertions on the exception. The sibling test that
inspects the exception message keeps the try/fail/catch form.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@smolnar82

Copy link
Copy Markdown
Contributor Author

Cc. @hsheinblatt @handavid

@smolnar82

Copy link
Copy Markdown
Contributor Author

Closing as I forgot to apply test-federation

@github-actions

github-actions Bot commented Aug 13, 2026

Copy link
Copy Markdown

Test Results

49 tests   49 ✅  10s ⏱️
 4 suites   0 💤
 4 files     0 ❌

Results for commit d34a447.

♻️ This comment has been updated with latest results.

@smolnar82 smolnar82 changed the title Knox IDF Knox as an OAuth 2.0 / OpenID Connect Provider (KnoxIDF) Aug 14, 2026
smolnar82 and others added 8 commits August 14, 2026 14:39
Close LDAP NamingEnumeration resource leaks in LdapUserParamsProvider by closing every search cursor in finally blocks.
Enforce HTTPS on discovery-supplied dynamic JWKS URIs in JWTFederationFilter, with knox.token.exchange.dynamic.jwks.allow.http to opt out per provider.
Return the canonical persisted FederatedIdentity on a concurrent first-login race so downstream auth codes are keyed to a real id instead of a phantom UUID.
Bound the KnoxIDF federated-OP token-exchange HTTP client with configurable connect/read timeouts so an unresponsive external OP cannot pin request threads.
Escape the OAuth state value in a double-quoted attribute in AuthConsentServlet to prevent single-quote attribute breakout XSS.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…token rotation

Persist FEDERATED_IDENTITY_ID onto the rotated refresh token and restore it on
the refresh_token grant so id_token generation keeps emitting federated profile
claims after the first rotation (previously fell through to a local id_token
carrying only sub+aud).

Review by thanicz

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ire POST

consentAccepted now verifies the current effective principal matches the subject
that initiated the authorization request (rejecting a replayed consent-state URL
from a different user with 403), and is a POST endpoint so it cannot be triggered
by passive GET navigation; the consent form posts directly to the endpoints.

Review by thanicz

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ng its body

The federated token exchange now throws a dedicated FederatedTokenExchangeException
carrying only the OP HTTP status; authCallback catches it, audits the real cause,
and returns a generic server_error instead of letting a RuntimeException carrying
the OP response body escape as a 500.

Review by thanicz

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Reflect the consent-flow changes: the accept/deny endpoints are POST-only and
consentAccepted is bound to the initiating subject (endpoints.md consent-page
section + endpoint table; security.md Consent section).

Review by thanicz

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… for federated login

The login-session id used as the OIDC state sent to the external OP (and as the key
for the in-flight authorize/OP-config/nonce stores) is now a per-flow UUID rather
than request.getSession().getId(). This stops the JSESSIONID leaking to the OP via
the state parameter and fixes last-writer-wins collision between concurrent
authorization flows sharing one browser session.

Review by thanicz

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A dynamically-registered client could previously put any scope in its
allowed_scopes, letting it self-assign a privileged scope name (e.g.
'admin') that a downstream service might trust. Bound the registerable
scopes by a server-side whitelist (knoxidf.registration.allowed.scopes):
an explicit value is authoritative, a blank/unset value defaults to the
OIDC-standard scope set, and 'openid' is always registerable. A blank
allowed_scopes request receives the built-in defaults intersected with
the whitelist so the default grant can never exceed operator policy.

Review by thanicz

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…pAuthFilterTest

The JWKS-URI HTTPS-enforcement hardening added a getInitParameter call for
knox.token.exchange.dynamic.jwks.allow.http in JWTFederationFilter.init().
The EasyMock-based HadoopAuthFilterTest replays a strict mock and failed
with an unexpected-method-call on that lookup. Add the missing expectation
(returning null, i.e. HTTPS enforcement stays on) to the JWT-supported setup.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@smolnar82
smolnar82 merged commit f0a5c94 into master Aug 14, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants