Skip to content

Commit

Permalink
Optimization of Spring AAD B2C configuration condition (Azure#21089)
Browse files Browse the repository at this point in the history
  • Loading branch information
Moary Chen authored May 10, 2021
1 parent 6df89d4 commit 5b5d822
Show file tree
Hide file tree
Showing 8 changed files with 196 additions and 39 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
### New Features
- Upgrade to [spring-boot-dependencies:2.4.5](https://repo.maven.apache.org/maven2/org/springframework/boot/spring-boot-dependencies/2.4.5/spring-boot-dependencies-2.4.5.pom).
- Upgrade to [spring-cloud-dependencies:2020.0.2](https://repo.maven.apache.org/maven2/org/springframework/cloud/spring-cloud-dependencies/2020.0.2/spring-cloud-dependencies-2020.0.2.pom).

- Support OAuth 2.0 Client Credentials Flow.


## 3.4.0 (2021-04-19)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import com.azure.spring.telemetry.TelemetrySender;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnResource;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
Expand All @@ -27,6 +28,7 @@
* and import {@link AADB2COAuth2ClientConfiguration} class for AAD B2C OAuth2 client support.
*/
@Configuration
@ConditionalOnResource(resources = "classpath:aadb2c.enable.config")
@Conditional({ AADB2CConditions.CommonCondition.class, AADB2CConditions.UserFlowCondition.class })
@EnableConfigurationProperties(AADB2CProperties.class)
@Import(AADB2COAuth2ClientConfiguration.class)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,18 @@
package com.azure.spring.autoconfigure.b2c;

import org.springframework.boot.autoconfigure.condition.AnyNestedCondition;
import org.springframework.boot.autoconfigure.condition.ConditionMessage;
import org.springframework.boot.autoconfigure.condition.ConditionOutcome;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.condition.ConditionalOnResource;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.autoconfigure.condition.SpringBootCondition;
import org.springframework.boot.context.properties.bind.Binder;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.type.AnnotatedTypeMetadata;
import org.springframework.util.CollectionUtils;

import java.util.Map;

/**
* Conditions for activating AAD B2C beans.
*/
Expand All @@ -30,7 +32,6 @@ static final class CommonCondition extends AnyNestedCondition {
* Web application scenario condition.
*/
@ConditionalOnWebApplication
@ConditionalOnResource(resources = "classpath:aadb2c.enable.config")
@ConditionalOnProperty(
prefix = AADB2CProperties.PREFIX,
value = {
Expand All @@ -46,7 +47,6 @@ static class WebAppMode {
* Web resource server scenario condition.
*/
@ConditionalOnWebApplication
@ConditionalOnResource(resources = "classpath:aadb2c.enable.config")
@ConditionalOnProperty(prefix = AADB2CProperties.PREFIX, value = { "tenant-id" })
static class WebApiMode {

Expand All @@ -61,12 +61,28 @@ static final class ClientRegistrationCondition extends SpringBootCondition {
@Override
public ConditionOutcome getMatchOutcome(final ConditionContext context,
final AnnotatedTypeMetadata metadata) {
AADB2CProperties aadb2CProperties = Binder.get(context.getEnvironment())
.bind("azure.activedirectory.b2c", AADB2CProperties.class)
.orElseGet(AADB2CProperties::new);
return new ConditionOutcome(!CollectionUtils.isEmpty(aadb2CProperties.getUserFlows())
|| !CollectionUtils.isEmpty(aadb2CProperties.getAuthorizationClients()),
"Configure at least one attribute 'user-flow' or 'authorization-clients'.");
ConditionMessage.Builder message = ConditionMessage.forCondition(
"AAD B2C OAuth 2.0 Clients Configured Condition");
AADB2CProperties aadb2CProperties = getAADB2CProperties(context);
if (aadb2CProperties == null) {
return ConditionOutcome.noMatch(message.notAvailable("aad b2c properties"));
}

if (CollectionUtils.isEmpty(aadb2CProperties.getUserFlows())
&& CollectionUtils.isEmpty(aadb2CProperties.getAuthorizationClients())) {
return ConditionOutcome.noMatch(message.didNotFind("registered clients")
.items("user-flows", "authorization-clients"));
}

StringBuilder details = new StringBuilder();
if (!CollectionUtils.isEmpty(aadb2CProperties.getUserFlows())) {
details.append(getConditionResult("user-flows", aadb2CProperties.getUserFlows()));
}
if (!CollectionUtils.isEmpty(aadb2CProperties.getAuthorizationClients())) {
details.append(getConditionResult("authorization-clients",
aadb2CProperties.getAuthorizationClients()));
}
return ConditionOutcome.match(message.foundExactly(details.toString()));
}
}

Expand All @@ -78,11 +94,40 @@ static final class UserFlowCondition extends SpringBootCondition {
@Override
public ConditionOutcome getMatchOutcome(final ConditionContext context,
final AnnotatedTypeMetadata metadata) {
AADB2CProperties aadb2CProperties = Binder.get(context.getEnvironment())
.bind("azure.activedirectory.b2c", AADB2CProperties.class)
.orElseGet(AADB2CProperties::new);
return new ConditionOutcome(!CollectionUtils.isEmpty(aadb2CProperties.getUserFlows()),
"Configure at least one attribute 'user-flow'.");
ConditionMessage.Builder message = ConditionMessage.forCondition(
"AAD B2C User Flow Clients Configured Condition");
AADB2CProperties aadb2CProperties = getAADB2CProperties(context);
if (aadb2CProperties == null) {
return ConditionOutcome.noMatch(message.notAvailable("aad b2c properties"));
}

if (CollectionUtils.isEmpty(aadb2CProperties.getUserFlows())) {
return ConditionOutcome.noMatch(message.didNotFind("user flows").atAll());
}

return ConditionOutcome.match(message.foundExactly(
getConditionResult("user-flows", aadb2CProperties.getUserFlows())));
}
}

/**
* Return the bound AADB2CProperties instance.
* @param context Condition context
* @return AADB2CProperties instance
*/
private static AADB2CProperties getAADB2CProperties(ConditionContext context) {
return Binder.get(context.getEnvironment())
.bind("azure.activedirectory.b2c", AADB2CProperties.class)
.orElse(null);
}

/**
* Return combined name and the string of the keys of the map which concatenated with ','.
* @param name name to concatenate
* @param map Map to concatenate
* @return the concatenated string.
*/
private static String getConditionResult(String name, Map<String, ?> map) {
return name + ": " + String.join(", ", map.keySet()) + " ";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import org.slf4j.LoggerFactory;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnResource;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
Expand All @@ -31,6 +32,7 @@
* Configuration for AAD B2C OAuth2 client support, when depends on the Spring OAuth2 Client module.
*/
@Configuration
@ConditionalOnResource(resources = "classpath:aadb2c.enable.config")
@Conditional({ AADB2CConditions.CommonCondition.class, AADB2CConditions.ClientRegistrationCondition.class })
@EnableConfigurationProperties(AADB2CProperties.class)
@ConditionalOnClass({ OAuth2LoginAuthenticationFilter.class })
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import com.nimbusds.jwt.proc.JWTProcessor;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnResource;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
Expand All @@ -38,6 +39,7 @@
* and import {@link AADB2COAuth2ClientConfiguration} class for AAD B2C OAuth2 client support.
*/
@Configuration
@ConditionalOnResource(resources = "classpath:aadb2c.enable.config")
@Conditional(AADB2CConditions.CommonCondition.class)
@ConditionalOnClass(BearerTokenAuthenticationToken.class)
@EnableConfigurationProperties(AADB2CProperties.class)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,42 +2,57 @@
// Licensed under the MIT License.
package com.azure.spring.autoconfigure.b2c;

import org.jetbrains.annotations.NotNull;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.mockito.MockedStatic;
import org.mockito.Mockito;
import org.springframework.beans.BeanUtils;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.FilteredClassLoader;
import org.springframework.boot.test.context.runner.WebApplicationContextRunner;
import org.springframework.core.io.ClassPathResource;
import org.springframework.security.oauth2.server.resource.BearerTokenAuthenticationToken;

import java.util.Arrays;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;

public class AADB2CAutoConfigurationTest extends AbstractAADB2COAuth2ClientTestConfiguration {

public AADB2CAutoConfigurationTest() {
contextRunner = new WebApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(WebOAuth2ClientApp.class, AADB2CAutoConfiguration.class))
.withClassLoader(new FilteredClassLoader(BearerTokenAuthenticationToken.class))
.withPropertyValues(
String.format("%s=%s", AADB2CConstants.BASE_URI, AADB2CConstants.TEST_BASE_URI),
String.format("%s=%s", AADB2CConstants.TENANT_ID, AADB2CConstants.TEST_TENANT_ID),
String.format("%s=%s", AADB2CConstants.CLIENT_ID, AADB2CConstants.TEST_CLIENT_ID),
String.format("%s=%s", AADB2CConstants.CLIENT_SECRET, AADB2CConstants.TEST_CLIENT_SECRET),
String.format("%s=%s", AADB2CConstants.LOGOUT_SUCCESS_URL, AADB2CConstants.TEST_LOGOUT_SUCCESS_URL),
String.format("%s=%s", AADB2CConstants.LOGIN_FLOW, AADB2CConstants.TEST_KEY_SIGN_UP_OR_IN),
String.format("%s.%s=%s", AADB2CConstants.USER_FLOWS,
AADB2CConstants.TEST_KEY_SIGN_UP_OR_IN, AADB2CConstants.TEST_SIGN_UP_OR_IN_NAME),
String.format("%s.%s=%s", AADB2CConstants.USER_FLOWS,
AADB2CConstants.TEST_KEY_SIGN_IN, AADB2CConstants.TEST_SIGN_IN_NAME),
String.format("%s.%s=%s", AADB2CConstants.USER_FLOWS,
AADB2CConstants.TEST_KEY_SIGN_UP, AADB2CConstants.TEST_SIGN_UP_NAME),
String.format("%s=%s", AADB2CConstants.CONFIG_PROMPT, AADB2CConstants.TEST_PROMPT),
String.format("%s=%s", AADB2CConstants.CONFIG_LOGIN_HINT, AADB2CConstants.TEST_LOGIN_HINT),
String.format("%s=%s", AADB2CConstants.USER_NAME_ATTRIBUTE_NAME, AADB2CConstants.TEST_ATTRIBUTE_NAME),
String.format("%s=%s", AADB2CConstants.USER_NAME_ATTRIBUTE_NAME, AADB2CConstants.TEST_ATTRIBUTE_NAME)
);
.withPropertyValues(getWebappCommonPropertyValues());
}

@NotNull
private String[] getWebappCommonPropertyValues() {
return new String[] { String.format("%s=%s", AADB2CConstants.BASE_URI, AADB2CConstants.TEST_BASE_URI),
String.format("%s=%s", AADB2CConstants.TENANT_ID, AADB2CConstants.TEST_TENANT_ID),
String.format("%s=%s", AADB2CConstants.CLIENT_ID, AADB2CConstants.TEST_CLIENT_ID),
String.format("%s=%s", AADB2CConstants.CLIENT_SECRET, AADB2CConstants.TEST_CLIENT_SECRET),
String.format("%s=%s", AADB2CConstants.LOGOUT_SUCCESS_URL, AADB2CConstants.TEST_LOGOUT_SUCCESS_URL),
String.format("%s=%s", AADB2CConstants.LOGIN_FLOW, AADB2CConstants.TEST_KEY_SIGN_UP_OR_IN),
String.format("%s.%s=%s", AADB2CConstants.USER_FLOWS,
AADB2CConstants.TEST_KEY_SIGN_UP_OR_IN, AADB2CConstants.TEST_SIGN_UP_OR_IN_NAME),
String.format("%s.%s=%s", AADB2CConstants.USER_FLOWS,
AADB2CConstants.TEST_KEY_SIGN_IN, AADB2CConstants.TEST_SIGN_IN_NAME),
String.format("%s.%s=%s", AADB2CConstants.USER_FLOWS,
AADB2CConstants.TEST_KEY_SIGN_UP, AADB2CConstants.TEST_SIGN_UP_NAME),
String.format("%s=%s", AADB2CConstants.CONFIG_PROMPT, AADB2CConstants.TEST_PROMPT),
String.format("%s=%s", AADB2CConstants.CONFIG_LOGIN_HINT, AADB2CConstants.TEST_LOGIN_HINT),
String.format("%s=%s", AADB2CConstants.USER_NAME_ATTRIBUTE_NAME, AADB2CConstants.TEST_ATTRIBUTE_NAME) };
}

@Test
Expand Down Expand Up @@ -88,4 +103,46 @@ public void testLogoutSuccessHandlerBean() {
Assertions.assertNotNull(handler);
});
}

@Test
public void testWebappConditionsIsInvokedWhenAADB2CEnableFileExists() {
try (MockedStatic<BeanUtils> beanUtils = mockStatic(BeanUtils.class, Mockito.CALLS_REAL_METHODS)) {
AADB2CConditions.UserFlowCondition userFlowCondition = spy(AADB2CConditions.UserFlowCondition.class);
AADB2CConditions.ClientRegistrationCondition clientRegistrationCondition =
spy(AADB2CConditions.ClientRegistrationCondition.class);
beanUtils.when(() -> BeanUtils.instantiateClass(AADB2CConditions.UserFlowCondition.class))
.thenReturn(userFlowCondition);
beanUtils.when(() -> BeanUtils.instantiateClass(AADB2CConditions.ClientRegistrationCondition.class))
.thenReturn(clientRegistrationCondition);
this.contextRunner
.run(c -> {
Assertions.assertTrue(c.getResource(AAD_B2C_ENABLE_CONFIG_FILE_NAME).exists());
verify(userFlowCondition, atLeastOnce()).getMatchOutcome(any(), any());
verify(clientRegistrationCondition, atLeastOnce()).getMatchOutcome(any(), any());
});
}
}

@Test
public void testWebappConditionsIsNotInvokedWhenAADB2CEnableFileDoesNotExists() {
try (MockedStatic<BeanUtils> beanUtils = mockStatic(BeanUtils.class, Mockito.CALLS_REAL_METHODS)) {
AADB2CConditions.UserFlowCondition userFlowCondition = mock(AADB2CConditions.UserFlowCondition.class);
AADB2CConditions.ClientRegistrationCondition clientRegistrationCondition =
spy(AADB2CConditions.ClientRegistrationCondition.class);
beanUtils.when(() -> BeanUtils.instantiateClass(AADB2CConditions.UserFlowCondition.class))
.thenReturn(userFlowCondition);
beanUtils.when(() -> BeanUtils.instantiateClass(AADB2CConditions.ClientRegistrationCondition.class))
.thenReturn(clientRegistrationCondition);
new WebApplicationContextRunner()
.withClassLoader(new FilteredClassLoader(new ClassPathResource(AAD_B2C_ENABLE_CONFIG_FILE_NAME)))
.withConfiguration(AutoConfigurations.of(WebResourceServerApp.class,
AADB2CResourceServerAutoConfiguration.class))
.withPropertyValues(getWebappCommonPropertyValues())
.run(c -> {
Assertions.assertFalse(c.getResource(AAD_B2C_ENABLE_CONFIG_FILE_NAME).exists());
verify(userFlowCondition, never()).getMatchOutcome(any(), any());
verify(clientRegistrationCondition, never()).getMatchOutcome(any(), any());
});
}
}
}
Loading

0 comments on commit 5b5d822

Please sign in to comment.