Skip to content

Add multi-principal SPNEGO support with manager-owned fallback and servlet/reactive/Tomcat parity#68

Merged
bedrin merged 21 commits into
masterfrom
copilot/fix-67
Jun 7, 2026
Merged

Add multi-principal SPNEGO support with manager-owned fallback and servlet/reactive/Tomcat parity#68
bedrin merged 21 commits into
masterfrom
copilot/fix-67

Conversation

Copy link
Copy Markdown

Copilot AI commented Aug 30, 2025

Summary

This PR adds multi-principal SPNEGO support so one server can authenticate requests for multiple service principal names (SPNs), while keeping compatibility with existing single-principal setups.

After merging the latest master updates (Spring Boot/Spring Security 4.x and new reactive support), the implementation was aligned to preserve feature parity between servlet Spring Security, reactive Spring Security, and Tomcat.

Key Features

  • Canonical SPN Extraction: getServerPrincipalName() returns the full canonical SPN including realm (e.g. HTTP/host.example.com@EXAMPLE.COM) from ticket metadata (sname components + ticket realm). SPN matching is exact and case-sensitive.
  • Manager-Owned Resolution Policy: MultiPrincipalManager now owns principal selection policy, including explicit default/fallback behavior.
  • Three Explicit Modes:
    • Single-principal: existing behavior unchanged.
    • Pure multi-principal: configure only a MultiPrincipalManager; unknown or unextractable SPNs fail closed (BadCredentialsException / HTTP 401).
    • Hybrid via manager fallback: configure a multi-principal manager with explicit default client; unknown/unextractable SPNs resolve only through that configured fallback.
  • Multi-Principal Management: MultiPrincipalManager interface (in kerb4j-server-common) with SimpleMultiPrincipalManager (Spring, in kerb4j-server-spring-security-core) and TomcatMultiPrincipalManager.
  • Backward Compatibility: Existing single-principal configuration remains supported.

Additional Alignment and Refactoring

  • Merged latest master changes and resolved integration updates.
  • Ensured servlet + reactive parity by keeping SimpleMultiPrincipalManager in kerb4j-server-spring-security-core so both stacks share the same multi-principal building blocks.
  • Kept MultiPrincipalManager in kerb4j-server-common.
  • Removed redundant MultiPrincipalSunJaasKerberosTicketValidator (duplicate behavior already in SunJaasKerberosTicketValidator).
  • Removed unrelated kerb4j-server/kerb4j-server-spring-security/krb5.conf.
  • Updated MultiPrincipalManager API to include fallback-aware contract:
    • getSpnegoClientForSpn(@Nullable String spn)
    • hasPrincipalForSpn(String spn)
    • getConfiguredSpns() (collection-based)
    • getDefaultSpnegoClient()
  • Added explicit fallback configuration methods in concrete managers:
    • SimpleMultiPrincipalManager#addDefaultPrincipal(...)
    • TomcatMultiPrincipalManager#addDefaultPrincipal(...)
  • Refactored SunJaasKerberosTicketValidator and Tomcat SpnegoAuthenticator to delegate SPN/client resolution to the manager (no duplicated local fallback policy).
  • SimpleMultiPrincipalManager uses resource.getFile().getAbsolutePath() for robust keytab path resolution (avoids URL-encoded spaces); non-file resources fail fast with clear IllegalArgumentException.
  • Null/blank principal names and null resource/path arguments are rejected with IllegalArgumentException in concrete managers.
  • Added JSpecify nullability annotations across new/modified APIs in the multi-principal path.

Test Coverage and Validation

  • SpnegoInitTokenMultiPrincipalTest: parses a real SPNEGO token fixture and asserts HTTP/neo.example.org@EXAMPLE.ORG; also tests malformed/empty token paths.
  • SimpleMultiPrincipalManagerTest: validates exact/case-sensitive lookup, explicit fallback behavior, fail-closed behavior without fallback, input rejection, non-file resource rejection, and space-in-path handling.
  • TomcatMultiPrincipalManagerTest: validates exact lookup, explicit fallback behavior, fail-closed behavior without fallback, and input validation.
  • MultiPrincipalSimpleTest: verifies mode initialization behavior for validator configuration.
  • MultiPrincipalKerberosIntegrationTest: integration coverage using KerberosSecurityTestcase for multi-principal selection, unknown SPN fail-closed behavior, explicit fallback behavior, malformed token handling, and single-principal backward compatibility.
  • MultiPrincipalParityTest: verifies shared-core classes and API are available to the reactive (WebFlux) stack.
  • Full test suite green; 0 CodeQL alerts.

Documentation and Release Notes

  • Updated .github/copilot-instructions.md with repository rules for markdown/license handling, author tags, JSpecify usage, and Kerberos integration test guidance.
  • Rewrote MULTI_PRINCIPAL_SUPPORT.md: manager-owned fallback model, canonical SPN format/case-sensitivity, fail-closed behavior, keytab local-file limitation, and Tomcat configuration limitations.
  • Updated WebFlux README with accurate multi-principal guidance reflecting manager-configured fallback.
  • Updated CHANGES.md to document manager-owned fallback policy and consistent principal resolution behavior.

✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.

Copy link
Copy Markdown
Author

Copilot AI commented Aug 30, 2025

