Add multi-principal SPNEGO support with manager-owned fallback and servlet/reactive/Tomcat parity#68
Conversation
|
@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. |
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 merge latest changes from master |
This includes GitHub Actions workflow updates and macOS hostname validation fixes.
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:
If you need me to access, download, or install something from one of these locations, you can either:
|
|
@copilot merge latest changes from master again |
There was a problem hiding this comment.
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.
| try { | ||
| SpnegoClient client = SpnegoClient.loginWithKeyTab(principal, keyTabLocation, true); | ||
| spnegoClients.put(principal, client); | ||
| } catch (Exception e) { | ||
| throw new RuntimeException("Failed to initialize principal: " + principal, e); |
There was a problem hiding this comment.
The error message should include the keytab location for better debugging. Consider changing to: 'Failed to initialize principal: ' + principal + ' with keytab: ' + keyTabLocation
| 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); |
| 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); |
There was a problem hiding this comment.
The error message should include the keytab location for better debugging. Consider changing to: 'Failed to initialize principal: ' + principal + ' with keytab: ' + keyTabPath
| 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); |
| 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); |
There was a problem hiding this comment.
The log level should be DEBUG instead of WARN since fallback to default principal is expected behavior, not a warning condition.
| LOG.warn("Failed to extract SPN from token, using default principal", e); | |
| LOG.debug("Failed to extract SPN from token, using default principal", e); |
| 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"); |
There was a problem hiding this comment.
Consider logging the exception details even at debug level for troubleshooting. The current implementation only logs the message but not the exception cause.
| 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); |
There was a problem hiding this comment.
Remove this file. It's neither related to this PR nor it makes any sense since it contains dummy hardcoded host and port
There was a problem hiding this comment.
Removed the krb5.conf file as requested. It was indeed unrelated to this PR and contained dummy hardcoded values. Commit: 16c314d
There was a problem hiding this comment.
This class isn't specific to Spring framework and should be in kerb4j-server-common module instead
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
Try to avoid code duplication with SunJaasKerberosTicketValidator.java
There was a problem hiding this comment.
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>
bedrin
left a comment
There was a problem hiding this comment.
@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
SpnegoClientwhenservicePrincipal+ 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
MultiPrincipalManagerand mocked/fakeSpnegoClient/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, andSunJaasKerberosTicketValidator; - 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
All 10 points addressed in commit 1. SPN extraction fixed — 2. Hybrid fallback fixed — 3. Error handling clarified — In pure multi mode, unknown/unextractable SPNs throw 4. API renamed — 5. Resource path fixed — 6. Tests replaced — Placeholder "method exists" tests replaced with real behavioural tests:
7. Module boundaries — WebFlux module already has a compile dependency on 8. Documentation — Rewrote 9 & 10. Code style aligned with project conventions. Full test suite: |
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>
|
@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 instructionsPlease add or update
Do not add any license header to Main design change: fallback belongs inside
|
Implemented in
Validation:
|
…correct package and simplify keytab handling Signed-off-by: Dmitry Bedrin <dmitry.bedrin@gmail.com>
|
@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 |
Fixed in commit 0876ba4. I investigated the failing CI run and resolved the compile errors by restoring Validation run:
|
…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>
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
masterupdates (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
getServerPrincipalName()returns the full canonical SPN including realm (e.g.HTTP/host.example.com@EXAMPLE.COM) from ticket metadata (snamecomponents + ticket realm). SPN matching is exact and case-sensitive.MultiPrincipalManagernow owns principal selection policy, including explicit default/fallback behavior.MultiPrincipalManager; unknown or unextractable SPNs fail closed (BadCredentialsException/ HTTP 401).MultiPrincipalManagerinterface (inkerb4j-server-common) withSimpleMultiPrincipalManager(Spring, inkerb4j-server-spring-security-core) andTomcatMultiPrincipalManager.Additional Alignment and Refactoring
masterchanges and resolved integration updates.SimpleMultiPrincipalManagerinkerb4j-server-spring-security-coreso both stacks share the same multi-principal building blocks.MultiPrincipalManagerinkerb4j-server-common.MultiPrincipalSunJaasKerberosTicketValidator(duplicate behavior already inSunJaasKerberosTicketValidator).kerb4j-server/kerb4j-server-spring-security/krb5.conf.MultiPrincipalManagerAPI to include fallback-aware contract:getSpnegoClientForSpn(@Nullable String spn)hasPrincipalForSpn(String spn)getConfiguredSpns()(collection-based)getDefaultSpnegoClient()SimpleMultiPrincipalManager#addDefaultPrincipal(...)TomcatMultiPrincipalManager#addDefaultPrincipal(...)SunJaasKerberosTicketValidatorand TomcatSpnegoAuthenticatorto delegate SPN/client resolution to the manager (no duplicated local fallback policy).SimpleMultiPrincipalManagerusesresource.getFile().getAbsolutePath()for robust keytab path resolution (avoids URL-encoded spaces); non-file resources fail fast with clearIllegalArgumentException.IllegalArgumentExceptionin concrete managers.Test Coverage and Validation
SpnegoInitTokenMultiPrincipalTest: parses a real SPNEGO token fixture and assertsHTTP/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 usingKerberosSecurityTestcasefor 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.Documentation and Release Notes
.github/copilot-instructions.mdwith repository rules for markdown/license handling, author tags, JSpecify usage, and Kerberos integration test guidance.MULTI_PRINCIPAL_SUPPORT.md: manager-owned fallback model, canonical SPN format/case-sensitivity, fail-closed behavior, keytab local-file limitation, and Tomcat configuration limitations.CHANGES.mdto 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.