diff --git a/sdk/spring/azure-spring-boot-test-aad/pom.xml b/sdk/spring/azure-spring-boot-test-aad/pom.xml index 2230d832e3876..644fe0540017c 100644 --- a/sdk/spring/azure-spring-boot-test-aad/pom.xml +++ b/sdk/spring/azure-spring-boot-test-aad/pom.xml @@ -30,6 +30,15 @@ spring-boot-starter-test test + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-oauth2-client + diff --git a/sdk/spring/azure-spring-boot-test-aad/src/test/java/com/azure/test/aad/auth/AppAutoConfigTest.java b/sdk/spring/azure-spring-boot-test-aad/src/test/java/com/azure/test/aad/auth/AppAutoConfigTest.java new file mode 100644 index 0000000000000..498b78e30b5f9 --- /dev/null +++ b/sdk/spring/azure-spring-boot-test-aad/src/test/java/com/azure/test/aad/auth/AppAutoConfigTest.java @@ -0,0 +1,230 @@ +package com.azure.test.aad.auth; + +import com.azure.spring.aad.implementation.AzureClientRegistrationRepository; +import com.azure.spring.aad.implementation.DefaultClient; +import com.azure.spring.aad.implementation.IdentityEndpoints; +import com.azure.test.utils.AppRunner; +import org.junit.jupiter.api.Test; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; + +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class AppAutoConfigTest { + + @Test + public void clientRegistered() { + try (AppRunner appRunner = createApp()) { + appRunner.start(); + + ClientRegistrationRepository clientRegistrationRepository = + appRunner.getBean(ClientRegistrationRepository.class); + ClientRegistration azureClientRegistration = clientRegistrationRepository.findByRegistrationId("azure"); + + assertNotNull(azureClientRegistration); + assertEquals("fake-client-id", azureClientRegistration.getClientId()); + assertEquals("fake-client-secret", azureClientRegistration.getClientSecret()); + + IdentityEndpoints identityEndpoints = new IdentityEndpoints(); + assertEquals( + identityEndpoints.authorizationEndpoint("fake-tenant-id"), + azureClientRegistration.getProviderDetails().getAuthorizationUri() + ); + assertEquals( + identityEndpoints.tokenEndpoint("fake-tenant-id"), + azureClientRegistration.getProviderDetails().getTokenUri() + ); + assertEquals( + identityEndpoints.jwkSetEndpoint("fake-tenant-id"), + azureClientRegistration.getProviderDetails().getJwkSetUri() + ); + assertEquals( + "{baseUrl}/login/oauth2/code/{registrationId}", + azureClientRegistration.getRedirectUriTemplate() + ); + assertDefaultScopes(azureClientRegistration, "openid", "profile"); + } + } + + @Test + public void clientRequiresPermissionRegistered() { + try (AppRunner appRunner = createApp()) { + appRunner.property("azure.activedirectory.authorization.graph.scope", "Calendars.Read"); + appRunner.start(); + + ClientRegistrationRepository clientRegistrationRepository = + appRunner.getBean(ClientRegistrationRepository.class); + ClientRegistration azureClientRegistration = clientRegistrationRepository.findByRegistrationId("azure"); + ClientRegistration graphClientRegistration = clientRegistrationRepository.findByRegistrationId("graph"); + + assertNotNull(azureClientRegistration); + assertDefaultScopes(azureClientRegistration, "openid", "profile", "offline_access", "Calendars.Read"); + + assertNotNull(graphClientRegistration); + assertDefaultScopes(graphClientRegistration, "Calendars.Read"); + } + } + + @Test + public void clientRequiresMultiPermissions() { + try (AppRunner appRunner = createApp()) { + appRunner.property("azure.activedirectory.authorization.graph.scope", "Calendars.Read"); + appRunner.property( + "azure.activedirectory.authorization.arm.scope", + "https://management.core.windows.net/user_impersonation" + ); + appRunner.start(); + + ClientRegistrationRepository clientRegistrationRepository = + appRunner.getBean(ClientRegistrationRepository.class); + ClientRegistration azureClientRegistration = clientRegistrationRepository.findByRegistrationId("azure"); + ClientRegistration graphClientRegistration = clientRegistrationRepository.findByRegistrationId("graph"); + + assertNotNull(azureClientRegistration); + assertDefaultScopes( + azureClientRegistration, + "openid", + "profile", + "offline_access", + "Calendars.Read", + "https://management.core.windows.net/user_impersonation" + ); + + assertNotNull(graphClientRegistration); + assertDefaultScopes(graphClientRegistration, "Calendars.Read"); + } + } + + @Test + public void clientRequiresPermissionInDefaultClient() { + try (AppRunner appRunner = createApp()) { + appRunner.property("azure.activedirectory.authorization.azure.scope", "Calendars.Read"); + appRunner.start(); + + ClientRegistrationRepository clientRegistrationRepository = + appRunner.getBean(ClientRegistrationRepository.class); + ClientRegistration azureClientRegistration = clientRegistrationRepository.findByRegistrationId("azure"); + + assertNotNull(azureClientRegistration); + assertDefaultScopes( + azureClientRegistration, + "openid", "profile", "offline_access", "Calendars.Read" + ); + } + } + + @Test + public void aadAwareClientRepository() { + try (AppRunner appRunner = createApp()) { + appRunner.property("azure.activedirectory.authorization.graph.scope", "Calendars.Read"); + appRunner.start(); + + AzureClientRegistrationRepository azureClientRegistrationRepository = + (AzureClientRegistrationRepository) appRunner.getBean(ClientRegistrationRepository.class); + ClientRegistration azureClientRegistration = + azureClientRegistrationRepository.findByRegistrationId("azure"); + ClientRegistration graphClientRegistration = + azureClientRegistrationRepository.findByRegistrationId("graph"); + + assertDefaultScopes( + azureClientRegistrationRepository.defaultClient(), + "openid", "profile", "offline_access" + ); + assertEquals(azureClientRegistrationRepository.defaultClient().getClientRegistration(), azureClientRegistration); + + assertFalse(azureClientRegistrationRepository.isAuthorizedClient(azureClientRegistration)); + assertTrue(azureClientRegistrationRepository.isAuthorizedClient(graphClientRegistration)); + assertFalse(azureClientRegistrationRepository.isAuthorizedClient("azure")); + assertTrue(azureClientRegistrationRepository.isAuthorizedClient("graph")); + + List clientRegistrations = collectClients(azureClientRegistrationRepository); + assertEquals(1, clientRegistrations.size()); + assertEquals("azure", clientRegistrations.get(0).getRegistrationId()); + } + } + + @Test + public void defaultClientWithAuthzScope() { + try (AppRunner appRunner = createApp()) { + appRunner.property("azure.activedirectory.authorization.azure.scope", "Calendars.Read"); + appRunner.start(); + + AzureClientRegistrationRepository azureClientRegistrationRepository = + appRunner.getBean(AzureClientRegistrationRepository.class); + assertDefaultScopes( + azureClientRegistrationRepository.defaultClient(), + "openid", "profile", "offline_access", "Calendars.Read" + ); + } + } + + @Test + public void customizeUri() { + try (AppRunner appRunner = createApp()) { + appRunner.property("azure.activedirectory.uri", "http://localhost/"); + appRunner.start(); + + AzureClientRegistrationRepository azureClientRegistrationRepository = + appRunner.getBean(AzureClientRegistrationRepository.class); + ClientRegistration azureClientRegistration = + azureClientRegistrationRepository.findByRegistrationId("azure"); + + IdentityEndpoints endpoints = new IdentityEndpoints("http://localhost/"); + assertEquals( + endpoints.authorizationEndpoint("fake-tenant-id"), + azureClientRegistration.getProviderDetails().getAuthorizationUri() + ); + assertEquals( + endpoints.tokenEndpoint("fake-tenant-id"), + azureClientRegistration.getProviderDetails().getTokenUri() + ); + assertEquals( + endpoints.jwkSetEndpoint("fake-tenant-id"), + azureClientRegistration.getProviderDetails().getJwkSetUri() + ); + } + } + + private AppRunner createApp() { + AppRunner result = new AppRunner(DumbApp.class); + result.property("azure.activedirectory.tenant-id", "fake-tenant-id"); + result.property("azure.activedirectory.client-id", "fake-client-id"); + result.property("azure.activedirectory.client-secret", "fake-client-secret"); + result.property("azure.activedirectory.user-group.allowed-groups", "group1"); + return result; + } + + private void assertDefaultScopes(ClientRegistration client, String ... scopes) { + assertEquals(scopes.length, client.getScopes().size()); + for (String s : scopes) { + assertTrue(client.getScopes().contains(s)); + } + } + + private void assertDefaultScopes(DefaultClient client, String ... expected) { + assertEquals(expected.length, client.getScopeList().size()); + for (String e : expected) { + assertTrue(client.getScopeList().contains(e)); + } + } + + private List collectClients(Iterable iterable) { + List result = new ArrayList<>(); + iterable.forEach(result::add); + return result; + } + + @Configuration + @EnableAutoConfiguration + @EnableWebSecurity + public static class DumbApp {} +} diff --git a/sdk/spring/azure-spring-boot-test-aad/src/test/java/com/azure/test/aad/auth/AuthorizedClientRepoTest.java b/sdk/spring/azure-spring-boot-test-aad/src/test/java/com/azure/test/aad/auth/AuthorizedClientRepoTest.java new file mode 100644 index 0000000000000..6450c5d9bef77 --- /dev/null +++ b/sdk/spring/azure-spring-boot-test-aad/src/test/java/com/azure/test/aad/auth/AuthorizedClientRepoTest.java @@ -0,0 +1,157 @@ +package com.azure.test.aad.auth; + +import com.azure.spring.aad.implementation.AzureClientRegistrationRepository; +import com.azure.spring.aad.implementation.AzureOAuth2AuthorizedClientRepository; +import com.azure.test.utils.AppRunner; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.context.annotation.Configuration; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository; +import org.springframework.security.oauth2.core.AbstractOAuth2Token; +import org.springframework.security.oauth2.core.OAuth2AccessToken; +import org.springframework.security.oauth2.core.OAuth2RefreshToken; +import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken; + +import java.time.Instant; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class AuthorizedClientRepoTest { + + private AppRunner appRunner; + + private ClientRegistration azureClientRegistration; + private ClientRegistration graphClientRegistration; + + private OAuth2AuthorizedClientRepository oAuth2AuthorizedClientRepository; + private MockHttpServletRequest mockHttpServletRequest; + private MockHttpServletResponse mockHttpServletResponse; + + @BeforeEach + public void setup() { + appRunner = createApp(); + appRunner.start(); + + AzureClientRegistrationRepository azureClientRegistrationRepository = + appRunner.getBean(AzureClientRegistrationRepository.class); + azureClientRegistration = azureClientRegistrationRepository.findByRegistrationId("azure"); + graphClientRegistration = azureClientRegistrationRepository.findByRegistrationId("graph"); + + oAuth2AuthorizedClientRepository = new AzureOAuth2AuthorizedClientRepository(azureClientRegistrationRepository); + mockHttpServletRequest = new MockHttpServletRequest(); + mockHttpServletResponse = new MockHttpServletResponse(); + } + + private AppRunner createApp() { + AppRunner result = new AppRunner(AppAutoConfigTest.DumbApp.class); + result.property("azure.activedirectory.tenant-id", "fake-tenant-id"); + result.property("azure.activedirectory.client-id", "fake-client-id"); + result.property("azure.activedirectory.client-secret", "fake-client-secret"); + result.property("azure.activedirectory.user-group.allowed-groups", "group1"); + result.property("azure.activedirectory.authorization.graph.scope", "Calendars.Read"); + return result; + } + + @AfterEach + public void tearDown() { + appRunner.stop(); + } + + @Test + public void loadInitAzureAuthzClient() { + oAuth2AuthorizedClientRepository.saveAuthorizedClient( + toOAuthAuthorizedClient(azureClientRegistration), + createAuthentication(), + mockHttpServletRequest, + mockHttpServletResponse + ); + + OAuth2AuthorizedClient oAuth2AuthorizedClient = + oAuth2AuthorizedClientRepository.loadAuthorizedClient( + "graph", + createAuthentication(), + mockHttpServletRequest + ); + + assertNotNull(oAuth2AuthorizedClient); + assertNotNull(oAuth2AuthorizedClient.getAccessToken()); + assertNotNull(oAuth2AuthorizedClient.getRefreshToken()); + + assertTrue(isTokenExpired(oAuth2AuthorizedClient.getAccessToken())); + assertEquals("fake-refresh-token", oAuth2AuthorizedClient.getRefreshToken().getTokenValue()); + } + + @Test + public void saveAndLoadAzureAuthzClient() { + oAuth2AuthorizedClientRepository.saveAuthorizedClient( + toOAuthAuthorizedClient(graphClientRegistration), + createAuthentication(), + mockHttpServletRequest, + mockHttpServletResponse + ); + + OAuth2AuthorizedClient oAuth2AuthorizedClient = + oAuth2AuthorizedClientRepository.loadAuthorizedClient( + "graph", + createAuthentication(), + mockHttpServletRequest + ); + + assertNotNull(oAuth2AuthorizedClient); + assertNotNull(oAuth2AuthorizedClient.getAccessToken()); + assertNotNull(oAuth2AuthorizedClient.getRefreshToken()); + + assertEquals("fake-access-token", oAuth2AuthorizedClient.getAccessToken().getTokenValue()); + assertEquals("fake-refresh-token", oAuth2AuthorizedClient.getRefreshToken().getTokenValue()); + } + + private OAuth2AuthorizedClient toOAuthAuthorizedClient(ClientRegistration clientRegistration) { + return new OAuth2AuthorizedClient( + clientRegistration, + "fake-principal-name", + createAccessToken(), + createRefreshToken() + ); + } + + private OAuth2AccessToken createAccessToken() { + return new OAuth2AccessToken( + OAuth2AccessToken.TokenType.BEARER, + "fake-access-token", + Instant.MIN, + Instant.MAX + ); + } + + private OAuth2RefreshToken createRefreshToken() { + return new OAuth2RefreshToken("fake-refresh-token", Instant.MIN); + } + + private Authentication createAuthentication() { + return new PreAuthenticatedAuthenticationToken("fake-user", "fake-crednetial"); + } + + private boolean isTokenExpired(OAuth2AccessToken oAuth2AccessToken) { + return Optional.ofNullable(oAuth2AccessToken) + .map(AbstractOAuth2Token::getExpiresAt) + .map(expiredAt -> expiredAt.isBefore(Instant.now())) + .orElse(false); + } + + @Configuration + @EnableAutoConfiguration + @EnableWebSecurity + public static class DumbApp { + } +} diff --git a/sdk/spring/azure-spring-boot-test-aad/src/test/java/com/azure/test/aad/auth/AuthzCodeGrantRequestEntityConverterTest.java b/sdk/spring/azure-spring-boot-test-aad/src/test/java/com/azure/test/aad/auth/AuthzCodeGrantRequestEntityConverterTest.java new file mode 100644 index 0000000000000..091bbd2cca0ae --- /dev/null +++ b/sdk/spring/azure-spring-boot-test-aad/src/test/java/com/azure/test/aad/auth/AuthzCodeGrantRequestEntityConverterTest.java @@ -0,0 +1,118 @@ +package com.azure.test.aad.auth; + +import com.azure.spring.aad.implementation.AzureClientRegistrationRepository; +import com.azure.spring.aad.implementation.AzureOAuth2AuthorizationCodeGrantRequestEntityConverter; +import com.azure.test.utils.AppRunner; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpEntity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequest; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationExchange; +import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest; +import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponse; +import org.springframework.util.MultiValueMap; + +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +public class AuthzCodeGrantRequestEntityConverterTest { + + private AppRunner appRunner; + private AzureClientRegistrationRepository azureClientRegistrationRepository; + private ClientRegistration azureClientRegistration; + private ClientRegistration graphClientRegistration; + + @BeforeEach + public void setupApp() { + appRunner = createApp(); + appRunner.start(); + + azureClientRegistrationRepository = appRunner.getBean(AzureClientRegistrationRepository.class); + azureClientRegistration = azureClientRegistrationRepository.findByRegistrationId("azure"); + graphClientRegistration = azureClientRegistrationRepository.findByRegistrationId("graph"); + } + + private AppRunner createApp() { + AppRunner result = new AppRunner(DumbApp.class); + result.property("azure.activedirectory.tenant-id", "fake-tenant-id"); + result.property("azure.activedirectory.client-id", "fake-client-id"); + result.property("azure.activedirectory.client-secret", "fake-client-secret"); + result.property("azure.activedirectory.user-group.allowed-groups", "group1"); + result.property("azure.activedirectory.authorization.graph.scope", "Calendars.Read"); + return result; + } + + @AfterEach + public void tearDownApp() { + appRunner.stop(); + } + + @Test + public void addScopeForDefaultClient() { + MultiValueMap multiValueMap = toMultiValueMap(createCodeGrantRequest(azureClientRegistration)); + assertEquals("openid profile offline_access", multiValueMap.getFirst("scope")); + } + + @Test + public void noScopeParamForOtherClient() { + MultiValueMap multiValueMap = toMultiValueMap(createCodeGrantRequest(graphClientRegistration)); + assertNull(multiValueMap.get("scope")); + } + + @SuppressWarnings("unchecked") + private MultiValueMap toMultiValueMap(OAuth2AuthorizationCodeGrantRequest request) { + return (MultiValueMap) + Optional.ofNullable(azureClientRegistrationRepository) + .map(AzureClientRegistrationRepository::defaultClient) + .map(AzureOAuth2AuthorizationCodeGrantRequestEntityConverter::new) + .map(converter -> converter.convert(request)) + .map(HttpEntity::getBody) + .orElse(null); + } + + private OAuth2AuthorizationCodeGrantRequest createCodeGrantRequest(ClientRegistration clientRegistration) { + return new OAuth2AuthorizationCodeGrantRequest( + clientRegistration, + toOAuth2AuthorizationExchange(clientRegistration) + ); + } + + private OAuth2AuthorizationExchange toOAuth2AuthorizationExchange(ClientRegistration clientRegistration) { + return new OAuth2AuthorizationExchange( + toOAuth2AuthorizationRequest(clientRegistration), + toOAuth2AuthorizationResponse() + ); + } + + private OAuth2AuthorizationRequest toOAuth2AuthorizationRequest(ClientRegistration clientRegistration) { + return OAuth2AuthorizationRequest.authorizationCode() + .authorizationUri( + clientRegistration.getProviderDetails().getAuthorizationUri() + ) + .clientId(clientRegistration.getClientId()) + .scopes(clientRegistration.getScopes()) + .state("fake-state") + .redirectUri("http://localhost") + .build(); + } + + private OAuth2AuthorizationResponse toOAuth2AuthorizationResponse() { + return OAuth2AuthorizationResponse.success("fake-code") + .redirectUri("http://localhost") + .state("fake-state") + .build(); + } + + @Configuration + @EnableAutoConfiguration + @EnableWebSecurity + public static class DumbApp { + } +} diff --git a/sdk/spring/azure-spring-boot/pom.xml b/sdk/spring/azure-spring-boot/pom.xml index 82db3abb727a3..34187448a2464 100644 --- a/sdk/spring/azure-spring-boot/pom.xml +++ b/sdk/spring/azure-spring-boot/pom.xml @@ -259,6 +259,11 @@ 4.0.2 compile + + org.springframework + spring-core + 5.2.10.RELEASE + @@ -282,6 +287,7 @@ javax.validation:validation-api:[2.0.1.Final] org.slf4j:slf4j-api:[1.7.30] org.hibernate.validator:hibernate-validator:[6.1.6.Final] + org.springframework:spring-core:[5.2.10.RELEASE] org.springframework:spring-web:[5.2.10.RELEASE] org.springframework:spring-jms:[5.2.10.RELEASE] org.springframework.boot:spring-boot-actuator-autoconfigure:[2.3.5.RELEASE] diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/implementation/AuthorizationProperties.java b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/implementation/AuthorizationProperties.java new file mode 100644 index 0000000000000..7ee16ec92fb77 --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/implementation/AuthorizationProperties.java @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.aad.implementation; + +import java.util.Arrays; +import java.util.List; + +public class AuthorizationProperties { + + private String[] scope = new String[0]; + + public void setScope(String[] scope) { + this.scope = scope.clone(); + } + + public String[] getScope() { + return scope.clone(); + } + + public List scopes() { + return Arrays.asList(scope); + } +} diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/implementation/AzureActiveDirectoryAutoConfiguration.java b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/implementation/AzureActiveDirectoryAutoConfiguration.java new file mode 100644 index 0000000000000..e4ec1ddf12518 --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/implementation/AzureActiveDirectoryAutoConfiguration.java @@ -0,0 +1,166 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.aad.implementation; + +import com.azure.spring.autoconfigure.aad.AADAuthenticationProperties; +import com.azure.spring.autoconfigure.aad.AADOAuth2UserService; +import com.azure.spring.autoconfigure.aad.ServiceEndpointsProperties; +import com.azure.spring.telemetry.TelemetrySender; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +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.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.PropertySource; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; +import org.springframework.security.oauth2.client.userinfo.OAuth2UserService; +import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.oidc.user.OidcUser; +import org.springframework.util.ClassUtils; + +import javax.annotation.PostConstruct; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static com.azure.spring.telemetry.TelemetryData.SERVICE_NAME; +import static com.azure.spring.telemetry.TelemetryData.getClassPackageSimpleName; + +@Configuration +@ConditionalOnResource(resources = "classpath:aad.enable.config") +@ConditionalOnClass(ClientRegistrationRepository.class) +@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET) +@ConditionalOnProperty(prefix = "azure.activedirectory", value = {"client-id", "client-secret", "tenant-id"}) +@PropertySource(value = "classpath:service-endpoints.properties") +@EnableConfigurationProperties({ AADAuthenticationProperties.class, ServiceEndpointsProperties.class }) +public class AzureActiveDirectoryAutoConfiguration { + + private static final String DEFAULT_CLIENT = "azure"; + + @Autowired + private AADAuthenticationProperties aadAuthenticationProperties; + @Autowired + private ServiceEndpointsProperties serviceEndpointsProperties; + + @Bean + @ConditionalOnProperty(prefix = "azure.activedirectory.user-group", value = "allowed-groups") + public OAuth2UserService oidcUserService() { + return new AADOAuth2UserService(aadAuthenticationProperties, serviceEndpointsProperties); + } + + @Bean + @ConditionalOnMissingBean({ ClientRegistrationRepository.class, AzureClientRegistrationRepository.class }) + public AzureClientRegistrationRepository clientRegistrationRepository() { + return new AzureClientRegistrationRepository( + createDefaultClient(), + createClientRegistrations() + ); + } + + private DefaultClient createDefaultClient() { + ClientRegistration clientRegistration = toClientRegistrationBuilder(DEFAULT_CLIENT) + .scope(allScopes()) + .build(); + return new DefaultClient(clientRegistration, defaultScopes()); + } + + private String[] allScopes() { + List result = openidScopes(); + for (AuthorizationProperties properties : aadAuthenticationProperties.getAuthorization().values()) { + result.addAll(properties.scopes()); + } + return result.toArray(new String[0]); + } + + private String[] defaultScopes() { + List result = openidScopes(); + AuthorizationProperties authorizationProperties = + aadAuthenticationProperties.getAuthorization().get(DEFAULT_CLIENT); + if (authorizationProperties != null) { + result.addAll(authorizationProperties.scopes()); + } + return result.toArray(new String[0]); + } + + private List openidScopes() { + List result = new ArrayList<>(); + result.add("openid"); + result.add("profile"); + if (!aadAuthenticationProperties.getAuthorization().isEmpty()) { + result.add("offline_access"); + } + return result; + } + + private List createClientRegistrations() { + List result = new ArrayList<>(); + for (String name : aadAuthenticationProperties.getAuthorization().keySet()) { + if (DEFAULT_CLIENT.equals(name)) { + continue; + } + AuthorizationProperties authorizationProperties = + aadAuthenticationProperties.getAuthorization().get(name); + result.add(toClientRegistration(name, authorizationProperties)); + } + return result; + } + + private ClientRegistration toClientRegistration(String id, AuthorizationProperties authorizationProperties) { + return toClientRegistrationBuilder(id) + .scope(authorizationProperties.getScope()) + .build(); + } + + private ClientRegistration.Builder toClientRegistrationBuilder(String registrationId) { + IdentityEndpoints endpoints = new IdentityEndpoints(aadAuthenticationProperties.getUri()); + String tenantId = aadAuthenticationProperties.getTenantId(); + return ClientRegistration.withRegistrationId(registrationId) + .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) + .redirectUriTemplate("{baseUrl}/login/oauth2/code/{registrationId}") + .clientId(aadAuthenticationProperties.getClientId()) + .clientSecret(aadAuthenticationProperties.getClientSecret()) + .authorizationUri(endpoints.authorizationEndpoint(tenantId)) + .tokenUri(endpoints.tokenEndpoint(tenantId)) + .jwkSetUri(endpoints.jwkSetEndpoint(tenantId)); + } + + @Bean + @ConditionalOnMissingBean + public OAuth2AuthorizedClientRepository authorizedClientRepository(AzureClientRegistrationRepository repo) { + return new AzureOAuth2AuthorizedClientRepository(repo); + } + + @Configuration + @ConditionalOnMissingBean(WebSecurityConfigurerAdapter.class) + @EnableWebSecurity + public static class DefaultAzureOAuth2WebSecurityConfigurerAdapter extends AzureOAuth2WebSecurityConfigurerAdapter { + + @Override + protected void configure(HttpSecurity http) throws Exception { + super.configure(http); + http.authorizeRequests().anyRequest().authenticated(); + } + } + + @PostConstruct + private void sendTelemetry() { + if (aadAuthenticationProperties.isAllowTelemetry()) { + final Map events = new HashMap<>(); + final TelemetrySender sender = new TelemetrySender(); + events.put(SERVICE_NAME, getClassPackageSimpleName(AzureActiveDirectoryAutoConfiguration.class)); + sender.send(ClassUtils.getUserClass(getClass()).getSimpleName(), events); + } + } +} diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/implementation/AzureClientRegistrationRepository.java b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/implementation/AzureClientRegistrationRepository.java new file mode 100644 index 0000000000000..7af598850c78b --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/implementation/AzureClientRegistrationRepository.java @@ -0,0 +1,65 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.aad.implementation; + +import org.jetbrains.annotations.NotNull; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +public class AzureClientRegistrationRepository implements ClientRegistrationRepository, Iterable { + + private final DefaultClient defaultClient; + private final List authorizedClientRegistrations; + + private final Map clientRegistrations; + + public AzureClientRegistrationRepository(DefaultClient defaultClient, + List authorizedClientRegistrations) { + this.defaultClient = defaultClient; + this.authorizedClientRegistrations = new ArrayList<>(authorizedClientRegistrations); + clientRegistrations = new HashMap<>(); + addClientRegistration(defaultClient.getClientRegistration()); + for (ClientRegistration clientRegistration : authorizedClientRegistrations) { + addClientRegistration(clientRegistration); + } + } + + private void addClientRegistration(ClientRegistration clientRegistration) { + clientRegistrations.put(clientRegistration.getRegistrationId(), clientRegistration); + } + + @Override + public ClientRegistration findByRegistrationId(String registrationId) { + return clientRegistrations.get(registrationId); + } + + @NotNull + @Override + public Iterator iterator() { + return Collections.singleton(defaultClient.getClientRegistration()).iterator(); + } + + public DefaultClient defaultClient() { + return defaultClient; + } + + public boolean isAuthorizedClient(ClientRegistration clientRegistration) { + return authorizedClientRegistrations.contains(clientRegistration); + } + + public boolean isAuthorizedClient(String id) { + return Optional.of(id) + .map(this::findByRegistrationId) + .map(this::isAuthorizedClient) + .orElse(false); + } +} diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/implementation/AzureOAuth2AuthorizationCodeGrantRequestEntityConverter.java b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/implementation/AzureOAuth2AuthorizationCodeGrantRequestEntityConverter.java new file mode 100644 index 0000000000000..3609681c1d0ea --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/implementation/AzureOAuth2AuthorizationCodeGrantRequestEntityConverter.java @@ -0,0 +1,43 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.aad.implementation; + +import org.springframework.http.HttpEntity; +import org.springframework.http.RequestEntity; +import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequest; +import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequestEntityConverter; +import org.springframework.util.MultiValueMap; + +import java.util.Optional; + +public class AzureOAuth2AuthorizationCodeGrantRequestEntityConverter + extends OAuth2AuthorizationCodeGrantRequestEntityConverter { + + private final DefaultClient defaultClient; + + public AzureOAuth2AuthorizationCodeGrantRequestEntityConverter(DefaultClient defaultClient) { + this.defaultClient = defaultClient; + } + + @Override + @SuppressWarnings("unchecked") + public RequestEntity convert(OAuth2AuthorizationCodeGrantRequest request) { + RequestEntity result = super.convert(request); + if (isRequestForDefaultClient(request)) { + Optional.ofNullable(result) + .map(HttpEntity::getBody) + .map(b -> (MultiValueMap) b) + .ifPresent(map -> map.add("scope", scopeValue())); + } + return result; + } + + private boolean isRequestForDefaultClient(OAuth2AuthorizationCodeGrantRequest request) { + return request.getClientRegistration().equals(defaultClient.getClientRegistration()); + } + + private String scopeValue() { + return String.join(" ", defaultClient.getScope()); + } +} diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/implementation/AzureOAuth2AuthorizedClientRepository.java b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/implementation/AzureOAuth2AuthorizedClientRepository.java new file mode 100644 index 0000000000000..8312c7605b71f --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/implementation/AzureOAuth2AuthorizedClientRepository.java @@ -0,0 +1,115 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.aad.implementation; + +import org.springframework.security.core.Authentication; +import org.springframework.security.oauth2.client.InMemoryOAuth2AuthorizedClientService; +import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; +import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; +import org.springframework.security.oauth2.client.web.AuthenticatedPrincipalOAuth2AuthorizedClientRepository; +import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepository; +import org.springframework.security.oauth2.core.OAuth2AccessToken; +import org.springframework.security.oauth2.core.OAuth2RefreshToken; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.Optional; + +public class AzureOAuth2AuthorizedClientRepository implements OAuth2AuthorizedClientRepository { + + private final AzureClientRegistrationRepository azureClientRegistrationRepository; + private final OAuth2AuthorizedClientRepository delegatedOAuth2AuthorizedClientRepository; + + private static OAuth2AuthorizedClientRepository createDefaultDelegate( + ClientRegistrationRepository clientRegistrationRepository + ) { + return new AuthenticatedPrincipalOAuth2AuthorizedClientRepository( + new InMemoryOAuth2AuthorizedClientService(clientRegistrationRepository) + ); + } + + public AzureOAuth2AuthorizedClientRepository(AzureClientRegistrationRepository azureClientRegistrationRepository) { + this(azureClientRegistrationRepository, createDefaultDelegate(azureClientRegistrationRepository)); + } + + public AzureOAuth2AuthorizedClientRepository( + AzureClientRegistrationRepository azureClientRegistrationRepository, + OAuth2AuthorizedClientRepository delegatedOAuth2AuthorizedClientRepository + ) { + this.azureClientRegistrationRepository = azureClientRegistrationRepository; + this.delegatedOAuth2AuthorizedClientRepository = delegatedOAuth2AuthorizedClientRepository; + } + + @Override + public void saveAuthorizedClient( + OAuth2AuthorizedClient oAuth2AuthorizedClient, + Authentication principal, + HttpServletRequest request, + HttpServletResponse response + ) { + delegatedOAuth2AuthorizedClientRepository.saveAuthorizedClient( + oAuth2AuthorizedClient, principal, request, response); + } + + @Override + @SuppressWarnings("unchecked") + public T loadAuthorizedClient( + String id, + Authentication principal, + HttpServletRequest request + ) { + OAuth2AuthorizedClient result = + delegatedOAuth2AuthorizedClientRepository.loadAuthorizedClient(id, principal, request); + if (result != null) { + return (T) result; + } + if (azureClientRegistrationRepository.isAuthorizedClient(id)) { + OAuth2AuthorizedClient defaultOAuth2AuthorizedClient = + loadAuthorizedClient(defaultClientRegistrationId(), principal, request); + return (T) toOauth2AuthorizedClient(defaultOAuth2AuthorizedClient, id, principal); + } + return null; + } + + private String defaultClientRegistrationId() { + return azureClientRegistrationRepository.defaultClient().getClientRegistration().getRegistrationId(); + } + + private OAuth2AuthorizedClient toOauth2AuthorizedClient( + OAuth2AuthorizedClient oAuth2AuthorizedClient, + String id, + Authentication principal + ) { + OAuth2RefreshToken oAuth2RefreshToken = Optional.ofNullable(oAuth2AuthorizedClient) + .map(OAuth2AuthorizedClient::getRefreshToken) + .orElse(null); + if (oAuth2RefreshToken == null) { + return null; + } + OAuth2AccessToken accessToken = new OAuth2AccessToken( + OAuth2AccessToken.TokenType.BEARER, + "non-access-token", + Instant.MIN, + Instant.now().minus(100, ChronoUnit.DAYS) + ); + return new OAuth2AuthorizedClient( + azureClientRegistrationRepository.findByRegistrationId(id), + principal.getName(), + accessToken, + oAuth2RefreshToken + ); + } + + @Override + public void removeAuthorizedClient( + String id, + Authentication principal, + HttpServletRequest request, + HttpServletResponse response + ) { + delegatedOAuth2AuthorizedClientRepository.removeAuthorizedClient(id, principal, request, response); + } +} diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/implementation/AzureOAuth2WebSecurityConfigurerAdapter.java b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/implementation/AzureOAuth2WebSecurityConfigurerAdapter.java new file mode 100644 index 0000000000000..77127de6b20f0 --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/implementation/AzureOAuth2WebSecurityConfigurerAdapter.java @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.aad.implementation; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.oauth2.client.endpoint.DefaultAuthorizationCodeTokenResponseClient; +import org.springframework.security.oauth2.client.endpoint.OAuth2AccessTokenResponseClient; +import org.springframework.security.oauth2.client.endpoint.OAuth2AuthorizationCodeGrantRequest; + +import java.util.Optional; + +public abstract class AzureOAuth2WebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter { + + @Autowired + private AzureClientRegistrationRepository azureClientRegistrationRepository; + + @Override + protected void configure(HttpSecurity http) throws Exception { + http.oauth2Login().tokenEndpoint().accessTokenResponseClient(accessTokenResponseClient()); + } + + protected OAuth2AccessTokenResponseClient accessTokenResponseClient() { + DefaultAuthorizationCodeTokenResponseClient result = new DefaultAuthorizationCodeTokenResponseClient(); + Optional.ofNullable(azureClientRegistrationRepository) + .map(AzureClientRegistrationRepository::defaultClient) + .map(AzureOAuth2AuthorizationCodeGrantRequestEntityConverter::new) + .ifPresent(result::setRequestEntityConverter); + return result; + } +} diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/implementation/DefaultClient.java b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/implementation/DefaultClient.java new file mode 100644 index 0000000000000..3c552b77f6999 --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/implementation/DefaultClient.java @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.aad.implementation; + +import org.springframework.security.oauth2.client.registration.ClientRegistration; + +import java.util.Arrays; +import java.util.List; + +public class DefaultClient { + + private final ClientRegistration clientRegistration; + private final String[] scope; + + public DefaultClient(ClientRegistration clientRegistration, String[] scope) { + this.clientRegistration = clientRegistration; + this.scope = scope.clone(); + } + + public ClientRegistration getClientRegistration() { + return clientRegistration; + } + + public String[] getScope() { + return scope.clone(); + } + + public List getScopeList() { + return Arrays.asList(scope); + } +} diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/implementation/IdentityEndpoints.java b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/implementation/IdentityEndpoints.java new file mode 100644 index 0000000000000..81c98dbed9f43 --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/implementation/IdentityEndpoints.java @@ -0,0 +1,43 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.aad.implementation; + +import com.nimbusds.oauth2.sdk.util.StringUtils; + +public class IdentityEndpoints { + + private static final String IDENTITY_PLATFORM = "https://login.microsoftonline.com/"; + private static final String AUTHORIZATION_ENDPOINT = "/oauth2/v2.0/authorize"; + private static final String TOKEN_ENDPOINT = "/oauth2/v2.0/token"; + private static final String JWK_SET_ENDPOINT = "/discovery/v2.0/keys"; + + private final String baseUri; + + public IdentityEndpoints() { + this(IDENTITY_PLATFORM); + } + + public IdentityEndpoints(String baseUri) { + if (StringUtils.isBlank(baseUri)) { + baseUri = IDENTITY_PLATFORM; + } + this.baseUri = addSlash(baseUri); + } + + private String addSlash(String uri) { + return uri.endsWith("/") ? uri : uri + "/"; + } + + public String authorizationEndpoint(String tenant) { + return baseUri + tenant + AUTHORIZATION_ENDPOINT; + } + + public String tokenEndpoint(String tenant) { + return baseUri + tenant + TOKEN_ENDPOINT; + } + + public String jwkSetEndpoint(String tenant) { + return baseUri + tenant + JWK_SET_ENDPOINT; + } +} diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/autoconfigure/aad/AADAuthenticationProperties.java b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/autoconfigure/aad/AADAuthenticationProperties.java index 3cc59da933d5b..f1dbcd56948f4 100644 --- a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/autoconfigure/aad/AADAuthenticationProperties.java +++ b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/autoconfigure/aad/AADAuthenticationProperties.java @@ -3,6 +3,7 @@ package com.azure.spring.autoconfigure.aad; +import com.azure.spring.aad.implementation.AuthorizationProperties; import com.nimbusds.jose.jwk.source.RemoteJWKSet; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -15,7 +16,9 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.concurrent.TimeUnit; @@ -33,6 +36,10 @@ public class AADAuthenticationProperties { private static final String GROUP_RELATIONSHIP_DIRECT = "direct"; private static final String GROUP_RELATIONSHIP_TRANSITIVE = "transitive"; + private String uri; + + private Map authorization = new HashMap<>(); + /** * Default UserGroup configuration. */ @@ -64,8 +71,10 @@ public class AADAuthenticationProperties { /** * Optional. scope doc: * https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-permissions-and-consent#scopes-and-permissions + * @deprecated Please use "azure.activedirectory.authorization.client-registration-id.scope" instead. */ - private List scope = Arrays.asList("openid", "https://graph.microsoft.com/user.read", "profile"); + @Deprecated + private List scope = Arrays.asList("openid", "profile", "https://graph.microsoft.com/user.read"); /** * App ID URI which might be used in the "aud" claim of an id_token. @@ -260,6 +269,22 @@ public void validateUserGroupProperties() { } } + public void setUri(String uri) { + this.uri = uri; + } + + public String getUri() { + return uri; + } + + public void setAuthorization(Map authorization) { + this.authorization = authorization; + } + + public Map getAuthorization() { + return authorization; + } + public UserGroupProperties getUserGroup() { return userGroup; } @@ -300,10 +325,20 @@ public void setRedirectUriTemplate(String redirectUriTemplate) { this.redirectUriTemplate = redirectUriTemplate; } + /** + * @param scope scope + * @deprecated Please use "azure.activedirectory.authorization.client-registration-id.scope" instead. + */ + @Deprecated public void setScope(List scope) { this.scope = scope; } + /** + * @return scope + * @deprecated Please use "azure.activedirectory.authorization.client-registration-id.scope" instead. + */ + @Deprecated public List getScope() { return scope; } diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/autoconfigure/aad/AADOAuth2AutoConfiguration.java b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/autoconfigure/aad/AADOAuth2AutoConfiguration.java deleted file mode 100644 index ad7b737d3f7be..0000000000000 --- a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/autoconfigure/aad/AADOAuth2AutoConfiguration.java +++ /dev/null @@ -1,147 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -package com.azure.spring.autoconfigure.aad; - -import com.azure.spring.telemetry.TelemetrySender; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -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.context.properties.EnableConfigurationProperties; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.PropertySource; -import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest; -import org.springframework.security.oauth2.client.registration.ClientRegistration; -import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; -import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository; -import org.springframework.security.oauth2.client.userinfo.OAuth2UserService; -import org.springframework.security.oauth2.core.AuthorizationGrantType; -import org.springframework.security.oauth2.core.ClientAuthenticationMethod; -import org.springframework.security.oauth2.core.oidc.user.OidcUser; -import org.springframework.util.Assert; -import org.springframework.util.ClassUtils; - -import javax.annotation.PostConstruct; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; - -import static com.azure.spring.telemetry.TelemetryData.SERVICE_NAME; -import static com.azure.spring.telemetry.TelemetryData.getClassPackageSimpleName; - -/** - * {@link EnableAutoConfiguration Auto-configuration} for Azure Active Authentication OAuth 2.0. - *

- * The configuration will be activated when configured: - * 1. {@literal azure.activedirectory.client-id} - * 2. {@literal azure.activedirectory.client-secret} - * 3. {@literal azure.activedirectory.tenant-id} - * client-id, client-secret, tenant-id used in ClientRegistration. - * client-id, client-secret also used to get graphApiToken, then get groups. - *

- * A OAuth2 user service {@link AADOAuth2UserService} will be auto-configured by specifying {@literal - * azure.activedirectory.user-group.allowed-groups} property. - */ -@Configuration -@ConditionalOnResource(resources = "classpath:aad.enable.config") -@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET) -@ConditionalOnProperty(prefix = "azure.activedirectory", value = {"client-id", "client-secret", "tenant-id"}) -@PropertySource(value = "classpath:service-endpoints.properties") -@EnableConfigurationProperties({ AADAuthenticationProperties.class, ServiceEndpointsProperties.class }) -public class AADOAuth2AutoConfiguration { - - private static final Logger LOGGER = LoggerFactory.getLogger(AADOAuth2AutoConfiguration.class); - private final AADAuthenticationProperties aadAuthenticationProperties; - private final ServiceEndpointsProperties serviceEndpointsProperties; - - public AADOAuth2AutoConfiguration(AADAuthenticationProperties aadAuthProperties, - ServiceEndpointsProperties serviceEndpointsProperties) { - this.aadAuthenticationProperties = aadAuthProperties; - this.serviceEndpointsProperties = serviceEndpointsProperties; - } - - @Bean - @ConditionalOnProperty(prefix = "azure.activedirectory.user-group", value = "allowed-groups") - public OAuth2UserService oidcUserService() { - return new AADOAuth2UserService(aadAuthenticationProperties, serviceEndpointsProperties); - } - - @Bean - public ClientRegistrationRepository clientRegistrationRepository() { - return new InMemoryClientRegistrationRepository(azureClientRegistration()); - } - - private ClientRegistration azureClientRegistration() { - String tenantId = aadAuthenticationProperties.getTenantId().trim(); - Assert.hasText(tenantId, "azure.activedirectory.tenant-id should have text."); - Assert.doesNotContain(tenantId, " ", "azure.activedirectory.tenant-id should not contain ' '."); - Assert.doesNotContain(tenantId, "/", "azure.activedirectory.tenant-id should not contain '/'."); - - String redirectUriTemplate = Optional.of(aadAuthenticationProperties) - .map(AADAuthenticationProperties::getRedirectUriTemplate) - .orElse("{baseUrl}/login/oauth2/code/{registrationId}"); - - List scope = aadAuthenticationProperties.getScope(); - if (!scope.toString().contains(".default")) { - if (aadAuthenticationProperties.allowedGroupsConfigured() - && !scope.contains("https://graph.microsoft.com/user.read") - ) { - scope.add("https://graph.microsoft.com/user.read"); - LOGGER.warn("scope 'https://graph.microsoft.com/user.read' has been added."); - } - if (!scope.contains("openid")) { - scope.add("openid"); - LOGGER.warn("scope 'openid' has been added."); - } - if (!scope.contains("profile")) { - scope.add("profile"); - LOGGER.warn("scope 'profile' has been added."); - } - } - - return ClientRegistration.withRegistrationId("azure") - .clientId(aadAuthenticationProperties.getClientId()) - .clientSecret(aadAuthenticationProperties.getClientSecret()) - .clientAuthenticationMethod(ClientAuthenticationMethod.POST) - .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) - .redirectUriTemplate(redirectUriTemplate) - .scope(scope) - .authorizationUri( - String.format( - "https://login.microsoftonline.com/%s/oauth2/v2.0/authorize", - tenantId - ) - ) - .tokenUri( - String.format( - "https://login.microsoftonline.com/%s/oauth2/v2.0/token", - tenantId - ) - ) - .userInfoUri("https://graph.microsoft.com/oidc/userinfo") - .userNameAttributeName(AADTokenClaim.NAME) - .jwkSetUri( - String.format( - "https://login.microsoftonline.com/%s/discovery/v2.0/keys", - tenantId - ) - ) - .clientName("Azure") - .build(); - } - - @PostConstruct - private void sendTelemetry() { - if (aadAuthenticationProperties.isAllowTelemetry()) { - final Map events = new HashMap<>(); - final TelemetrySender sender = new TelemetrySender(); - events.put(SERVICE_NAME, getClassPackageSimpleName(AADOAuth2AutoConfiguration.class)); - sender.send(ClassUtils.getUserClass(getClass()).getSimpleName(), events); - } - } -} diff --git a/sdk/spring/azure-spring-boot/src/main/resources/META-INF/spring.factories b/sdk/spring/azure-spring-boot/src/main/resources/META-INF/spring.factories index 8436a89f9eaea..2f189a2583bed 100644 --- a/sdk/spring/azure-spring-boot/src/main/resources/META-INF/spring.factories +++ b/sdk/spring/azure-spring-boot/src/main/resources/META-INF/spring.factories @@ -1,14 +1,15 @@ org.springframework.boot.env.EnvironmentPostProcessor=com.azure.spring.cloudfoundry.environment.VcapProcessor -org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.azure.spring.autoconfigure.cosmos.CosmosAutoConfiguration,\ -com.azure.spring.autoconfigure.cosmos.CosmosRepositoriesAutoConfiguration,\ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +com.azure.spring.aad.implementation.AzureActiveDirectoryAutoConfiguration, \ +com.azure.spring.autoconfigure.aad.AADAuthenticationFilterAutoConfiguration, \ +com.azure.spring.autoconfigure.b2c.AADB2CAutoConfiguration,\ +com.azure.spring.autoconfigure.cosmos.CosmosAutoConfiguration,\ +com.azure.spring.autoconfigure.cosmos.CosmosHealthConfiguration,\ com.azure.spring.autoconfigure.cosmos.CosmosReactiveRepositoriesAutoConfiguration,\ +com.azure.spring.autoconfigure.cosmos.CosmosRepositoriesAutoConfiguration,\ com.azure.spring.autoconfigure.gremlin.GremlinAutoConfiguration,\ com.azure.spring.autoconfigure.gremlin.GremlinRepositoriesAutoConfiguration,\ -com.azure.spring.autoconfigure.aad.AADAuthenticationFilterAutoConfiguration,\ -com.azure.spring.autoconfigure.aad.AADOAuth2AutoConfiguration,\ -com.azure.spring.autoconfigure.b2c.AADB2CAutoConfiguration,\ com.azure.spring.autoconfigure.jms.ServiceBusJMSAutoConfiguration,\ com.azure.spring.autoconfigure.storage.StorageAutoConfiguration,\ -com.azure.spring.autoconfigure.cosmos.CosmosHealthConfiguration,\ com.azure.spring.autoconfigure.storage.StorageHealthConfiguration,\ com.azure.spring.keyvault.KeyVaultHealthConfiguration diff --git a/sdk/spring/azure-spring-boot/src/test/java/com/azure/spring/autoconfigure/aad/AADOAuth2ConfigTest.java b/sdk/spring/azure-spring-boot/src/test/java/com/azure/spring/autoconfigure/aad/AADOAuth2ConfigTest.java index 295c47928c88b..04f7e428605e6 100644 --- a/sdk/spring/azure-spring-boot/src/test/java/com/azure/spring/autoconfigure/aad/AADOAuth2ConfigTest.java +++ b/sdk/spring/azure-spring-boot/src/test/java/com/azure/spring/autoconfigure/aad/AADOAuth2ConfigTest.java @@ -3,6 +3,8 @@ package com.azure.spring.autoconfigure.aad; +import com.azure.spring.aad.implementation.AzureActiveDirectoryAutoConfiguration; +import com.azure.spring.aad.implementation.AzureClientRegistrationRepository; import org.junit.After; import org.junit.Assert; import org.junit.Before; @@ -16,19 +18,18 @@ import org.springframework.core.io.support.ResourcePropertySource; import org.springframework.mock.env.MockPropertySource; import org.springframework.security.oauth2.client.registration.ClientRegistration; -import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; import org.springframework.security.oauth2.client.userinfo.OAuth2UserService; import org.springframework.test.context.support.TestPropertySourceUtils; import org.springframework.web.context.support.AnnotationConfigWebApplicationContext; -import static org.junit.jupiter.api.Assertions.assertTrue; - import java.util.Arrays; +import java.util.Collections; import java.util.HashSet; import java.util.Map; import java.util.Set; import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.assertEquals; public class AADOAuth2ConfigTest { private static final String AAD_OAUTH2_MINIMUM_PROPS = "aad-backend-oauth2-minimum.properties"; @@ -56,7 +57,7 @@ public void clear() { @Test public void noOAuth2UserServiceBeanCreatedIfPropsNotConfigured() { final AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext(); - context.register(AADOAuth2AutoConfiguration.class); + context.register(AzureActiveDirectoryAutoConfiguration.class); context.refresh(); exception.expect(NoSuchBeanDefinitionException.class); @@ -124,37 +125,38 @@ public void testEndpointsPropertiesLoadAndOverridable() { @Test public void testScopePropertyConfiguredWithDynamicPermissions() { - testContext = initTestContext("azure.activedirectory.scope=email"); - + testContext = initTestContext("azure.activedirectory.authorization.graph.scope=email"); final Environment environment = testContext.getEnvironment(); - assertThat(environment.getProperty("azure.activedirectory.scope")) - .isEqualTo("email"); - - final ClientRegistrationRepository clientRegistrationRepository = - testContext.getBean(ClientRegistrationRepository.class); - final ClientRegistration clientRegistration = clientRegistrationRepository.findByRegistrationId("azure"); - final Set createdScopes = clientRegistration.getScopes(); - final Set expectedScopes = new HashSet<>(Arrays.asList("email", "openid", "profile", - "https://graph.microsoft.com/user.read")); - assertTrue(createdScopes.equals(expectedScopes)); - + assertThat(environment.getProperty("azure.activedirectory.authorization.graph.scope")).isEqualTo("email"); + + final AzureClientRegistrationRepository azureClientRegistrationRepository = + testContext.getBean(AzureClientRegistrationRepository.class); + final ClientRegistration clientRegistration = azureClientRegistrationRepository.findByRegistrationId("azure"); + final Set actualScopes = clientRegistration.getScopes(); + final Set expectedScopes = new HashSet<>(Arrays.asList("openid", "profile", "offline_access", "email")); + assertEquals(expectedScopes, actualScopes); } @Test public void testScopePropertyConfiguredWithStaticPermissions() { - testContext = initTestContext("azure.activedirectory.scope=1111/.default"); - + testContext = initTestContext("azure.activedirectory.authorization.graph.scope=1111/.default"); final Environment environment = testContext.getEnvironment(); - assertThat(environment.getProperty("azure.activedirectory.scope")) - .isEqualTo("1111/.default"); - - final ClientRegistrationRepository clientRegistrationRepository = - testContext.getBean(ClientRegistrationRepository.class); - final ClientRegistration clientRegistration = clientRegistrationRepository.findByRegistrationId("azure"); - final Set createdScopes = clientRegistration.getScopes(); - final Set expectedScopes = new HashSet<>(Arrays.asList("1111/.default")); - assertTrue(createdScopes.equals(expectedScopes)); - + assertThat(environment.getProperty("azure.activedirectory.authorization.graph.scope")).isEqualTo("1111/" + + ".default"); + + final AzureClientRegistrationRepository azureClientRegistrationRepository = + testContext.getBean(AzureClientRegistrationRepository.class); + final ClientRegistration clientRegistration = azureClientRegistrationRepository.findByRegistrationId("azure"); + final Set actualScopes = clientRegistration.getScopes(); + final Set expectedScopes = + new HashSet<>(Arrays.asList("openid", "profile", "offline_access", "1111/.default")); + assertEquals(expectedScopes, actualScopes); + + final ClientRegistration graphClientRegistration = + azureClientRegistrationRepository.findByRegistrationId("graph"); + final Set graphActualScopes = graphClientRegistration.getScopes(); + final Set graphExpectedScopes = new HashSet<>(Collections.singletonList("1111/.default")); + assertEquals(graphExpectedScopes, graphActualScopes); } private AnnotationConfigWebApplicationContext initTestContext(String... environment) { @@ -167,7 +169,7 @@ private AnnotationConfigWebApplicationContext initTestContext(String... environm TestPropertySourceUtils.addInlinedPropertiesToEnvironment(context, environment); } - context.register(AADOAuth2AutoConfiguration.class); + context.register(AzureActiveDirectoryAutoConfiguration.class); context.refresh(); return context;