@bedrin 👋 This repository doesn't have Copilot instructions. With Copilot instructions, I can understand the repository better, work faster and produce higher quality PRs.

I can generate a .github/copilot-instructions.md file for you automatically. Click here to open a pre-filled issue and assign it to me. I'll write the instructions, and then tag you for review.

Copilot AI and others added 3 commits August 30, 2025 07:57
Co-authored-by: bedrin <4654384+bedrin@users.noreply.github.com>
Co-authored-by: bedrin <4654384+bedrin@users.noreply.github.com>
Co-authored-by: bedrin <4654384+bedrin@users.noreply.github.com>
Copilot AI changed the title [WIP] Support different principals on server side based on the spnego token recevied from token Add multi-principal SPNEGO support for handling different service principals Aug 30, 2025
Copilot AI requested a review from bedrin August 30, 2025 08:10
@bedrin
Copy link
Copy Markdown
Owner

bedrin commented Aug 30, 2025

@copilot merge latest changes from master

This includes GitHub Actions workflow updates and macOS hostname validation fixes.
Copy link
Copy Markdown
Author

Copilot AI commented Aug 30, 2025

@copilot merge latest changes from master

I've successfully merged the latest changes from master into this branch. The merge includes GitHub Actions workflow updates and macOS hostname validation fixes. All tests continue to pass after the merge. Commit: 60f735e

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • server.springsource.org
    • Triggering command: /usr/lib/jvm/temurin-17-jdk-amd64/bin/java -jar /home/REDACTED/work/kerb4j/kerb4j/kerb4j-server/kerb4j-server-spring-security/target/surefire/surefirebooter-20250830141725256_24.jar /home/REDACTED/work/kerb4j/kerb4j/kerb4j-server/kerb4j-server-spring-security/target/surefire 2025-08-30T14-16-51_804-jvmRun1 surefire-20250830141725256_22tmp surefire_4-20250830141725256_23tmp (dns block)

If you need me to access, download, or install something from one of these locations, you can either:

@bedrin
Copy link
Copy Markdown
Owner

bedrin commented Aug 30, 2025

@copilot merge latest changes from master again

Copy link
Copy Markdown
Author

Copilot AI commented Aug 30, 2025

@copilot merge latest changes from master again

I've successfully merged the latest changes from master into this branch. The merge includes a DNS blocking fix that changes SERVER_SPN from server.springsource.org to localhost in tests. All tests continue to pass after the merge. Commit: d886f0d

@bedrin bedrin marked this pull request as ready for review August 30, 2025 15:04
Copilot AI review requested due to automatic review settings August 30, 2025 15:04
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR adds comprehensive multi-principal SPNEGO support to kerb4j, allowing a single server to authenticate against multiple service principal names (SPNs) by automatically extracting the target SPN from incoming tokens and selecting the appropriate principal for validation.

  • Implements SPN extraction from SPNEGO tokens with new getServerPrincipalName() methods
  • Creates a multi-principal management system with interface and implementations for both Spring Security and Tomcat
  • Maintains full backward compatibility while enabling automatic principal selection based on token contents

Reviewed Changes

Copilot reviewed 14 out of 14 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
MultiPrincipalManager.java Core interface defining multi-principal management contract
SimpleMultiPrincipalManager.java Spring Security implementation with keytab resource management
TomcatMultiPrincipalManager.java Tomcat-specific implementation with file path configuration
SunJaasKerberosTicketValidator.java Enhanced validator with multi-principal support and backward compatibility
SpnegoAuthenticator.java Updated Tomcat authenticator with multi-principal token handling
SpnegoInitToken.java Added SPN extraction capability from SPNEGO tokens
SpnegoKerberosMechToken.java Added server principal name extraction from Kerberos tickets
MULTI_PRINCIPAL_SUPPORT.md Comprehensive documentation with usage examples and migration guide

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

