-
Notifications
You must be signed in to change notification settings - Fork 2.2k
Fix: Add tenant ID validation in UserPrincipalManager
#49631
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
rujche
merged 15 commits into
main
from
rujche/main/fix-icm-issue-about-UserPrincipalManager
Jun 29, 2026
Merged
Changes from all commits
Commits
Show all changes
15 commits
Select commit
Hold shift + click to select a range
02af357
Fix: Add tenant ID validation to prevent cross-tenant authentication …
rujche 7861f1d
docs: Update CHANGELOG.md with tenant ID validation fix
rujche 17c3438
fix: Address Copilot review comments on tenant ID validation
rujche 1ebe0af
fix: Address security and naming review comments
rujche 37b78bf
fix: Wrap long line to comply with 120-character Checkstyle LineLengt…
rujche dbbf516
refactor: Add package-private constructor to avoid reflective mutatio…
rujche c0a60cb
fix: Fix import order to put com.azure imports before third-party imp…
rujche 51a582c
feat: Add logging for multi-tenant configuration tenant ID verificati…
rujche 8f3a5de
refactor: Extract JWT claims verification into separate AadJwtClaimsV…
rujche 25bd969
fix: Remove unused imports from UserPrincipalManager and AadJwtClaims…
rujche bd022a2
refactor: Address Copilot review comments for code quality
rujche 1bca4de
fix: Null-safe audience validation and remove trailing whitespace
rujche ff65623
fix: Remove trailing whitespace and add null-safe audience handling i…
rujche 3c9361f
fix: Make AadJwtClaimsVerifier constructor package-private and clarif…
rujche cf7df6b
refactor: Remove package-private UserPrincipalManager constructor and…
rujche File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
34 changes: 34 additions & 0 deletions
34
...va/com/azure/spring/cloud/autoconfigure/implementation/aad/filter/AadIssuerValidator.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| // Copyright (c) Microsoft Corporation. All rights reserved. | ||
| // Licensed under the MIT License. | ||
|
|
||
| package com.azure.spring.cloud.autoconfigure.implementation.aad.filter; | ||
|
|
||
| /** | ||
| * Utility class for validating AAD JWT issuer claims. | ||
| * Provides centralized issuer validation logic to prevent duplication and ensure consistency. | ||
| */ | ||
| final class AadIssuerValidator { | ||
|
|
||
| private static final String LOGIN_MICROSOFT_ONLINE_ISSUER = "https://login.microsoftonline.com/"; | ||
| private static final String STS_WINDOWS_ISSUER = "https://sts.windows.net/"; | ||
| private static final String STS_CHINA_CLOUD_API_ISSUER = "https://sts.chinacloudapi.cn/"; | ||
|
|
||
| private AadIssuerValidator() { | ||
| // Utility class, not instantiable | ||
| } | ||
|
|
||
| /** | ||
| * Checks if the given issuer is a valid AAD issuer. | ||
| * | ||
| * @param issuer the issuer string to validate | ||
| * @return true if the issuer is a valid AAD issuer, false otherwise | ||
| */ | ||
| static boolean isValidAadIssuer(String issuer) { | ||
| if (issuer == null) { | ||
| return false; | ||
| } | ||
| return issuer.startsWith(LOGIN_MICROSOFT_ONLINE_ISSUER) | ||
| || issuer.startsWith(STS_WINDOWS_ISSUER) | ||
| || issuer.startsWith(STS_CHINA_CLOUD_API_ISSUER); | ||
| } | ||
| } |
105 changes: 105 additions & 0 deletions
105
.../com/azure/spring/cloud/autoconfigure/implementation/aad/filter/AadJwtClaimsVerifier.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,105 @@ | ||
| // Copyright (c) Microsoft Corporation. All rights reserved. | ||
| // Licensed under the MIT License. | ||
|
|
||
| package com.azure.spring.cloud.autoconfigure.implementation.aad.filter; | ||
|
|
||
| import com.azure.spring.cloud.autoconfigure.implementation.aad.configuration.properties.AadAuthenticationProperties; | ||
| import com.azure.spring.cloud.autoconfigure.implementation.aad.security.constants.AadJwtClaimNames; | ||
| import com.nimbusds.jwt.JWTClaimsSet; | ||
| import com.nimbusds.jwt.proc.BadJWTException; | ||
| import com.nimbusds.jwt.proc.DefaultJWTClaimsVerifier; | ||
| import org.slf4j.Logger; | ||
| import org.slf4j.LoggerFactory; | ||
| import org.springframework.util.StringUtils; | ||
|
|
||
| /** | ||
| * Custom JWT claims verifier that adds tenant ID validation for AAD authentication. | ||
| * Extends {@link DefaultJWTClaimsVerifier} to validate that JWT tokens contain the expected tenant ID (tid claim). | ||
| * Package-private implementation detail of the filter package. | ||
| */ | ||
| final class AadJwtClaimsVerifier extends DefaultJWTClaimsVerifier<com.nimbusds.jose.proc.SecurityContext> { | ||
|
|
||
| private static final Logger LOGGER = LoggerFactory.getLogger(AadJwtClaimsVerifier.class); | ||
|
|
||
| private final AadAuthenticationProperties aadAuthenticationProperties; | ||
| private final boolean explicitAudienceCheck; | ||
| private final java.util.Set<String> validAudiences; | ||
|
|
||
| /** | ||
| * Creates a new AadJwtClaimsVerifier. | ||
| * | ||
| * @param aadAuthenticationProperties AAD authentication properties (may be null) | ||
| * @param explicitAudienceCheck whether to explicitly check the audience | ||
| * @param validAudiences valid audience values for explicit audience check | ||
| */ | ||
| AadJwtClaimsVerifier(AadAuthenticationProperties aadAuthenticationProperties, | ||
| boolean explicitAudienceCheck, | ||
| java.util.Set<String> validAudiences) { | ||
| super(null, null); | ||
| this.aadAuthenticationProperties = aadAuthenticationProperties; | ||
| this.explicitAudienceCheck = explicitAudienceCheck; | ||
| this.validAudiences = validAudiences != null ? validAudiences : java.util.Collections.emptySet(); | ||
| } | ||
|
|
||
| @Override | ||
| public void verify(JWTClaimsSet claimsSet, com.nimbusds.jose.proc.SecurityContext ctx) throws BadJWTException { | ||
| super.verify(claimsSet, ctx); | ||
|
|
||
| // Issuer validation | ||
| final String issuer = claimsSet.getIssuer(); | ||
| if (!AadIssuerValidator.isValidAadIssuer(issuer)) { | ||
| throw new BadJWTException("Invalid token issuer"); | ||
| } | ||
|
|
||
| // Audience validation | ||
| if (explicitAudienceCheck) { | ||
| java.util.List<String> audiences = claimsSet.getAudience(); | ||
| if (audiences == null || audiences.isEmpty()) { | ||
| throw new BadJWTException("Invalid token audience. No audience claim found in token."); | ||
| } | ||
| java.util.Optional<String> matchedAudience = audiences.stream() | ||
| .filter(validAudiences::contains) | ||
| .findFirst(); | ||
| if (matchedAudience.isPresent()) { | ||
| LOGGER.debug("Matched audience: [{}]", matchedAudience.get()); | ||
| } else { | ||
| throw new BadJWTException("Invalid token audience. Provided value " + audiences | ||
| + " does not match either the client-id or AppIdUri."); | ||
| } | ||
| } | ||
|
|
||
| // Tenant ID validation | ||
| if (aadAuthenticationProperties != null) { | ||
| String configuredTenantId = aadAuthenticationProperties.getProfile().getTenantId(); | ||
| if (StringUtils.hasText(configuredTenantId)) { | ||
| // Skip validation for multi-tenant values: common, organizations, consumers | ||
| String trimmedTenantId = configuredTenantId.trim().toLowerCase(java.util.Locale.ROOT); | ||
| if (!isMultiTenantValue(trimmedTenantId)) { | ||
| Object tidClaim = claimsSet.getClaim(AadJwtClaimNames.TID); | ||
| String tokenTid = tidClaim != null ? tidClaim.toString() : null; | ||
| String normalizedTokenTid = tokenTid != null | ||
| ? tokenTid.trim().toLowerCase(java.util.Locale.ROOT) | ||
| : null; | ||
| if (!trimmedTenantId.equals(normalizedTokenTid)) { | ||
| throw new BadJWTException("Invalid token tenant. Token tid claim '" + tokenTid | ||
| + "' does not match the configured tenant '" + configuredTenantId + "'."); | ||
| } | ||
| LOGGER.debug("Token tenant validated: [{}]", tokenTid); | ||
| } else { | ||
| LOGGER.debug("Tenant ID verification skipped: multi-tenant configuration detected [{}]", | ||
| configuredTenantId); | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Checks if the given tenant ID represents a multi-tenant configuration. | ||
| * Multi-tenant values (common, organizations, consumers) should skip tenant ID validation. | ||
| */ | ||
| private static boolean isMultiTenantValue(String normalizedTenantId) { | ||
| return "common".equals(normalizedTenantId) | ||
| || "organizations".equals(normalizedTenantId) | ||
| || "consumers".equals(normalizedTenantId); | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.