Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions sdk/spring/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ This section includes changes in `spring-cloud-azure-autoconfigure` module.

#### Bugs Fixed

- Fixed the AAD and B2C OpenID Connect login (`oauth2Login`) ID token decoders not validating the `iss` (issuer) and `aud` (audience) claims. `AadOidcIdTokenDecoderFactory` and `AadB2cOidcIdTokenDecoderFactory` now validate the standard OIDC ID token claims (audience, expiry, issued-at and subject) and the issuer. For single tenant applications the issuer must belong to the configured tenant, and for multi-tenant applications (the `common`, `organizations` or `consumers` endpoints) the issuer must be a trusted Microsoft identity platform issuer consistent with the token's own `tid` claim. This prevents users from unauthorized tenants from signing in to multi-tenant applications that rely on the issuer/tenant claim for tenant restriction ([#49423](https://github.com/Azure/azure-sdk-for-java/pull/49423)).
- Fixed the missing bean name in `@ConditionalOnMissingBean` for `LettuceClientConfigurationBuilderCustomizer` ([#49290](https://github.com/Azure/azure-sdk-for-java/issues/49290)).
- Fixed the AAD and B2C resource server JWT decoder not honoring the `spring.cloud.azure.active-directory.jwt-connect-timeout`, `spring.cloud.azure.active-directory.jwt-read-timeout`, `spring.cloud.azure.active-directory.b2c.jwt-connect-timeout`, and `spring.cloud.azure.active-directory.b2c.jwt-read-timeout` configuration properties ([#49329](https://github.com/Azure/azure-sdk-for-java/pull/49329)).
- Fixed AAD resource server JWK retrieval not honoring the `spring.cloud.azure.active-directory.jwk-set-cache-lifespan` and `spring.cloud.azure.active-directory.jwk-set-cache-refresh-time` configuration properties ([#42159](https://github.com/Azure/azure-sdk-for-java/issues/42159)).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,8 @@ JwtDecoderFactory<ClientRegistration> azureAdJwtDecoderFactory(AadAuthentication
AadProfileProperties profile = properties.getProfile();
AadAuthorizationServerEndpoints endpoints = new AadAuthorizationServerEndpoints(
profile.getEnvironment().getActiveDirectoryEndpoint(), profile.getTenantId());
return new AadOidcIdTokenDecoderFactory(endpoints.getJwkSetEndpoint(), createRestTemplate(restTemplateBuilder));
return new AadOidcIdTokenDecoderFactory(endpoints.getJwkSetEndpoint(),
createRestTemplate(restTemplateBuilder), properties);
}

private void clientCredentialsGrantBuilderAccessTokenResponseClientCustomizer(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,40 +3,140 @@

package com.azure.spring.cloud.autoconfigure.implementation.aad.security;

import com.azure.spring.cloud.autoconfigure.implementation.aad.configuration.properties.AadAuthenticationProperties;
import com.azure.spring.cloud.autoconfigure.implementation.aad.security.constants.AadJwtClaimNames;
import com.azure.spring.cloud.autoconfigure.implementation.aad.security.jwt.AadJwtIssuerValidator;
import com.azure.spring.cloud.autoconfigure.implementation.aad.security.jwt.AadTrustedIssuerRepository;
import org.springframework.security.oauth2.client.oidc.authentication.OidcIdTokenValidator;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.core.DelegatingOAuth2TokenValidator;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult;
import org.springframework.security.oauth2.core.oidc.OidcIdToken;
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.JwtDecoderFactory;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.springframework.web.client.RestOperations;

import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
* A factory that provides a {@link JwtDecoder} used for {@link OidcIdToken} signature verification.
* A factory that provides a {@link JwtDecoder} used for {@link OidcIdToken} signature verification and claim validation.
* <p>
* Besides verifying the token signature, the decoder validates the standard OpenID Connect ID token claims (audience,
* expiry, issued-at and subject) via {@link OidcIdTokenValidator} and additionally validates the {@code iss} (issuer)
* claim:
* <ul>
* <li>For a single tenant application, the issuer must belong to the configured tenant.</li>
* <li>For a multi-tenant application (the {@code common}, {@code organizations} or {@code consumers} endpoints),
* the issuer must be a trusted Microsoft identity platform issuer that is consistent with the token's own
* {@code tid} (tenant id) claim, so that the relying application can safely use the {@code iss}/{@code tid} claims
* to restrict which tenants are allowed to sign in.</li>
* </ul>
*
* @see <a href="https://learn.microsoft.com/azure/active-directory/develop/id-tokens">azure-active-directory id-tokens</a>
*/
public class AadOidcIdTokenDecoderFactory implements JwtDecoderFactory<ClientRegistration> {

private final JwtDecoder jwtDecoder;
private final String jwkSetUri;
private final RestOperations restOperations;
private final AadAuthenticationProperties properties;
private final Map<String, JwtDecoder> jwtDecoders = new ConcurrentHashMap<>();

/**
*
* @param jwkSetUri The uri of the jwk set. For example:
* <a href="https://login.microsoftonline.com/common/discovery/v2.0/keys">
* https://login.microsoftonline.com/common/discovery/v2.0/keys</a>
* @param restOperations The RestOperations used to retrieve jwk from jwkSetUri.
* @param properties The AAD authentication properties used to build the ID token validators.
*/
public AadOidcIdTokenDecoderFactory(String jwkSetUri, RestOperations restOperations) {
this.jwtDecoder = NimbusJwtDecoder
.withJwkSetUri(jwkSetUri)
.jwsAlgorithm(SignatureAlgorithm.RS256)
.restOperations(restOperations)
.build();
public AadOidcIdTokenDecoderFactory(String jwkSetUri, RestOperations restOperations,
AadAuthenticationProperties properties) {
Assert.notNull(jwkSetUri, "jwkSetUri cannot be null");
Assert.notNull(restOperations, "restOperations cannot be null");
Assert.notNull(properties, "properties cannot be null");
this.jwkSetUri = jwkSetUri;
this.restOperations = restOperations;
this.properties = properties;
}

@Override
public JwtDecoder createDecoder(ClientRegistration context) {
return jwtDecoder;
Assert.notNull(context, "context cannot be null");
return this.jwtDecoders.computeIfAbsent(context.getRegistrationId(), key -> {
NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder
.withJwkSetUri(jwkSetUri)
.jwsAlgorithm(SignatureAlgorithm.RS256)
.restOperations(restOperations)
.build();
jwtDecoder.setJwtValidator(createJwtValidator(context));
return jwtDecoder;
});
}

private OAuth2TokenValidator<Jwt> createJwtValidator(ClientRegistration context) {
List<OAuth2TokenValidator<Jwt>> validators = new ArrayList<>();
// Validates the standard OIDC ID token claims: audience (must contain the client id), expiry, issued-at,
// subject and authorized party.
validators.add(new OidcIdTokenValidator(context));
// Validates the issuer to restrict which tenant(s) are allowed to sign in.
validators.add(createIssuerValidator());
return new DelegatingOAuth2TokenValidator<>(validators);
}

private OAuth2TokenValidator<Jwt> createIssuerValidator() {
String tenantId = getTrimmedTenantId();
if (isMultiTenant(tenantId)) {
return new AadMultiTenantIssuerValidator();
}
return new AadJwtIssuerValidator(new AadTrustedIssuerRepository(tenantId));
}

private String getTrimmedTenantId() {
String tenantId = properties.getProfile().getTenantId();
return tenantId != null ? tenantId.trim().toLowerCase(Locale.ROOT) : null;
}

private static boolean isMultiTenant(String tenantId) {
return !StringUtils.hasText(tenantId)
|| "common".equalsIgnoreCase(tenantId)
|| "organizations".equalsIgnoreCase(tenantId)
|| "consumers".equalsIgnoreCase(tenantId);
}

/**
* Validates that the {@code iss} claim is a trusted Microsoft identity platform issuer that is consistent with the
* token's own {@code tid} (tenant id) claim. This is used by multi-tenant applications, where the configured tenant
* id ({@code common}, {@code organizations} or {@code consumers}) cannot be used to derive a single expected issuer.
*/
private static final class AadMultiTenantIssuerValidator implements OAuth2TokenValidator<Jwt> {

@Override
public OAuth2TokenValidatorResult validate(Jwt token) {
Assert.notNull(token, "token cannot be null");
OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.INVALID_TOKEN,
"The iss claim is not valid.",
"https://learn.microsoft.com/azure/active-directory/develop/id-tokens");
String issuer = token.getClaimAsString(AadJwtClaimNames.ISS);
String tenantId = token.getClaimAsString(AadJwtClaimNames.TID);
if (!StringUtils.hasText(issuer) || !StringUtils.hasText(tenantId)) {
return OAuth2TokenValidatorResult.failure(error);
}
if (new AadTrustedIssuerRepository(tenantId).isTrusted(issuer)) {
return OAuth2TokenValidatorResult.success();
}
return OAuth2TokenValidatorResult.failure(error);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ AadB2cOidcLoginConfigurer b2cLoginConfigurer(AadB2cLogoutSuccessHandler handler,

@Bean
@ConditionalOnMissingBean
JwtDecoderFactory<ClientRegistration> azureAdJwtDecoderFactory() {
return new AadB2cOidcIdTokenDecoderFactory(createRestTemplate(restTemplateBuilder));
JwtDecoderFactory<ClientRegistration> azureAdJwtDecoderFactory(AadB2cProperties properties) {
return new AadB2cOidcIdTokenDecoderFactory(createRestTemplate(restTemplateBuilder), properties);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,31 +3,52 @@

package com.azure.spring.cloud.autoconfigure.implementation.aadb2c.security;

import com.azure.spring.cloud.autoconfigure.implementation.aad.security.jwt.AadJwtIssuerValidator;
import com.azure.spring.cloud.autoconfigure.implementation.aadb2c.configuration.properties.AadB2cProperties;
import com.azure.spring.cloud.autoconfigure.implementation.aadb2c.security.jwt.AadB2cTrustedIssuerRepository;
import org.springframework.security.oauth2.client.oidc.authentication.OidcIdTokenValidator;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.core.DelegatingOAuth2TokenValidator;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.OAuth2TokenValidator;
import org.springframework.security.oauth2.core.oidc.OidcIdToken;
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.JwtDecoderFactory;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.springframework.web.client.RestOperations;

import java.util.ArrayList;
import java.util.List;

/**
* A factory that provides a {@link JwtDecoder} used for {@link OidcIdToken} signature verification.
*
* A factory that provides a {@link JwtDecoder} used for {@link OidcIdToken} signature verification and claim validation.
* <p>
* Besides verifying the token signature, the decoder validates the standard OpenID Connect ID token claims (audience,
* expiry, issued-at and subject) via {@link OidcIdTokenValidator} and additionally validates the {@code iss} (issuer)
* claim against the trusted Azure AD B2C issuers, so that only tokens issued by the configured B2C tenant and user
* flows are accepted.
*/
public class AadB2cOidcIdTokenDecoderFactory implements JwtDecoderFactory<ClientRegistration> {

private final RestOperations restOperations;

private final AadB2cProperties properties;

/**
*
* @param restOperations The RestOperations used to retrieve jwk from jwkSetUri.
* @param properties The AAD B2C properties used to build the ID token validators.
*/
public AadB2cOidcIdTokenDecoderFactory(RestOperations restOperations) {
public AadB2cOidcIdTokenDecoderFactory(RestOperations restOperations, AadB2cProperties properties) {
Assert.notNull(restOperations, "restOperations cannot be null");
Assert.notNull(properties, "properties cannot be null");
this.restOperations = restOperations;
this.properties = properties;
}

@Override
Expand All @@ -41,10 +62,22 @@ public JwtDecoder createDecoder(ClientRegistration clientRegistration) {
null);
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
}
return NimbusJwtDecoder
NimbusJwtDecoder decoder = NimbusJwtDecoder
.withJwkSetUri(jwkSetUri)
.jwsAlgorithm(SignatureAlgorithm.RS256)
.restOperations(restOperations)
.build();
decoder.setJwtValidator(createJwtValidator(clientRegistration));
return decoder;
}

private OAuth2TokenValidator<Jwt> createJwtValidator(ClientRegistration clientRegistration) {
List<OAuth2TokenValidator<Jwt>> validators = new ArrayList<>();
// Validates the standard OIDC ID token claims: audience (must contain the client id), expiry, issued-at,
// subject and authorized party.
validators.add(new OidcIdTokenValidator(clientRegistration));
// Validates the issuer against the trusted Azure AD B2C issuers (tenant and user flows).
validators.add(new AadJwtIssuerValidator(new AadB2cTrustedIssuerRepository(properties)));
return new DelegatingOAuth2TokenValidator<>(validators);
}
}
Loading
Loading