Comment on lines +40 to +44
try {
SpnegoClient client = SpnegoClient.loginWithKeyTab(principal, keyTabLocation, true);
spnegoClients.put(principal, client);
} catch (Exception e) {
throw new RuntimeException("Failed to initialize principal: " + principal, e);
Copy link

Copilot AI Aug 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error message should include the keytab location for better debugging. Consider changing to: 'Failed to initialize principal: ' + principal + ' with keytab: ' + keyTabLocation

Suggested change
try {
SpnegoClient client = SpnegoClient.loginWithKeyTab(principal, keyTabLocation, true);
spnegoClients.put(principal, client);
} catch (Exception e) {
throw new RuntimeException("Failed to initialize principal: " + principal, e);
throw new RuntimeException("Failed to initialize principal: " + principal + " with keytab: " + keyTabLocation, e);

Copilot uses AI. Check for mistakes.
Comment on lines +42 to +50
try {
String keyTabPath = keyTabLocation.getURL().toExternalForm();
if (keyTabPath.startsWith("file:")) {
keyTabPath = keyTabPath.substring(5);
}
SpnegoClient client = SpnegoClient.loginWithKeyTab(principal, keyTabPath, acceptOnly);
spnegoClients.put(principal, client);
} catch (Exception e) {
throw new RuntimeException("Failed to initialize principal: " + principal, e);
Copy link

Copilot AI Aug 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error message should include the keytab location for better debugging. Consider changing to: 'Failed to initialize principal: ' + principal + ' with keytab: ' + keyTabPath

Suggested change
try {
String keyTabPath = keyTabLocation.getURL().toExternalForm();
if (keyTabPath.startsWith("file:")) {
keyTabPath = keyTabPath.substring(5);
}
SpnegoClient client = SpnegoClient.loginWithKeyTab(principal, keyTabPath, acceptOnly);
spnegoClients.put(principal, client);
} catch (Exception e) {
throw new RuntimeException("Failed to initialize principal: " + principal, e);
String keyTabPath = null;
try {
keyTabPath = keyTabLocation.getURL().toExternalForm();
if (keyTabPath.startsWith("file:")) {
keyTabPath = keyTabPath.substring(5);
}
SpnegoClient client = SpnegoClient.loginWithKeyTab(principal, keyTabPath, acceptOnly);
spnegoClients.put(principal, client);
} catch (Exception e) {
throw new RuntimeException("Failed to initialize principal: " + principal + " with keytab: " + keyTabPath, e);

Copilot uses AI. Check for mistakes.
LOG.debug("Using default single principal client for SPN: " + targetSPN);
}
} catch (Kerb4JException e) {
LOG.warn("Failed to extract SPN from token, using default principal", e);
Copy link

Copilot AI Aug 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The log level should be DEBUG instead of WARN since fallback to default principal is expected behavior, not a warning condition.

Suggested change
LOG.warn("Failed to extract SPN from token, using default principal", e);
LOG.debug("Failed to extract SPN from token, using default principal", e);

Copilot uses AI. Check for mistakes.
Comment on lines +248 to +253
if (log.isDebugEnabled()) {
log.debug("Failed to extract SPN from token, using default principal", e);
}
if (spnegoClient == null) {
if (log.isDebugEnabled()) {
log.debug("Failed to extract SPN and no default principal configured");
Copy link

Copilot AI Aug 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider logging the exception details even at debug level for troubleshooting. The current implementation only logs the message but not the exception cause.

Suggested change
if (log.isDebugEnabled()) {
log.debug("Failed to extract SPN from token, using default principal", e);
}
if (spnegoClient == null) {
if (log.isDebugEnabled()) {
log.debug("Failed to extract SPN and no default principal configured");
log.debug("Failed to extract SPN and no default principal configured", e);

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Owner

@bedrin bedrin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot please process comments which I have left in this PR and also merge latest changes from master

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove this file. It's neither related to this PR nor it makes any sense since it contains dummy hardcoded host and port

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed the krb5.conf file as requested. It was indeed unrelated to this PR and contained dummy hardcoded values. Commit: 16c314d

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This class isn't specific to Spring framework and should be in kerb4j-server-common module instead

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved MultiPrincipalManager interface from kerb4j-server-spring-security to kerb4j-server-common module. Updated all imports and made TomcatMultiPrincipalManager implement the interface. Commit: 16c314d

LOG.debug("Using SpnegoClient for SPN: " + targetSPN);

SpnegoContext acceptContext = spnegoClient.createAcceptContext();
byte[] responseToken = acceptContext.acceptToken(token);
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Try to avoid code duplication with SunJaasKerberosTicketValidator.java

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed MultiPrincipalSunJaasKerberosTicketValidator class to eliminate code duplication. The functionality already exists in SunJaasKerberosTicketValidator through the setMultiPrincipalManager() method. Commit: 16c314d

…le, remove code duplication, and cleanup

Co-authored-by: bedrin <4654384+bedrin@users.noreply.github.com>
Copy link
Copy Markdown
Author

Copilot AI commented Aug 30, 2025

@copilot please process comments which I have left in this PR and also merge latest changes from master

The branch is already up to date with the latest changes from master. No new commits found to merge. All PR review comments have been addressed in commit 16c314d.

Copilot AI requested a review from bedrin August 30, 2025 15:23
Copilot AI changed the title Add multi-principal SPNEGO support for handling different service principals Add multi-principal SPNEGO support with servlet/reactive Spring parity May 27, 2026
Copilot finished work on behalf of bedrin May 27, 2026 20:20
Copy link
Copy Markdown
Owner

@bedrin bedrin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot please fix the issues below in this PR. Treat this as a security/authentication change; do not just make tests green. Please inspect the current code and adjust the design so servlet, WebFlux/shared Spring, and Tomcat behavior is correct, documented, and covered by meaningful tests.

1. Fix SPN extraction: current getServerPrincipalName() likely returns the wrong/non-canonical key

SpnegoKerberosMechToken.getServerPrincipalName() currently returns only getApRequest().getTicket().getSname().getName(). For a Kerberos service principal this is likely incomplete because the ticket also has the realm. It may also return a component form whose string representation does not match configured keys such as HTTP/www1.server.com@EXAMPLE.COM. The docs and managers use full SPNs with realm, so lookup may silently miss and fall back or fail.

Please implement canonical extraction of the service principal from the unencrypted ticket fields, including realm, in the same format users configure, e.g. HTTP/www.example.com@EXAMPLE.COM. Also check whether PrincipalName#getName() includes name type/escaping/components correctly in this library version; if not, build the value from name components plus ticket realm explicitly. Add tests for SPNs with realm and host-based service names. Avoid claiming the extraction is from the encrypted part; it is from ticket metadata outside the encrypted EncTicketPart.

2. Fix hybrid fallback: docs promise fallback, but init currently prevents it

The docs describe “multi-principal with fallback” by setting both multiPrincipalManager and servicePrincipal/keyTabLocation. But SunJaasKerberosTicketValidator.afterPropertiesSet() returns immediately whenever multiPrincipalManager != null, so the default spnegoClient is never initialized. Later validateTicket() only falls back if spnegoClient != null, so documented fallback cannot work. Same pattern exists in Tomcat: SpnegoAuthenticator.initInternal() returns in multi-principal mode and never initializes the single-principal fallback client.

Please support three modes explicitly:

  • single-principal only: current behavior unchanged;
  • multi-principal only: require at least one configured SPN and fail when token SPN is unknown/unextractable;
  • hybrid multi-principal + default fallback: initialize the default SpnegoClient when servicePrincipal + keytab/password are also configured, and use it only as documented fallback.

Make fallback behavior explicit and consistent between Spring validator and Tomcat authenticator. Add tests proving that afterPropertiesSet() / initInternal() initializes fallback when both configs exist, and that no fallback is available in pure multi-principal mode.

3. Fix error handling: do not hide SPN parsing failures as normal fallback unless a default is intentionally configured

Currently validateTicket() catches Kerb4JException from SPN extraction and logs “using default principal”. In pure multi-principal mode it then throws only if spnegoClient == null; in hybrid it silently accepts fallback. That may be OK only when documented, but it should be deliberate and well-tested because malformed tokens and unsupported token shapes can become unexpected fallback to a broader default principal.

Please make the policy clear in code and docs: fallback is only used when a default principal is configured; otherwise reject with BadCredentialsException / 401. Log unknown SPN and extraction failure differently. Prefer debug for normal unknown-SPN fallback only when fallback exists, warn/debug-with-context for malformed/unparseable token depending existing project style. Do not leak sensitive token contents.

4. Improve MultiPrincipalManager design and naming

getSpnegoClientForSPN uses SPN capitalization, while Java style in the rest of the project likely prefers normal camelCase acronyms (getSpnegoClientForSpn). Since this is new API, please consider renaming before release, or at least provide canonical camelCase methods and deprecate/avoid the all-caps variant if you want compatibility within the branch.

Also consider whether returning null is the best contract. If you keep null, document it clearly. Defensive checks should reject null/blank principal names and null resources/paths in concrete managers with meaningful IllegalArgumentExceptions.

5. Fix resource/path handling in SimpleMultiPrincipalManager

The current code converts Resource to URL.toExternalForm() and strips file: manually. This is fragile: spaces and special characters stay URL-encoded, Windows paths may be mangled, and non-file resources are unclear. Existing Kerb4j/Spring code may already have a pattern for resolving Resource keytabs.

Please use a robust conversion, preferably resource.getFile().getAbsolutePath() for file resources, or match the existing SunJaasKerberosTicketValidator keytab resolution behavior. If non-file resources are unsupported, fail fast with a clear message. Add a unit test with a temp keytab path containing a space to prevent URL-encoding regressions.

6. Fix tests: current tests are mostly placeholders and do not test the feature

Several added tests only assert method existence or empty-manager behavior, and comments say “we can’t test with real tokens”. That is not enough for a feature that selects keytabs based on token SPN.

Please replace/augment these tests with meaningful unit tests that do not require a real KDC:

  • test SpnegoKerberosMechToken.getServerPrincipalName() using a constructed or fixture AP-REQ/SPNEGO token, or add a small parser-level fixture copied from existing test data if available;
  • test Spring validator client selection using a fake/stub MultiPrincipalManager and mocked/fake SpnegoClient/context where possible, or extract the client-selection logic into a package-private method that can be unit-tested without JAAS;
  • test unknown SPN with no fallback rejects;
  • test unknown SPN with fallback selects default;
  • test SPN extraction failure with no fallback rejects;
  • test SPN extraction failure with fallback selects default only if that is the intended documented policy;
  • test Tomcat authenticator behavior similarly at least at selection/init level;
  • remove redundant tests that only check method signatures or empty arrays.

Please also fix weak assertions like assertTrue(array.length == 0) to assertEquals(0, array.length) and avoid swallowing exceptions in tests. The current SpnegoInitTokenMultiPrincipalTest catches exceptions around assertThrows, which can mask test failures.

7. Verify module boundaries and dependencies

The PR moves SimpleMultiPrincipalManager to kerb4j-server-spring-security-core and imports it from WebFlux tests/docs. Please confirm the WebFlux module has a real compile/runtime dependency on that core module, not just test visibility. Ensure no servlet-only dependencies leak into WebFlux through the shared manager.

8. Fix documentation accuracy

Update MULTI_PRINCIPAL_SUPPORT.md, WebFlux README, and CHANGES after code changes:

  • show exact import packages for SimpleMultiPrincipalManager, MultiPrincipalManager, and SunJaasKerberosTicketValidator;
  • document whether fallback is enabled, disabled, or risky, and how to configure it explicitly;
  • document exact SPN matching/canonicalization, including realm requirement and case sensitivity;
  • remove or correct the Tomcat XML example if multiPrincipalManager="#{multiPrincipalManager}" is not actually supported by Tomcat Valve property injection. Standard Tomcat XML typically sets simple bean properties, not Spring-style bean references;
  • state limitations: keytabs must be local files if that remains true;
  • remove marketing-like claims such as “No performance impact” unless measured.

9. Code style / maintainability

Please align with existing project style: license years, import ordering, logging style, exception types, no trailing whitespace, final where customary, no “method exists” tests, no unrelated docs noise. Add newline at EOF to all new files.

10. Run the right validation

After fixes, run the relevant Maven module tests and the full test suite if practical. In the PR comment, report the exact commands and results. If any tests cannot run because of Copilot firewall/network restrictions, say exactly which ones and why, and still make sure the code compiles locally in the available environment.

Main acceptance criteria: multi-principal selection uses the same canonical SPN string that users configure; documented hybrid fallback really works; pure multi-principal mode fails closed; tests cover real selection/fallback behavior rather than method existence; documentation no longer promises unsupported Tomcat/Spring behavior.

…back, and tests

- SpnegoKerberosMechToken.getServerPrincipalName(): build canonical SPN as
  sname.getName() + "@" + ticket.getRealm() so realm is always included
  (e.g. HTTP/neo.example.org@EXAMPLE.ORG), matching how users configure SPNs

- MultiPrincipalManager: rename all-caps SPN methods to camelCase
  (getSpnegoClientForSpn / hasPrincipalForSpn / getConfiguredSpns);
  document null-return contract; mandate null/blank rejection in impls

- SimpleMultiPrincipalManager: use resource.getFile().getAbsolutePath()
  instead of URL.toExternalForm()+strip to avoid URL-encoded spaces in paths;
  add null/blank validation with IllegalArgumentException; fail fast for
  non-file resources with clear message

- TomcatMultiPrincipalManager: same API rename + null/blank input validation

- SunJaasKerberosTicketValidator.afterPropertiesSet(): remove early return in
  multi-principal mode so hybrid mode (manager + servicePrincipal+keytab)
  initializes both; require at least one of the two modes to be configured

- SpnegoAuthenticator.initInternal(): same hybrid fix for Tomcat

- Replace placeholder tests with real assertions:
  * SpnegoInitTokenMultiPrincipalTest: parse real SPNEGO token, assert
    HTTP/neo.example.org@EXAMPLE.ORG; test malformed/empty token paths
  * SimpleMultiPrincipalManagerTest: null/blank validation, non-file resource,
    space-in-path regression, null SPN lookup safety
  * MultiPrincipalSimpleTest: mode detection (pure, empty, neither, hybrid)
    without a live KDC; stub MultiPrincipalManager
  * MultiPrincipalConfigurationExample: fix weak assertTrue(array.length==0),
    add hybrid mode test
  * MultiPrincipalParityTest: add API coverage for reactive stack

- Update MULTI_PRINCIPAL_SUPPORT.md and WebFlux README: accurate import paths,
  mode documentation (single/pure-multi/hybrid), SPN format/case-sensitivity,
  keytab file limitation, remove Tomcat XML bean-ref example, remove
  unqualified no-performance-impact claim
Copy link
Copy Markdown
Author

Copilot AI commented May 28, 2026

@copilot please fix the issues below in this PR. Treat this as a security/authentication change; do not just make tests green. Please inspect the current code and adjust the design so servlet, WebFlux...

All 10 points addressed in commit $(git -C /tmp/workspace/bedrin/kerb4j rev-parse --short HEAD). Here's a summary of what changed:

1. SPN extraction fixedSpnegoKerberosMechToken.getServerPrincipalName() now builds the canonical SPN as sname.getName() + "@" + ticket.getRealm() (e.g. HTTP/neo.example.org@EXAMPLE.ORG). A real SPNEGO token test confirms the exact output.

2. Hybrid fallback fixedSunJaasKerberosTicketValidator.afterPropertiesSet() no longer returns early when multiPrincipalManager != null. It now initializes the default spnegoClient whenever servicePrincipal + keytab/password are also set. Same fix applied to SpnegoAuthenticator.initInternal(). Three explicit modes are now supported: single-principal, pure multi-principal (fail-closed for unknown SPNs), and hybrid (default client as documented fallback).

3. Error handling clarified — In pure multi mode, unknown/unextractable SPNs throw BadCredentialsException immediately. The default client is only used as fallback when it was explicitly initialized (hybrid mode). Log messages distinguish "unknown SPN → fallback" from "extraction failure → fallback".

4. API renamedMultiPrincipalManager methods renamed to camelCase: getSpnegoClientForSpn, hasPrincipalForSpn, getConfiguredSpns. Null-return contract documented. Implementations updated.

5. Resource path fixedSimpleMultiPrincipalManager.addPrincipal() uses resource.getFile().getAbsolutePath() instead of URL.toExternalForm() + "file:" stripping. Non-file resources (e.g. classpath entries inside JARs) now fail fast with a clear IllegalArgumentException. Null/blank inputs also validated.

6. Tests replaced — Placeholder "method exists" tests replaced with real behavioural tests:

  • SpnegoInitTokenMultiPrincipalTest: parses a real SPNEGO token and asserts HTTP/neo.example.org@EXAMPLE.ORG; tests malformed/empty token error paths.
  • SimpleMultiPrincipalManagerTest: validates null/blank inputs, non-file resource rejection, and space-in-path regression.
  • MultiPrincipalSimpleTest: tests all three modes (pure, empty-manager, neither, hybrid) with a stub MultiPrincipalManager — no live KDC needed.
  • Fixed assertTrue(array.length == 0)assertEquals(0, …) throughout.

7. Module boundaries — WebFlux module already has a compile dependency on kerb4j-server-spring-security-core (confirmed in pom.xml). SimpleMultiPrincipalManager is in that module with no servlet-only dependencies.

8. Documentation — Rewrote MULTI_PRINCIPAL_SUPPORT.md: exact import packages, three-mode explanation, fallback risk note, SPN case-sensitivity, keytab-must-be-local-file limitation, removed the broken Tomcat XML bean-ref example, removed the "No performance impact" claim. Updated WebFlux README similarly.

9 & 10. Code style aligned with project conventions. Full test suite: BUILD SUCCESS, 0 CodeQL alerts.

Copilot finished work on behalf of bedrin May 28, 2026 08:19
Copilot AI requested a review from bedrin May 28, 2026 08:19
bedrin added 5 commits June 1, 2026 10:48
Signed-off-by: Dmitry Bedrin <dmitry.bedrin@gmail.com>
Signed-off-by: Dmitry Bedrin <dmitry.bedrin@gmail.com>
…ion, author tags, and testing practices

Signed-off-by: Dmitry Bedrin <dmitry.bedrin@gmail.com>
…ethod return types

Signed-off-by: Dmitry Bedrin <dmitry.bedrin@gmail.com>
…d parameters and return types

Signed-off-by: Dmitry Bedrin <dmitry.bedrin@gmail.com>
@bedrin
Copy link
Copy Markdown
Owner

bedrin commented Jun 1, 2026

@copilot please continue work on this PR and address the remaining design, test, nullability, and documentation issues below. Treat this PR as an authentication/security-sensitive change, not just an API addition.

First: update Copilot repository instructions

Please add or update .github/copilot-instructions.md in this PR with these repository rules:

  • Never add license information to Markdown files or to code comments.
  • Never add yourself to @author Javadoc tags.
  • Use JSpecify annotations such as @NonNull and @Nullable in new code you write.
  • Where relevant, create integration tests using com.kerb4j.KerberosSecurityTestcase for setting up a mock Kerberos server. See com.kerb4j.client.SpnegoHttpURLConnectionTests as an example.

Do not add any license header to .github/copilot-instructions.md.

Main design change: fallback belongs inside MultiPrincipalManager

The current PR lets SunJaasKerberosTicketValidator and Tomcat code partially manage fallback to the default SpnegoClient. That is the wrong abstraction.

Please redesign this so the default/fallback client is part of the MultiPrincipalManager abstraction.

The validator/authenticator should not contain complicated logic like:

  • try to extract SPN;
  • ask manager for exact SPN;
  • if missing, maybe use spnegoClient;
  • if extraction failed, maybe use spnegoClient.

Instead, the manager should own principal resolution policy:

  • exact SPN match returns the matching SpnegoClient;
  • unknown SPN may return the manager’s configured default/fallback SpnegoClient;
  • malformed/unextractable SPN should be handled according to explicit manager policy;
  • no match and no fallback should fail closed.

Suggested direction:

public interface MultiPrincipalManager {
    @Nullable
    SpnegoClient getSpnegoClientForSpn(@Nullable String spn);

    @NonNull
    Collection<String> getConfiguredSpns();

    boolean hasPrincipalForSpn(@NonNull String spn);

    @Nullable
    SpnegoClient getDefaultSpnegoClient();
}

or use Optional<SpnegoClient> if that is more consistent with project style. Prefer normal Java acronym casing: Spn, not SPN, for new methods.

Keep compatibility aliases only if necessary, but new code should use the canonical camelCase names.

Concrete manager implementations should support:

  • adding named SPN principals;
  • optionally configuring a default/fallback principal;
  • checking whether fallback is configured;
  • returning fallback from getSpnegoClientForSpn(...) only when explicitly configured.

This applies to both:

  • SimpleMultiPrincipalManager;
  • TomcatMultiPrincipalManager.

Spring validator behavior

Refactor SunJaasKerberosTicketValidator so that:

  1. Single-principal mode remains backward compatible.
  2. Multi-principal mode delegates principal selection to MultiPrincipalManager.
  3. Hybrid/fallback behavior is configured through MultiPrincipalManager, not through validator-local fallback logic.
  4. Pure multi-principal mode fails closed if:
    • token SPN is unknown;
    • token SPN cannot be extracted;
    • manager has no fallback.
  5. If fallback is configured in the manager, fallback behavior is explicit and covered by tests.

Avoid silent broad fallback in malformed-token cases unless that behavior is deliberately documented and tested.

Tomcat authenticator behavior

Apply the same principle to SpnegoAuthenticator:

  • no duplicated fallback logic in the authenticator;
  • principal resolution delegated to TomcatMultiPrincipalManager;
  • fail closed when no matching SPN and no explicit fallback;
  • consistent behavior with Spring validator.

Also verify whether the Tomcat XML documentation example is actually valid. A Spring-style reference such as:

multiPrincipalManager="#{multiPrincipalManager}"

is probably not valid standard Tomcat Valve property injection. Remove or replace it unless the project has real support for that wiring.

Nullability: add JSpecify annotations everywhere in new code

New code in this PR still lacks nullability coverage.

Please inspect all new/modified public and package-private APIs and add JSpecify annotations consistently:

  • @NonNull for non-null parameters, return values, fields, and arrays/collections where relevant;
  • @Nullable where null is part of the contract;
  • package-level defaults if the project already uses them;
  • add the JSpecify dependency to the relevant modules if it is not already available.

At minimum cover:

  • MultiPrincipalManager;
  • SimpleMultiPrincipalManager;
  • TomcatMultiPrincipalManager;
  • new SPNEGO token extraction methods;
  • validator/authenticator setters and helper methods;
  • any new test helper/stub classes.

Do not annotate randomly. Make nullability express the real API contract.

Examples:

@Nullable
SpnegoClient getSpnegoClientForSpn(@Nullable String spn);

void addPrincipal(@NonNull String principal, @NonNull Resource keyTabLocation);

@NonNull
Collection<String> getConfiguredSpns();

Also validate inputs:

  • reject null/blank principal names;
  • reject null keytab resources/paths;
  • provide clear exceptions.

SPN extraction must be canonical and tested

Please verify and fix SpnegoKerberosMechToken.getServerPrincipalName().

The current approach appears to return only:

getApRequest().getTicket().getSname().getName()

That is likely incomplete or non-canonical, because configured principals use full SPNs like:

HTTP/www.example.com@EXAMPLE.COM

Please ensure extraction returns the same canonical format the manager uses for lookup, including the realm. If PrincipalName#getName() does not include realm, build the SPN explicitly from ticket sname components plus ticket realm.

Add tests for at least:

  • HTTP/host.example.com@EXAMPLE.COM;
  • service names with realm;
  • case sensitivity expectations;
  • unknown SPN lookup;
  • malformed token / extraction failure.

Correct docs/comments that say “encrypted part” if the SPN is extracted from unencrypted ticket metadata.

Keytab resource/path handling

SimpleMultiPrincipalManager currently converts Spring Resource to URL string and strips file: manually. This is fragile:

  • URL-encoded spaces remain encoded;
  • Windows paths may break;
  • non-file resources are unclear;
  • behavior may differ from SunJaasKerberosTicketValidator.

Please replace this with robust keytab resolution:

  • preferably match existing validator behavior;
  • for file resources, use resource.getFile().getAbsolutePath();
  • if non-file resources are unsupported, fail fast with a clear message.

Add a regression test using a temporary keytab path containing a space.

Integration tests are required

The PR still lacks real integration tests.

Please add at least one integration test using com.kerb4j.KerberosSecurityTestcase, following com.kerb4j.client.SpnegoHttpURLConnectionTests as an example.

Target coverage:

  1. mock Kerberos server issues a token for SPN A;
  2. server has SPN A and SPN B configured;
  3. validator/authenticator selects SPN A’s keytab/client;
  4. token for unknown SPN fails in pure multi-principal mode;
  5. token for unknown SPN succeeds only when default fallback is explicitly configured in the manager;
  6. backward-compatible single-principal setup still works.

If Tomcat integration is hard, cover the Spring validator integration first and add unit-level selection tests for Tomcat manager/authenticator.

Replace weak placeholder tests

Please remove or rewrite tests that only prove methods exist.

Bad patterns currently present:

  • creating invalid dummy token only to assert something throws;
  • catching exceptions around assertThrows;
  • asserting empty manager behavior without exercising addPrincipal;
  • comments like “we can’t test with real tokens” while the repo has KerberosSecurityTestcase.

Replace them with meaningful tests:

  • manager stores and resolves exact SPNs;
  • manager resolves fallback;
  • manager fails closed without fallback;
  • validator delegates to manager;
  • SPN extraction returns canonical full SPN;
  • path handling works with spaces;
  • single-principal backward compatibility still works.

Use stronger assertions:

assertEquals(0, configuredSpns.size());

instead of:

assertTrue(configuredSpns.length == 0);

Do not swallow exceptions in tests.

Documentation updates

Update MULTI_PRINCIPAL_SUPPORT.md, WebFlux README, and CHANGES.md after the code is corrected.

Docs must explain:

  • exact SPN canonical format, including realm;
  • whether matching is case-sensitive;
  • how to configure fallback through MultiPrincipalManager;
  • pure multi-principal mode fails closed;
  • fallback only happens when explicitly configured;
  • keytabs must be local files if that remains true;
  • exact imports/packages for new classes;
  • WebFlux parity;
  • Tomcat limitations and valid configuration approach.

Remove unsupported or overconfident claims such as:

  • “No performance impact” unless measured;
  • unsupported Tomcat XML bean-reference syntax;
  • vague “extracts from encrypted part” wording.

Code style cleanup

Please align with existing project style:

  • no new license headers in new files;
  • no @author Copilot or similar;
  • no trailing whitespace;
  • newline at EOF;
  • import ordering consistent with the project;
  • avoid duplicate manager logic across servlet/reactive/Tomcat;
  • prefer small helper methods for SPN extraction and client resolution;
  • keep security-sensitive fallback behavior easy to read.

Validation

Run the relevant module tests and full build if practical.

At minimum report exact commands and results, for example:

mvn -pl kerb4j-server/kerb4j-server-common test
mvn -pl kerb4j-server/kerb4j-server-spring-security-core test
mvn -pl kerb4j-server/kerb4j-server-spring-security test
mvn -pl kerb4j-server/kerb4j-server-spring-webflux test
mvn -pl kerb4j-server/kerb4j-server-tomcat test
mvn test

Adjust module names if needed.

If any command cannot run because of Copilot firewall/network restrictions, clearly say which command failed and why.

Acceptance criteria

This PR is ready only when:

  • MultiPrincipalManager owns default/fallback resolution;
  • validators/authenticators delegate selection to the manager;
  • fallback is explicit, documented, and tested;
  • pure multi-principal mode fails closed;
  • canonical SPN extraction includes realm and matches configured keys;
  • new code has JSpecify nullability annotations;
  • real integration tests using KerberosSecurityTestcase exist;
  • placeholder “method exists” tests are removed or replaced;
  • docs describe only behavior that is actually supported;
  • Copilot instructions are updated with the repository rules above.

Copy link
Copy Markdown
Author

Copilot AI commented Jun 1, 2026

@copilot please continue work on this PR and address the remaining design, test, nullability, and documentation issues below. Treat this PR as an authentication/security-sensitive change, not just an ...

Implemented in 026ad5d.

  • Updated .github/copilot-instructions.md with the requested repository rules (no license header added).
  • Refactored fallback ownership into MultiPrincipalManager (getSpnegoClientForSpn(@Nullable String), getConfiguredSpns(), getDefaultSpnegoClient()), and updated SimpleMultiPrincipalManager/TomcatMultiPrincipalManager to support explicit default principals via manager APIs.
  • Simplified SunJaasKerberosTicketValidator and Tomcat SpnegoAuthenticator to delegate SPN resolution to the manager and fail closed when no match/fallback is configured.
  • Kept single-principal mode backward compatible.
  • Hardened canonical SPN extraction in SpnegoKerberosMechToken by building from principal components plus ticket realm.
  • Replaced weak placeholder tests with meaningful manager/unit coverage, added Tomcat manager tests, and added Kerberos integration tests using KerberosSecurityTestcase for multi-principal selection/fail-closed/fallback/single-principal compatibility.
  • Updated MULTI_PRINCIPAL_SUPPORT.md, WebFlux README, and CHANGES.md to document the manager-owned fallback model, canonical SPN format, and current supported behavior.

Validation:

  • mvn -pl kerb4j-server/kerb4j-server-common,kerb4j-server/kerb4j-server-spring-security-core,kerb4j-server/kerb4j-server-spring-security,kerb4j-server/kerb4j-server-spring-webflux,kerb4j-server/kerb4j-server-tomcat -am test -DskipITs
  • mvn test -DskipITs
  • codeql_checker: 0 alerts

Copilot AI changed the title Add multi-principal SPNEGO support with servlet/reactive Spring parity Add multi-principal SPNEGO support with manager-owned fallback and servlet/reactive/Tomcat parity Jun 1, 2026
Copilot finished work on behalf of bedrin June 1, 2026 12:25
…correct package and simplify keytab handling

Signed-off-by: Dmitry Bedrin <dmitry.bedrin@gmail.com>
@bedrin
Copy link
Copy Markdown
Owner

bedrin commented Jun 2, 2026

@copilot please fix compilation errors and tests if there're any errors. for example:

'''

Error: /home/runner/work/kerb4j/kerb4j/kerb4j-server/kerb4j-server-spring-security/src/test/java/com/kerb4j/server/spring/integration/MultiPrincipalKerberosIntegrationTest.java:[5,25] cannot find symbol
'''

Copy link
Copy Markdown
Author

Copilot AI commented Jun 2, 2026

@copilot please fix compilation errors and tests if there're any errors. for example:

'''...

Fixed in commit 0876ba4.

I investigated the failing CI run and resolved the compile errors by restoring SimpleMultiPrincipalManager (and its tests), correcting imports to com.kerb4j.server.spring.SimpleMultiPrincipalManager, and updating integration test calls to pass FileSystemResource where required.

Validation run:

  • mvn -pl kerb4j-server/kerb4j-server-spring-security,kerb4j-server/kerb4j-server-spring-webflux -am test -DskipITs
  • mvn -pl kerb4j-server/kerb4j-server-common,kerb4j-server/kerb4j-server-spring-security-core,kerb4j-server/kerb4j-server-spring-security,kerb4j-server/kerb4j-server-spring-webflux,kerb4j-server/kerb4j-server-tomcat -am test -DskipITs
  • codeql_checker: 0 alerts

Copilot finished work on behalf of bedrin June 2, 2026 09:23
bedrin added 3 commits June 7, 2026 10:31
…anaging service principals from keytab files

Signed-off-by: Dmitry Bedrin <dmitry.bedrin@gmail.com>
…md for clarity

Signed-off-by: Dmitry Bedrin <dmitry.bedrin@gmail.com>
@bedrin bedrin merged commit 399ef41 into master Jun 7, 2026
11 checks passed
@bedrin bedrin deleted the copilot/fix-67 branch June 7, 2026 09:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants