rolesToGrantedAuthorities(JSONArray roles) {
+ return roles.stream()
+ .filter(Objects::nonNull)
+ .map(s -> new SimpleGrantedAuthority(ROLE_PREFIX + s))
+ .collect(Collectors.toSet());
+ }
+}
diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/aad/AADAuthenticationFailureHandler.java b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/aad/AADAuthenticationFailureHandler.java
new file mode 100644
index 000000000000..907ad8f9d834
--- /dev/null
+++ b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/aad/AADAuthenticationFailureHandler.java
@@ -0,0 +1,58 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.microsoft.azure.spring.autoconfigure.aad;
+
+import com.microsoft.aad.msal4j.MsalServiceException;
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
+import org.springframework.security.web.authentication.AuthenticationFailureHandler;
+import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
+import org.springframework.security.web.savedrequest.DefaultSavedRequest;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+
+/**
+ * Strategy used to handle a failed authentication attempt.
+ *
+ * To redirect the user to the authentication page to allow them to try again when conditional access policy is
+ * configured on Azure Active Directory.
+ */
+public class AADAuthenticationFailureHandler implements AuthenticationFailureHandler {
+
+ private AuthenticationFailureHandler defaultHandler;
+
+ public AADAuthenticationFailureHandler() {
+ this.defaultHandler = new SimpleUrlAuthenticationFailureHandler(AADConstantsHelper.FAILURE_DEFAULT_URL);
+ }
+
+ @Override
+ public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
+ AuthenticationException exception) throws IOException, ServletException {
+ final OAuth2AuthenticationException targetException = (OAuth2AuthenticationException) exception;
+ //handle conditional access policy
+ if (AADConstantsHelper.CONDITIONAL_ACCESS_POLICY.equals((targetException.getError().getErrorCode()))) {
+ //get infos
+ final Throwable cause = targetException.getCause();
+ if (cause instanceof MsalServiceException) {
+ final MsalServiceException e = (MsalServiceException) cause;
+ final String claims = e.claims();
+
+ final DefaultSavedRequest savedRequest = (DefaultSavedRequest) request.getSession()
+ .getAttribute(AADConstantsHelper.SAVED_REQUEST);
+ final String savedRequestUrl = savedRequest.getRedirectUrl();
+ //put claims into session
+ request.getSession().setAttribute(AADConstantsHelper.CAP_CLAIMS, claims);
+ //redirect
+ response.setStatus(302);
+ response.sendRedirect(savedRequestUrl);
+ return;
+ }
+ }
+ //default handle logic
+ defaultHandler.onAuthenticationFailure(request, response, exception);
+ }
+}
diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/aad/AADAuthenticationFilter.java b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/aad/AADAuthenticationFilter.java
new file mode 100644
index 000000000000..d0a4de95ab4a
--- /dev/null
+++ b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/aad/AADAuthenticationFilter.java
@@ -0,0 +1,124 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.microsoft.azure.spring.autoconfigure.aad;
+
+import com.microsoft.aad.msal4j.MsalServiceException;
+import com.nimbusds.jose.JOSEException;
+import com.nimbusds.jose.jwk.source.JWKSetCache;
+import com.nimbusds.jose.proc.BadJOSEException;
+import com.nimbusds.jose.util.ResourceRetriever;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken;
+import org.springframework.web.filter.OncePerRequestFilter;
+
+import javax.naming.ServiceUnavailableException;
+import javax.servlet.FilterChain;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.net.MalformedURLException;
+import java.text.ParseException;
+
+/**
+ * A stateful authentication filter which uses Microsoft Graph groups to authorize. Both ID token and access token are
+ * supported. In the case of access token, only access token issued for the exact same application this filter used for
+ * could be accepted, e.g. access token issued for Microsoft Graph could not be processed by users' application.
+ */
+public class AADAuthenticationFilter extends OncePerRequestFilter {
+ private static final Logger LOGGER = LoggerFactory.getLogger(AADAuthenticationFilter.class);
+
+ private static final String CURRENT_USER_PRINCIPAL = "CURRENT_USER_PRINCIPAL";
+ private static final String CURRENT_USER_PRINCIPAL_GRAPHAPI_TOKEN = "CURRENT_USER_PRINCIPAL_GRAPHAPI_TOKEN";
+ private static final String CURRENT_USER_PRINCIPAL_JWT_TOKEN = "CURRENT_USER_PRINCIPAL_JWT_TOKEN";
+
+ private static final String TOKEN_HEADER = "Authorization";
+ private static final String TOKEN_TYPE = "Bearer ";
+
+ private AADAuthenticationProperties aadAuthProps;
+ private ServiceEndpointsProperties serviceEndpointsProps;
+ private UserPrincipalManager principalManager;
+
+ public AADAuthenticationFilter(AADAuthenticationProperties aadAuthProps,
+ ServiceEndpointsProperties serviceEndpointsProps,
+ ResourceRetriever resourceRetriever) {
+ this.aadAuthProps = aadAuthProps;
+ this.serviceEndpointsProps = serviceEndpointsProps;
+ this.principalManager = new UserPrincipalManager(serviceEndpointsProps, aadAuthProps, resourceRetriever, false);
+ }
+
+ public AADAuthenticationFilter(AADAuthenticationProperties aadAuthProps,
+ ServiceEndpointsProperties serviceEndpointsProps,
+ ResourceRetriever resourceRetriever,
+ JWKSetCache jwkSetCache) {
+ this.aadAuthProps = aadAuthProps;
+ this.serviceEndpointsProps = serviceEndpointsProps;
+ this.principalManager = new UserPrincipalManager(serviceEndpointsProps,
+ aadAuthProps,
+ resourceRetriever,
+ false,
+ jwkSetCache);
+ }
+
+ @Override
+ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
+ FilterChain filterChain) throws ServletException, IOException {
+ final String authHeader = request.getHeader(TOKEN_HEADER);
+
+ if (authHeader != null && authHeader.startsWith(TOKEN_TYPE)) {
+ try {
+ final String idToken = authHeader.replace(TOKEN_TYPE, "");
+ UserPrincipal principal = (UserPrincipal) request
+ .getSession().getAttribute(CURRENT_USER_PRINCIPAL);
+ String graphApiToken = (String) request
+ .getSession().getAttribute(CURRENT_USER_PRINCIPAL_GRAPHAPI_TOKEN);
+ final String currentToken = (String) request
+ .getSession().getAttribute(CURRENT_USER_PRINCIPAL_JWT_TOKEN);
+
+ final AzureADGraphClient client = new AzureADGraphClient(aadAuthProps.getClientId(),
+ aadAuthProps.getClientSecret(), aadAuthProps, serviceEndpointsProps);
+
+ if (principal == null
+ || graphApiToken == null
+ || graphApiToken.isEmpty()
+ || !idToken.equals(currentToken)) {
+ principal = principalManager.buildUserPrincipal(idToken);
+
+ final String tenantId = principal.getClaim().toString();
+ graphApiToken = client.acquireTokenForGraphApi(idToken, tenantId).accessToken();
+
+ principal.setUserGroups(client.getGroups(graphApiToken));
+
+ request.getSession().setAttribute(CURRENT_USER_PRINCIPAL, principal);
+ request.getSession().setAttribute(CURRENT_USER_PRINCIPAL_GRAPHAPI_TOKEN, graphApiToken);
+ request.getSession().setAttribute(CURRENT_USER_PRINCIPAL_JWT_TOKEN, idToken);
+ }
+
+ final Authentication authentication = new PreAuthenticatedAuthenticationToken(
+ principal, null, client.convertGroupsToGrantedAuthorities(principal.getUserGroups()));
+
+ authentication.setAuthenticated(true);
+ LOGGER.info("Request token verification success. {}", authentication);
+ SecurityContextHolder.getContext().setAuthentication(authentication);
+ } catch (MalformedURLException | ParseException | BadJOSEException | JOSEException ex) {
+ LOGGER.error("Failed to initialize UserPrincipal.", ex);
+ throw new ServletException(ex);
+ } catch (ServiceUnavailableException ex) {
+ LOGGER.error("Failed to acquire graph api token.", ex);
+ throw new ServletException(ex);
+ } catch (MsalServiceException ex) {
+ if (ex.claims() != null && !ex.claims().isEmpty()) {
+ throw new ServletException("Handle conditional access policy", ex);
+ } else {
+ throw ex;
+ }
+ }
+ }
+
+ filterChain.doFilter(request, response);
+ }
+}
diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/aad/AADAuthenticationFilterAutoConfiguration.java b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/aad/AADAuthenticationFilterAutoConfiguration.java
new file mode 100644
index 000000000000..6856d63a1e0d
--- /dev/null
+++ b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/aad/AADAuthenticationFilterAutoConfiguration.java
@@ -0,0 +1,115 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.microsoft.azure.spring.autoconfigure.aad;
+
+import static com.microsoft.azure.telemetry.TelemetryData.SERVICE_NAME;
+import static com.microsoft.azure.telemetry.TelemetryData.getClassPackageSimpleName;
+
+import com.microsoft.azure.telemetry.TelemetrySender;
+import com.nimbusds.jose.jwk.source.DefaultJWKSetCache;
+import com.nimbusds.jose.jwk.source.JWKSetCache;
+import com.nimbusds.jose.util.DefaultResourceRetriever;
+import com.nimbusds.jose.util.ResourceRetriever;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+import javax.annotation.PostConstruct;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
+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.util.ClassUtils;
+
+/**
+ * {@link EnableAutoConfiguration Auto-configuration} for Azure Active Authentication filters .
+ *
+ * The configuration will not be activated if no {@literal azure.activedirectory.client-id} property provided.
+ *
+ * A stateless filter {@link AADAppRoleStatelessAuthenticationFilter} will be auto-configured by specifying
+ * {@literal azure.activedirectory.session-stateless=true}. Otherwise, {@link AADAuthenticationFilter} will be
+ * configured.
+ */
+@Configuration
+@ConditionalOnWebApplication
+@ConditionalOnResource(resources = "classpath:aad.enable.config")
+@ConditionalOnProperty(prefix = AADAuthenticationFilterAutoConfiguration.PROPERTY_PREFIX, value = {"client-id"})
+@EnableConfigurationProperties({AADAuthenticationProperties.class, ServiceEndpointsProperties.class})
+@PropertySource(value = "classpath:serviceEndpoints.properties")
+public class AADAuthenticationFilterAutoConfiguration {
+ private static final Logger LOG = LoggerFactory.getLogger(AADAuthenticationProperties.class);
+
+ public static final String PROPERTY_PREFIX = "azure.activedirectory";
+ private static final String PROPERTY_SESSION_STATELESS = "session-stateless";
+
+ private final AADAuthenticationProperties aadAuthProps;
+
+ private final ServiceEndpointsProperties serviceEndpointsProps;
+
+ public AADAuthenticationFilterAutoConfiguration(AADAuthenticationProperties aadAuthFilterProps,
+ ServiceEndpointsProperties serviceEndpointsProps) {
+ this.aadAuthProps = aadAuthFilterProps;
+ this.serviceEndpointsProps = serviceEndpointsProps;
+ }
+
+ /**
+ * Declare AADAuthenticationFilter bean.
+ *
+ * @return AADAuthenticationFilter bean
+ */
+ @Bean
+ @ConditionalOnMissingBean(AADAuthenticationFilter.class)
+ @ConditionalOnProperty(prefix = PROPERTY_PREFIX, value = {"client-id", "client-secret"})
+ @ConditionalOnExpression("${azure.activedirectory.session-stateless:false} == false")
+ public AADAuthenticationFilter azureADJwtTokenFilter() {
+ LOG.info("AzureADJwtTokenFilter Constructor.");
+ return new AADAuthenticationFilter(aadAuthProps,
+ serviceEndpointsProps,
+ getJWTResourceRetriever(),
+ getJWKSetCache());
+ }
+
+ @Bean
+ @ConditionalOnMissingBean(AADAppRoleStatelessAuthenticationFilter.class)
+ @ConditionalOnProperty(prefix = PROPERTY_PREFIX, value = PROPERTY_SESSION_STATELESS, havingValue = "true")
+ public AADAppRoleStatelessAuthenticationFilter azureADStatelessAuthFilter(ResourceRetriever resourceRetriever) {
+ LOG.info("Creating AzureADStatelessAuthFilter bean.");
+ final boolean useExplicitAudienceCheck = true;
+ return new AADAppRoleStatelessAuthenticationFilter(new UserPrincipalManager(serviceEndpointsProps, aadAuthProps,
+ resourceRetriever, useExplicitAudienceCheck));
+ }
+
+ @Bean
+ @ConditionalOnMissingBean(ResourceRetriever.class)
+ public ResourceRetriever getJWTResourceRetriever() {
+ return new DefaultResourceRetriever(aadAuthProps.getJwtConnectTimeout(), aadAuthProps.getJwtReadTimeout(),
+ aadAuthProps.getJwtSizeLimit());
+ }
+
+ @Bean
+ @ConditionalOnMissingBean(JWKSetCache.class)
+ public JWKSetCache getJWKSetCache() {
+ return new DefaultJWKSetCache(aadAuthProps.getJwkSetCacheLifespan(), TimeUnit.MILLISECONDS);
+ }
+
+ @PostConstruct
+ private void sendTelemetry() {
+ if (aadAuthProps.isAllowTelemetry()) {
+ final Map events = new HashMap<>();
+ final TelemetrySender sender = new TelemetrySender();
+
+ events.put(SERVICE_NAME, getClassPackageSimpleName(AADAuthenticationFilterAutoConfiguration.class));
+
+ sender.send(ClassUtils.getUserClass(getClass()).getSimpleName(), events);
+ }
+ }
+}
diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/aad/AADAuthenticationProperties.java b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/aad/AADAuthenticationProperties.java
new file mode 100644
index 000000000000..70429c9469dd
--- /dev/null
+++ b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/aad/AADAuthenticationProperties.java
@@ -0,0 +1,315 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.microsoft.azure.spring.autoconfigure.aad;
+
+import com.nimbusds.jose.jwk.source.RemoteJWKSet;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.boot.context.properties.DeprecatedConfigurationProperty;
+import org.springframework.validation.annotation.Validated;
+
+import javax.annotation.PostConstruct;
+import javax.validation.constraints.NotEmpty;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Configuration properties for Azure Active Directory Authentication.
+ */
+@Validated
+@ConfigurationProperties("azure.activedirectory")
+public class AADAuthenticationProperties {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(AADAuthenticationProperties.class);
+
+ private static final String DEFAULT_SERVICE_ENVIRONMENT = "global";
+
+ private static final long DEFAULT_JWKSETCACHE_LIFESPAN = TimeUnit.MINUTES.toMillis(5);
+
+ /**
+ * Default UserGroup configuration.
+ */
+ private UserGroupProperties userGroup = new UserGroupProperties();
+
+
+ /**
+ * Azure service environment/region name, e.g., cn, global
+ */
+ private String environment = DEFAULT_SERVICE_ENVIRONMENT;
+ /**
+ * Registered application ID in Azure AD.
+ * Must be configured when OAuth2 authentication is done in front end
+ */
+ private String clientId;
+ /**
+ * API Access Key of the registered application.
+ * Must be configured when OAuth2 authentication is done in front end
+ */
+ private String clientSecret;
+
+ /**
+ * Azure AD groups.
+ */
+ private List activeDirectoryGroups = new ArrayList<>();
+
+ /**
+ * App ID URI which might be used in the "aud" claim of an id_token.
+ */
+ private String appIdUri;
+
+ /**
+ * Connection Timeout for the JWKSet Remote URL call.
+ */
+ private int jwtConnectTimeout = RemoteJWKSet.DEFAULT_HTTP_CONNECT_TIMEOUT; /* milliseconds */
+
+ /**
+ * Read Timeout for the JWKSet Remote URL call.
+ */
+ private int jwtReadTimeout = RemoteJWKSet.DEFAULT_HTTP_READ_TIMEOUT; /* milliseconds */
+
+ /**
+ * Size limit in Bytes of the JWKSet Remote URL call.
+ */
+ private int jwtSizeLimit = RemoteJWKSet.DEFAULT_HTTP_SIZE_LIMIT; /* bytes */
+
+ /**
+ * The lifespan of the cached JWK set before it expires, default is 5 minutes.
+ */
+ private long jwkSetCacheLifespan = DEFAULT_JWKSETCACHE_LIFESPAN;
+
+ /**
+ * Azure Tenant ID.
+ */
+ private String tenantId;
+
+ /**
+ * If Telemetry events should be published to Azure AD.
+ */
+ private boolean allowTelemetry = true;
+
+ /**
+ * If true activates the stateless auth filter {@link AADAppRoleStatelessAuthenticationFilter}.
+ * The default is false which activates {@link AADAuthenticationFilter}.
+ */
+ private Boolean sessionStateless = false;
+
+ @DeprecatedConfigurationProperty(reason = "Configuration moved to UserGroup class to keep UserGroup properties "
+ + "together", replacement = "azure.activedirectory.user-group.allowed-groups")
+ public List getActiveDirectoryGroups() {
+ return activeDirectoryGroups;
+ }
+ /**
+ * Properties dedicated to changing the behavior of how the groups are mapped from the Azure AD response. Depending
+ * on the graph API used the object will not be the same.
+ */
+ public static class UserGroupProperties {
+
+ /**
+ * Expected UserGroups that an authority will be granted to if found in the response from the MemeberOf Graph
+ * API Call.
+ */
+ private List allowedGroups = new ArrayList<>();
+
+ /**
+ * Key of the JSON Node to get from the Azure AD response object that will be checked to contain the {@code
+ * azure.activedirectory.user-group.value} to signify that this node is a valid {@code UserGroup}.
+ */
+ @NotEmpty
+ private String key = "objectType";
+
+ /**
+ * Value of the JSON Node identified by the {@code azure.activedirectory.user-group.key} to validate the JSON
+ * Node is a UserGroup.
+ */
+ @NotEmpty
+ private String value = "Group";
+
+ /**
+ * Key of the JSON Node containing the Azure Object ID for the {@code UserGroup}.
+ */
+ @NotEmpty
+ private String objectIDKey = "objectId";
+
+ public List getAllowedGroups() {
+ return allowedGroups;
+ }
+
+ public void setAllowedGroups(List allowedGroups) {
+ this.allowedGroups = allowedGroups;
+ }
+
+ public String getKey() {
+ return key;
+ }
+
+ public void setKey(String key) {
+ this.key = key;
+ }
+
+ public String getValue() {
+ return value;
+ }
+
+ public void setValue(String value) {
+ this.value = value;
+ }
+
+ public String getObjectIDKey() {
+ return objectIDKey;
+ }
+
+ public void setObjectIDKey(String objectIDKey) {
+ this.objectIDKey = objectIDKey;
+ }
+
+ @Override
+ public String toString() {
+ return "UserGroupProperties{"
+ + "allowedGroups=" + allowedGroups
+ + ", key='" + key + '\''
+ + ", value='" + value + '\''
+ + ", objectIDKey='" + objectIDKey + '\''
+ + '}';
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ UserGroupProperties that = (UserGroupProperties) o;
+ return Objects.equals(allowedGroups, that.allowedGroups)
+ && Objects.equals(key, that.key)
+ && Objects.equals(value, that.value)
+ && Objects.equals(objectIDKey, that.objectIDKey);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(allowedGroups, key, value, objectIDKey);
+ }
+ }
+
+
+ /**
+ * Validates at least one of the user group properties are populated.
+ */
+ @PostConstruct
+ public void validateUserGroupProperties() {
+ if (this.sessionStateless) {
+ if (!this.activeDirectoryGroups.isEmpty()) {
+ LOGGER.warn("Group names are not supported if you set 'sessionSateless' to 'true'.");
+ }
+ } else if (this.activeDirectoryGroups.isEmpty() && this.getUserGroup().getAllowedGroups().isEmpty()) {
+ throw new IllegalStateException("One of the User Group Properties must be populated. "
+ + "Please populate azure.activedirectory.user-group.allowed-groups");
+ }
+ }
+
+ public UserGroupProperties getUserGroup() {
+ return userGroup;
+ }
+
+ public void setUserGroup(UserGroupProperties userGroup) {
+ this.userGroup = userGroup;
+ }
+
+ public String getEnvironment() {
+ return environment;
+ }
+
+ public void setEnvironment(String environment) {
+ this.environment = environment;
+ }
+
+ public String getClientId() {
+ return clientId;
+ }
+
+ public void setClientId(String clientId) {
+ this.clientId = clientId;
+ }
+
+ public String getClientSecret() {
+ return clientSecret;
+ }
+
+ public void setClientSecret(String clientSecret) {
+ this.clientSecret = clientSecret;
+ }
+
+ public void setActiveDirectoryGroups(List activeDirectoryGroups) {
+ this.activeDirectoryGroups = activeDirectoryGroups;
+ }
+
+ public String getAppIdUri() {
+ return appIdUri;
+ }
+
+ public void setAppIdUri(String appIdUri) {
+ this.appIdUri = appIdUri;
+ }
+
+ public int getJwtConnectTimeout() {
+ return jwtConnectTimeout;
+ }
+
+ public void setJwtConnectTimeout(int jwtConnectTimeout) {
+ this.jwtConnectTimeout = jwtConnectTimeout;
+ }
+
+ public int getJwtReadTimeout() {
+ return jwtReadTimeout;
+ }
+
+ public void setJwtReadTimeout(int jwtReadTimeout) {
+ this.jwtReadTimeout = jwtReadTimeout;
+ }
+
+ public int getJwtSizeLimit() {
+ return jwtSizeLimit;
+ }
+
+ public void setJwtSizeLimit(int jwtSizeLimit) {
+ this.jwtSizeLimit = jwtSizeLimit;
+ }
+
+ public long getJwkSetCacheLifespan() {
+ return jwkSetCacheLifespan;
+ }
+
+ public void setJwkSetCacheLifespan(long jwkSetCacheLifespan) {
+ this.jwkSetCacheLifespan = jwkSetCacheLifespan;
+ }
+
+ public String getTenantId() {
+ return tenantId;
+ }
+
+ public void setTenantId(String tenantId) {
+ this.tenantId = tenantId;
+ }
+
+ public boolean isAllowTelemetry() {
+ return allowTelemetry;
+ }
+
+ public void setAllowTelemetry(boolean allowTelemetry) {
+ this.allowTelemetry = allowTelemetry;
+ }
+
+ public Boolean getSessionStateless() {
+ return sessionStateless;
+ }
+
+ public void setSessionStateless(Boolean sessionStateless) {
+ this.sessionStateless = sessionStateless;
+ }
+}
diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/aad/AADConstantsHelper.java b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/aad/AADConstantsHelper.java
new file mode 100644
index 000000000000..58fd05b2e1d1
--- /dev/null
+++ b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/aad/AADConstantsHelper.java
@@ -0,0 +1,12 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.microsoft.azure.spring.autoconfigure.aad;
+
+public class AADConstantsHelper {
+ public static final String CONDITIONAL_ACCESS_POLICY = "conditional_access_policy";
+ public static final String SAVED_REQUEST = "SPRING_SECURITY_SAVED_REQUEST";
+ public static final String CAP_CLAIMS = "CAP_Claims";
+ public static final String CLAIMS = "claims";
+ public static final String FAILURE_DEFAULT_URL = "/login?error";
+}
diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/aad/AADOAuth2AuthorizationRequestResolver.java b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/aad/AADOAuth2AuthorizationRequestResolver.java
new file mode 100644
index 000000000000..87038b1f0d06
--- /dev/null
+++ b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/aad/AADOAuth2AuthorizationRequestResolver.java
@@ -0,0 +1,74 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.microsoft.azure.spring.autoconfigure.aad;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.servlet.http.HttpServletRequest;
+
+import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
+import org.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizationRequestResolver;
+import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestRedirectFilter;
+import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestResolver;
+import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
+import org.springframework.util.StringUtils;
+
+/**
+ * To add conditional policy claims to authorization URL.
+ */
+public class AADOAuth2AuthorizationRequestResolver implements OAuth2AuthorizationRequestResolver {
+ private OAuth2AuthorizationRequestResolver defaultResolver;
+
+ public AADOAuth2AuthorizationRequestResolver(ClientRegistrationRepository clientRegistrationRepository) {
+ this.defaultResolver = new DefaultOAuth2AuthorizationRequestResolver(clientRegistrationRepository,
+ OAuth2AuthorizationRequestRedirectFilter.DEFAULT_AUTHORIZATION_REQUEST_BASE_URI);
+ }
+
+ @Override
+ public OAuth2AuthorizationRequest resolve(HttpServletRequest request) {
+ return addClaims(request, defaultResolver.resolve(request));
+ }
+
+ @Override
+ public OAuth2AuthorizationRequest resolve(HttpServletRequest request, String clientRegistrationId) {
+ return addClaims(request, defaultResolver.resolve(request, clientRegistrationId));
+ }
+
+ //add claims to authorization-url
+ private OAuth2AuthorizationRequest addClaims(HttpServletRequest request,
+ OAuth2AuthorizationRequest req) {
+ if (req == null || request == null) {
+ return req;
+ }
+
+ final String conditionalAccessPolicyClaims = getConditionalAccessPolicyClaims(request);
+ if (StringUtils.isEmpty(conditionalAccessPolicyClaims)) {
+ return req;
+ }
+
+ final Map extraParams = new HashMap<>();
+ if (req.getAdditionalParameters() != null) {
+ extraParams.putAll(req.getAdditionalParameters());
+ }
+ extraParams.put(AADConstantsHelper.CLAIMS, conditionalAccessPolicyClaims);
+ return OAuth2AuthorizationRequest
+ .from(req)
+ .additionalParameters(extraParams)
+ .build();
+ }
+
+ private String getConditionalAccessPolicyClaims(HttpServletRequest request) {
+ //claims just for one use
+ final String claims = request.getSession()
+ .getAttribute(AADConstantsHelper.CAP_CLAIMS) == null ? "" : (String) request
+ .getSession()
+ .getAttribute(AADConstantsHelper.CAP_CLAIMS);
+ //remove claims in session
+ if (!StringUtils.isEmpty(claims)) {
+ request.getSession().removeAttribute(AADConstantsHelper.CAP_CLAIMS);
+ }
+ return claims;
+ }
+}
diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/aad/AADOAuth2AutoConfiguration.java b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/aad/AADOAuth2AutoConfiguration.java
new file mode 100644
index 000000000000..47345fb454f2
--- /dev/null
+++ b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/aad/AADOAuth2AutoConfiguration.java
@@ -0,0 +1,71 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.microsoft.azure.spring.autoconfigure.aad;
+
+import com.microsoft.azure.telemetry.TelemetrySender;
+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.userinfo.OAuth2UserService;
+import org.springframework.security.oauth2.core.oidc.user.OidcUser;
+import org.springframework.util.ClassUtils;
+
+import javax.annotation.PostConstruct;
+import java.util.HashMap;
+import java.util.Map;
+
+import static com.microsoft.azure.telemetry.TelemetryData.SERVICE_NAME;
+import static com.microsoft.azure.telemetry.TelemetryData.getClassPackageSimpleName;
+
+/**
+ * {@link EnableAutoConfiguration Auto-configuration} for Azure Active Authentication OAuth 2.0.
+ *
+ * The configuration will not be activated if no {@literal azure.activedirectory.tenant-id} property provided.
+ *
+ * A OAuth2 user service {@link AADOAuth2UserService} will be auto-configured by specifying
+ * {@literal azure.activedirectory.active-directory-groups} property.
+ */
+@Configuration
+@ConditionalOnResource(resources = "classpath:aad.enable.config")
+@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
+@ConditionalOnProperty(prefix = "azure.activedirectory", value = "tenant-id")
+@PropertySource("classpath:/aad-oauth2-common.properties")
+@PropertySource(value = "classpath:serviceEndpoints.properties")
+@EnableConfigurationProperties({AADAuthenticationProperties.class, ServiceEndpointsProperties.class})
+public class AADOAuth2AutoConfiguration {
+
+ private final AADAuthenticationProperties aadAuthProps;
+
+ private final ServiceEndpointsProperties serviceEndpointsProps;
+
+ public AADOAuth2AutoConfiguration(AADAuthenticationProperties aadAuthProperties,
+ ServiceEndpointsProperties serviceEndpointsProps) {
+ this.aadAuthProps = aadAuthProperties;
+ this.serviceEndpointsProps = serviceEndpointsProps;
+ }
+
+ @Bean
+ @ConditionalOnProperty(prefix = "azure.activedirectory", value = "active-directory-groups")
+ public OAuth2UserService oidcUserService() {
+ return new AADOAuth2UserService(aadAuthProps, serviceEndpointsProps);
+ }
+
+ @PostConstruct
+ private void sendTelemetry() {
+ if (aadAuthProps.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/java/com/microsoft/azure/spring/autoconfigure/aad/AADOAuth2UserService.java b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/aad/AADOAuth2UserService.java
new file mode 100644
index 000000000000..16b4863a39bf
--- /dev/null
+++ b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/aad/AADOAuth2UserService.java
@@ -0,0 +1,100 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.microsoft.azure.spring.autoconfigure.aad;
+
+import com.microsoft.aad.msal4j.MsalServiceException;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest;
+import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserService;
+import org.springframework.security.oauth2.client.registration.ClientRegistration;
+import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
+import org.springframework.security.oauth2.client.userinfo.OAuth2UserService;
+import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
+import org.springframework.security.oauth2.core.OAuth2Error;
+import org.springframework.security.oauth2.core.oidc.OidcIdToken;
+import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser;
+import org.springframework.security.oauth2.core.oidc.user.OidcUser;
+import org.springframework.util.StringUtils;
+
+import javax.naming.ServiceUnavailableException;
+import java.io.IOException;
+import java.net.MalformedURLException;
+import java.util.Set;
+
+/**
+ * This implementation will retrieve group info of user from Microsoft Graph and map groups to {@link GrantedAuthority}.
+ */
+public class AADOAuth2UserService implements OAuth2UserService {
+ private static final String CONDITIONAL_ACCESS_POLICY = "conditional_access_policy";
+ private static final String INVALID_REQUEST = "invalid_request";
+ private static final String SERVER_ERROR = "server_error";
+ private static final String DEFAULT_USERNAME_ATTR_NAME = "name";
+
+ private AADAuthenticationProperties aadAuthProps;
+ private ServiceEndpointsProperties serviceEndpointsProps;
+
+ public AADOAuth2UserService(AADAuthenticationProperties aadAuthProps,
+ ServiceEndpointsProperties serviceEndpointsProps) {
+ this.aadAuthProps = aadAuthProps;
+ this.serviceEndpointsProps = serviceEndpointsProps;
+ }
+
+ @Override
+ public OidcUser loadUser(OidcUserRequest userRequest) throws OAuth2AuthenticationException {
+ final OidcUserService delegate = new OidcUserService();
+
+ // Delegate to the default implementation for loading a user
+ OidcUser oidcUser = delegate.loadUser(userRequest);
+ final OidcIdToken idToken = userRequest.getIdToken();
+
+ final String graphApiToken;
+ final Set mappedAuthorities;
+
+ try {
+ // https://github.com/MicrosoftDocs/azure-docs/issues/8121#issuecomment-387090099
+ // In AAD App Registration configure oauth2AllowImplicitFlow to true
+ final ClientRegistration registration = userRequest.getClientRegistration();
+
+ final AzureADGraphClient graphClient = new AzureADGraphClient(registration.getClientId(),
+ registration.getClientSecret(), aadAuthProps, serviceEndpointsProps);
+
+ graphApiToken = graphClient.acquireTokenForGraphApi(idToken.getTokenValue(),
+ aadAuthProps.getTenantId()).accessToken();
+
+ mappedAuthorities = graphClient.getGrantedAuthorities(graphApiToken);
+ } catch (MalformedURLException e) {
+ throw wrapException(INVALID_REQUEST, "Failed to acquire token for Graph API.", null, e);
+ } catch (ServiceUnavailableException e) {
+ throw wrapException(SERVER_ERROR, "Failed to acquire token for Graph API.", null, e);
+ } catch (IOException e) {
+ throw wrapException(SERVER_ERROR, "Failed to map group to authorities.", null, e);
+ } catch (MsalServiceException e) {
+ if (e.claims() != null && !e.claims().isEmpty()) {
+ throw wrapException(CONDITIONAL_ACCESS_POLICY, "Handle conditional access policy", null, e);
+ } else {
+ throw e;
+ }
+ }
+
+ // Create a copy of oidcUser but use the mappedAuthorities instead
+ oidcUser = new DefaultOidcUser(mappedAuthorities, oidcUser.getIdToken(), getUserNameAttrName(userRequest));
+
+ return oidcUser;
+ }
+
+ private OAuth2AuthenticationException wrapException(String errorCode, String errDesc, String uri, Exception e) {
+ final OAuth2Error oAuth2Error = new OAuth2Error(errorCode, errDesc, uri);
+ throw new OAuth2AuthenticationException(oAuth2Error, e);
+ }
+
+ private String getUserNameAttrName(OAuth2UserRequest userRequest) {
+ String userNameAttrName = userRequest.getClientRegistration()
+ .getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName();
+ if (StringUtils.isEmpty(userNameAttrName)) {
+ userNameAttrName = DEFAULT_USERNAME_ATTR_NAME;
+ }
+
+ return userNameAttrName;
+ }
+}
diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/aad/AzureADGraphClient.java b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/aad/AzureADGraphClient.java
new file mode 100644
index 000000000000..500d96d3aa29
--- /dev/null
+++ b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/aad/AzureADGraphClient.java
@@ -0,0 +1,241 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.microsoft.azure.spring.autoconfigure.aad;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.microsoft.aad.msal4j.ClientCredentialFactory;
+import com.microsoft.aad.msal4j.ConfidentialClientApplication;
+import com.microsoft.aad.msal4j.IAuthenticationResult;
+import com.microsoft.aad.msal4j.IClientCredential;
+import com.microsoft.aad.msal4j.MsalServiceException;
+import com.microsoft.aad.msal4j.OnBehalfOfParameters;
+import com.microsoft.aad.msal4j.UserAssertion;
+import com.nimbusds.oauth2.sdk.http.HTTPResponse;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.MediaType;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.authority.SimpleGrantedAuthority;
+
+import javax.naming.ServiceUnavailableException;
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.net.HttpURLConnection;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.stream.Collectors;
+import java.util.stream.StreamSupport;
+
+/**
+ * Microsoft Graph client encapsulation.
+ */
+public class AzureADGraphClient {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(AzureADGraphClient.class);
+ private static final SimpleGrantedAuthority DEFAULT_AUTHORITY = new SimpleGrantedAuthority("ROLE_USER");
+ private static final String DEFAULT_ROLE_PREFIX = "ROLE_";
+ private static final String MICROSOFT_GRAPH_SCOPE = "https://graph.microsoft.com/user.read";
+ private static final String AAD_GRAPH_API_SCOPE = "https://graph.windows.net/user.read";
+
+ private final String clientId;
+ private final String clientSecret;
+ private final ServiceEndpoints serviceEndpoints;
+ private final AADAuthenticationProperties aadAuthenticationProperties;
+
+ private static final String V2_VERSION_ENV_FLAG = "v2-graph";
+ private boolean aadMicrosoftGraphApiBool;
+
+ public AzureADGraphClient(String clientId, String clientSecret, AADAuthenticationProperties aadAuthProps,
+ ServiceEndpointsProperties serviceEndpointsProps) {
+ this.clientId = clientId;
+ this.clientSecret = clientSecret;
+ this.aadAuthenticationProperties = aadAuthProps;
+ this.serviceEndpoints = serviceEndpointsProps.getServiceEndpoints(aadAuthProps.getEnvironment());
+
+ this.initAADMicrosoftGraphApiBool(aadAuthProps.getEnvironment());
+ }
+
+ private void initAADMicrosoftGraphApiBool(String endpointEnv) {
+ this.aadMicrosoftGraphApiBool = endpointEnv.contains(V2_VERSION_ENV_FLAG);
+ }
+
+ private String getUserMembershipsV1(String accessToken) throws IOException {
+ final URL url = new URL(serviceEndpoints.getAadMembershipRestUri());
+ final HttpURLConnection conn = (HttpURLConnection) url.openConnection();
+ // Set the appropriate header fields in the request header.
+
+ if (this.aadMicrosoftGraphApiBool) {
+ conn.setRequestMethod(HttpMethod.GET.toString());
+ conn.setRequestProperty(HttpHeaders.AUTHORIZATION, String.format("Bearer %s", accessToken));
+ conn.setRequestProperty(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE);
+ conn.setRequestProperty(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE);
+ } else {
+ conn.setRequestMethod(HttpMethod.GET.toString());
+ conn.setRequestProperty("api-version", "1.6");
+ conn.setRequestProperty(HttpHeaders.AUTHORIZATION, accessToken);
+ conn.setRequestProperty(HttpHeaders.ACCEPT, "application/json;odata=minimalmetadata");
+ }
+ final String responseInJson = getResponseStringFromConn(conn);
+ final int responseCode = conn.getResponseCode();
+ if (responseCode == HTTPResponse.SC_OK) {
+ return responseInJson;
+ } else {
+ throw new IllegalStateException("Response is not "
+ + HTTPResponse.SC_OK + ", response json: " + responseInJson);
+ }
+ }
+
+ private static String getResponseStringFromConn(HttpURLConnection conn) throws IOException {
+
+ try (BufferedReader reader = new BufferedReader(
+ new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8))) {
+ final StringBuilder stringBuffer = new StringBuilder();
+ String line;
+ while ((line = reader.readLine()) != null) {
+ stringBuffer.append(line);
+ }
+ return stringBuffer.toString();
+ }
+ }
+
+ public List getGroups(String graphApiToken) throws IOException {
+ return loadUserGroups(graphApiToken);
+ }
+
+ private List loadUserGroups(String graphApiToken) throws IOException {
+ final String responseInJson = getUserMembershipsV1(graphApiToken);
+ final List lUserGroups = new ArrayList<>();
+ final ObjectMapper objectMapper = JacksonObjectMapperFactory.getInstance();
+ final JsonNode rootNode = objectMapper.readValue(responseInJson, JsonNode.class);
+ final JsonNode valuesNode = rootNode.get("value");
+
+ if (valuesNode != null) {
+ lUserGroups
+ .addAll(StreamSupport.stream(valuesNode.spliterator(), false).filter(this::isMatchingUserGroupKey)
+ .map(node -> {
+ final String objectID = node.
+ get(aadAuthenticationProperties.getUserGroup().getObjectIDKey()).asText();
+ final String displayName = node.get("displayName").asText();
+ return new UserGroup(objectID, displayName);
+ }).collect(Collectors.toList()));
+
+ }
+
+ return lUserGroups;
+ }
+
+ /**
+ * Checks that the JSON Node is a valid User Group to extract User Groups from
+ *
+ * @param node - json node to look for a key/value to equate against the
+ * {@link AADAuthenticationProperties.UserGroupProperties}
+ * @return true if the json node contains the correct key, and expected value to identify a user group.
+ */
+ private boolean isMatchingUserGroupKey(final JsonNode node) {
+ return node.get(aadAuthenticationProperties.getUserGroup().getKey()).asText()
+ .equals(aadAuthenticationProperties.getUserGroup().getValue());
+ }
+
+ public Set getGrantedAuthorities(String graphApiToken) throws IOException {
+ // Fetch the authority information from the protected resource using accessToken
+ final List groups = getGroups(graphApiToken);
+
+ // Map the authority information to one or more GrantedAuthority's and add it to mappedAuthorities
+ return convertGroupsToGrantedAuthorities(groups);
+ }
+
+
+ /**
+ * Converts UserGroup list to Set of GrantedAuthorities
+ *
+ * @param groups user groups
+ * @return granted authorities
+ */
+ public Set convertGroupsToGrantedAuthorities(final List groups) {
+ // Map the authority information to one or more GrantedAuthority's and add it to mappedAuthorities
+ final Set mappedAuthorities = groups.stream().filter(this::isValidUserGroupToGrantAuthority)
+ .map(userGroup -> new SimpleGrantedAuthority(DEFAULT_ROLE_PREFIX + userGroup.getDisplayName()))
+ .collect(Collectors.toCollection(LinkedHashSet::new));
+ if (mappedAuthorities.isEmpty()) {
+ mappedAuthorities.add(DEFAULT_AUTHORITY);
+ }
+
+ return mappedAuthorities;
+ }
+
+ /**
+ * Determines if this is a valid {@link UserGroup} to build to a GrantedAuthority.
+ *
+ * If the {@link AADAuthenticationProperties.UserGroupProperties#getAllowedGroups()} or the {@link
+ * AADAuthenticationProperties#getActiveDirectoryGroups()} contains the {@link UserGroup#getDisplayName()} return
+ * true.
+ *
+ * @param group - User Group to check if valid to grant an authority to.
+ * @return true if either of the allowed-groups or active-directory-groups contains the UserGroup display name
+ */
+ private boolean isValidUserGroupToGrantAuthority(final UserGroup group) {
+ return aadAuthenticationProperties.getUserGroup().getAllowedGroups().contains(group.getDisplayName())
+ || aadAuthenticationProperties.getActiveDirectoryGroups().contains(group.getDisplayName());
+ }
+
+ public IAuthenticationResult acquireTokenForGraphApi(String idToken, String tenantId)
+ throws ServiceUnavailableException {
+ final IClientCredential clientCredential = ClientCredentialFactory.createFromSecret(clientSecret);
+ final UserAssertion assertion = new UserAssertion(idToken);
+
+ IAuthenticationResult result = null;
+ ExecutorService service = null;
+ try {
+ service = Executors.newFixedThreadPool(1);
+
+ final ConfidentialClientApplication application = ConfidentialClientApplication
+ .builder(clientId, clientCredential)
+ .authority(serviceEndpoints.getAadSigninUri() + tenantId + "/")
+ .build();
+
+ final Set scopes = new HashSet<>();
+ scopes.add(aadMicrosoftGraphApiBool ? MICROSOFT_GRAPH_SCOPE : AAD_GRAPH_API_SCOPE);
+
+ final OnBehalfOfParameters onBehalfOfParameters = OnBehalfOfParameters
+ .builder(scopes, assertion)
+ .build();
+
+ final CompletableFuture future = application.acquireToken(onBehalfOfParameters);
+ result = future.get();
+ } catch (ExecutionException | InterruptedException | MalformedURLException e) {
+ // handle conditional access policy
+ final Throwable cause = e.getCause();
+ if (cause instanceof MsalServiceException) {
+ final MsalServiceException exception = (MsalServiceException) cause;
+ if (exception.claims() != null && !exception.claims().isEmpty()) {
+ throw exception;
+ }
+ }
+ LOGGER.error("acquire on behalf of token for graph api error", e);
+ } finally {
+ if (service != null) {
+ service.shutdown();
+ }
+ }
+
+ if (result == null) {
+ throw new ServiceUnavailableException("unable to acquire on-behalf-of token for client " + clientId);
+ }
+ return result;
+ }
+}
diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/aad/JacksonObjectMapperFactory.java b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/aad/JacksonObjectMapperFactory.java
new file mode 100644
index 000000000000..9b2e8101be88
--- /dev/null
+++ b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/aad/JacksonObjectMapperFactory.java
@@ -0,0 +1,20 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.microsoft.azure.spring.autoconfigure.aad;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+public final class JacksonObjectMapperFactory {
+
+ private JacksonObjectMapperFactory() {
+ }
+
+ public static ObjectMapper getInstance() {
+ return SingletonHelper.INSTANCE;
+ }
+
+ private static class SingletonHelper {
+ private static final ObjectMapper INSTANCE = new ObjectMapper();
+ }
+}
diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/aad/ServiceEndpoints.java b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/aad/ServiceEndpoints.java
new file mode 100644
index 000000000000..157f0d4e6fa7
--- /dev/null
+++ b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/aad/ServiceEndpoints.java
@@ -0,0 +1,47 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.microsoft.azure.spring.autoconfigure.aad;
+
+
+/**
+ * Pojo file to store the service urls for different azure services.
+ */
+public class ServiceEndpoints {
+ private String aadSigninUri;
+ private String aadGraphApiUri;
+ private String aadKeyDiscoveryUri;
+ private String aadMembershipRestUri;
+
+ public String getAadSigninUri() {
+ return aadSigninUri;
+ }
+
+ public void setAadSigninUri(String aadSigninUri) {
+ this.aadSigninUri = aadSigninUri;
+ }
+
+ public String getAadGraphApiUri() {
+ return aadGraphApiUri;
+ }
+
+ public void setAadGraphApiUri(String aadGraphApiUri) {
+ this.aadGraphApiUri = aadGraphApiUri;
+ }
+
+ public String getAadKeyDiscoveryUri() {
+ return aadKeyDiscoveryUri;
+ }
+
+ public void setAadKeyDiscoveryUri(String aadKeyDiscoveryUri) {
+ this.aadKeyDiscoveryUri = aadKeyDiscoveryUri;
+ }
+
+ public String getAadMembershipRestUri() {
+ return aadMembershipRestUri;
+ }
+
+ public void setAadMembershipRestUri(String aadMembershipRestUri) {
+ this.aadMembershipRestUri = aadMembershipRestUri;
+ }
+}
diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/aad/ServiceEndpointsProperties.java b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/aad/ServiceEndpointsProperties.java
new file mode 100644
index 000000000000..e8cbb168d3bf
--- /dev/null
+++ b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/aad/ServiceEndpointsProperties.java
@@ -0,0 +1,38 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.microsoft.azure.spring.autoconfigure.aad;
+
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.util.Assert;
+
+import java.util.HashMap;
+import java.util.Map;
+
+@Configuration
+@ConfigurationProperties("azure.service")
+public class ServiceEndpointsProperties {
+ private Map endpoints = new HashMap<>();
+
+ public Map getEndpoints() {
+ return endpoints;
+ }
+
+ /**
+ * Get ServiceEndpoints data for the given environment.
+ *
+ * @param environment the environment of the cloud service
+ * @return The ServiceEndpoints data for the given azure service environment
+ */
+ public ServiceEndpoints getServiceEndpoints(String environment) {
+ Assert.notEmpty(endpoints, "No service endpoints found");
+
+ if (!endpoints.containsKey(environment)) {
+ throw new IllegalArgumentException(environment + " is not found in the configuration,"
+ + " only following environments are supported: " + endpoints.keySet());
+ }
+
+ return endpoints.get(environment);
+ }
+}
diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/aad/UserGroup.java b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/aad/UserGroup.java
new file mode 100644
index 000000000000..360ae47ebb29
--- /dev/null
+++ b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/aad/UserGroup.java
@@ -0,0 +1,45 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.microsoft.azure.spring.autoconfigure.aad;
+
+import java.io.Serializable;
+import java.util.Objects;
+
+public class UserGroup implements Serializable {
+ private static final long serialVersionUID = 9064197572478554735L;
+
+ private String objectID;
+ private String displayName;
+
+ public UserGroup(String objectID, String displayName) {
+ this.objectID = objectID;
+ this.displayName = displayName;
+ }
+
+ public String getDisplayName() {
+ return displayName;
+ }
+
+ public String getObjectID() {
+ return objectID;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (o == this) {
+ return true;
+ }
+ if (!(o instanceof UserGroup)) {
+ return false;
+ }
+ final UserGroup group = (UserGroup) o;
+ return this.getDisplayName().equals(group.getDisplayName())
+ && this.getObjectID().equals(group.getObjectID());
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(objectID, displayName);
+ }
+}
diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/aad/UserPrincipal.java b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/aad/UserPrincipal.java
new file mode 100644
index 000000000000..0d7fa096bee6
--- /dev/null
+++ b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/aad/UserPrincipal.java
@@ -0,0 +1,76 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.microsoft.azure.spring.autoconfigure.aad;
+
+import com.nimbusds.jose.JWSObject;
+import com.nimbusds.jwt.JWTClaimsSet;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+public class UserPrincipal implements Serializable {
+ private static final long serialVersionUID = -3725690847771476854L;
+
+ private JWSObject jwsObject;
+ private JWTClaimsSet jwtClaimsSet;
+ private List userGroups = new ArrayList<>();
+
+ public UserPrincipal(JWSObject jwsObject, JWTClaimsSet jwtClaimsSet) {
+ this.jwsObject = jwsObject;
+ this.jwtClaimsSet = jwtClaimsSet;
+ }
+
+ // claimset
+ public String getIssuer() {
+ return jwtClaimsSet == null ? null : jwtClaimsSet.getIssuer();
+ }
+
+ public String getSubject() {
+ return jwtClaimsSet == null ? null : jwtClaimsSet.getSubject();
+ }
+
+ public Map getClaims() {
+ return jwtClaimsSet == null ? null : jwtClaimsSet.getClaims();
+ }
+
+ public Object getClaim() {
+ return jwtClaimsSet == null ? null : jwtClaimsSet.getClaim("tid");
+ }
+
+ public Object getClaim(String name) {
+ return jwtClaimsSet == null ? null : jwtClaimsSet.getClaim(name);
+ }
+
+ public String getUpn() {
+ return jwtClaimsSet == null ? null : (String) jwtClaimsSet.getClaim("upn");
+ }
+
+ public String getUniqueName() {
+ return jwtClaimsSet == null ? null : (String) jwtClaimsSet.getClaim("unique_name");
+ }
+
+ public String getName() {
+ return jwtClaimsSet == null ? null : (String) jwtClaimsSet.getClaim("name");
+ }
+
+ // header
+ public String getKid() {
+ return jwsObject == null ? null : jwsObject.getHeader().getKeyID();
+ }
+
+ public void setUserGroups(List groups) {
+ this.userGroups = groups;
+ }
+
+ public List getUserGroups() {
+ return this.userGroups;
+ }
+
+ public boolean isMemberOf(UserGroup group) {
+ return !(userGroups == null || userGroups.isEmpty()) && userGroups.contains(group);
+ }
+}
+
diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/aad/UserPrincipalManager.java b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/aad/UserPrincipalManager.java
new file mode 100644
index 000000000000..0a1353693e45
--- /dev/null
+++ b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/aad/UserPrincipalManager.java
@@ -0,0 +1,170 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.microsoft.azure.spring.autoconfigure.aad;
+
+import com.nimbusds.jose.JOSEException;
+import com.nimbusds.jose.JWSAlgorithm;
+import com.nimbusds.jose.JWSObject;
+import com.nimbusds.jose.jwk.source.JWKSetCache;
+import com.nimbusds.jose.jwk.source.JWKSource;
+import com.nimbusds.jose.jwk.source.RemoteJWKSet;
+import com.nimbusds.jose.proc.BadJOSEException;
+import com.nimbusds.jose.proc.JWSKeySelector;
+import com.nimbusds.jose.proc.JWSVerificationKeySelector;
+import com.nimbusds.jose.proc.SecurityContext;
+import com.nimbusds.jose.util.ResourceRetriever;
+import com.nimbusds.jwt.JWTClaimsSet;
+import com.nimbusds.jwt.proc.BadJWTException;
+import com.nimbusds.jwt.proc.ConfigurableJWTProcessor;
+import com.nimbusds.jwt.proc.DefaultJWTClaimsVerifier;
+import com.nimbusds.jwt.proc.DefaultJWTProcessor;
+import com.nimbusds.jwt.proc.JWTClaimsSetVerifier;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.text.ParseException;
+import java.util.HashSet;
+import java.util.Optional;
+import java.util.Set;
+
+/**
+ * A user principal manager to load user info from JWT.
+ */
+public class UserPrincipalManager {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(UserPrincipalManager.class);
+
+ 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 final JWKSource keySource;
+ private final AADAuthenticationProperties aadAuthProps;
+ private final Boolean explicitAudienceCheck;
+ private final Set validAudiences = new HashSet<>();
+
+ /**ø
+ * Creates a new {@link UserPrincipalManager} with a predefined {@link JWKSource}.
+ *
+ * This is helpful in cases the JWK is not a remote JWKSet or for unit testing.
+ *
+ * @param keySource - {@link JWKSource} containing at least one key
+ */
+ public UserPrincipalManager(JWKSource keySource) {
+ this.keySource = keySource;
+ this.explicitAudienceCheck = false;
+ this.aadAuthProps = null;
+ }
+
+ /**
+ * Create a new {@link UserPrincipalManager} based of the {@link ServiceEndpoints#getAadKeyDiscoveryUri()} and
+ * {@link AADAuthenticationProperties#getEnvironment()}.
+ *
+ * @param serviceEndpointsProps - used to retrieve the JWKS URL
+ * @param aadAuthProps - used to retrieve the environment.
+ * @param resourceRetriever - configures the {@link RemoteJWKSet} call.
+ * @param explicitAudienceCheck - explicit audience check
+ */
+ public UserPrincipalManager(ServiceEndpointsProperties serviceEndpointsProps,
+ AADAuthenticationProperties aadAuthProps,
+ ResourceRetriever resourceRetriever,
+ boolean explicitAudienceCheck) {
+ this.aadAuthProps = aadAuthProps;
+ this.explicitAudienceCheck = explicitAudienceCheck;
+ if (explicitAudienceCheck) {
+ // client-id for "normal" check
+ this.validAudiences.add(this.aadAuthProps.getClientId());
+ // app id uri for client credentials flow (server to server communication)
+ this.validAudiences.add(this.aadAuthProps.getAppIdUri());
+ }
+ try {
+ keySource = new RemoteJWKSet<>(new URL(serviceEndpointsProps
+ .getServiceEndpoints(aadAuthProps.getEnvironment()).getAadKeyDiscoveryUri()), resourceRetriever);
+ } catch (MalformedURLException e) {
+ LOGGER.error("Failed to parse active directory key discovery uri.", e);
+ throw new IllegalStateException("Failed to parse active directory key discovery uri.", e);
+ }
+ }
+
+ /**
+ * Create a new {@link UserPrincipalManager} based of the {@link ServiceEndpoints#getAadKeyDiscoveryUri()} and
+ * {@link AADAuthenticationProperties#getEnvironment()}.
+ *
+ * @param serviceEndpointsProps - used to retrieve the JWKS URL
+ * @param aadAuthProps - used to retrieve the environment.
+ * @param resourceRetriever - configures the {@link RemoteJWKSet} call.
+ * @param jwkSetCache - used to cache the JWK set for a finite time, default set to 5 minutes
+ * which matches constructor above if no jwkSetCache is passed in
+ * @param explicitAudienceCheck - explicit audience check
+ */
+ public UserPrincipalManager(ServiceEndpointsProperties serviceEndpointsProps,
+ AADAuthenticationProperties aadAuthProps,
+ ResourceRetriever resourceRetriever,
+ boolean explicitAudienceCheck,
+ JWKSetCache jwkSetCache) {
+ this.aadAuthProps = aadAuthProps;
+ this.explicitAudienceCheck = explicitAudienceCheck;
+ if (explicitAudienceCheck) {
+ // client-id for "normal" check
+ this.validAudiences.add(this.aadAuthProps.getClientId());
+ // app id uri for client credentials flow (server to server communication)
+ this.validAudiences.add(this.aadAuthProps.getAppIdUri());
+ }
+ try {
+ keySource = new RemoteJWKSet<>(new URL(serviceEndpointsProps
+ .getServiceEndpoints(aadAuthProps.getEnvironment()).getAadKeyDiscoveryUri()),
+ resourceRetriever,
+ jwkSetCache);
+ } catch (MalformedURLException e) {
+ LOGGER.error("Failed to parse active directory key discovery uri.", e);
+ throw new IllegalStateException("Failed to parse active directory key discovery uri.", e);
+ }
+ }
+
+ public UserPrincipal buildUserPrincipal(String idToken) throws ParseException, JOSEException, BadJOSEException {
+ final JWSObject jwsObject = JWSObject.parse(idToken);
+ final ConfigurableJWTProcessor validator =
+ getAadJwtTokenValidator(jwsObject.getHeader().getAlgorithm());
+ final JWTClaimsSet jwtClaimsSet = validator.process(idToken, null);
+ final JWTClaimsSetVerifier verifier = validator.getJWTClaimsSetVerifier();
+ verifier.verify(jwtClaimsSet, null);
+
+ return new UserPrincipal(jwsObject, jwtClaimsSet);
+ }
+
+ private ConfigurableJWTProcessor getAadJwtTokenValidator(JWSAlgorithm jwsAlgorithm) {
+ final ConfigurableJWTProcessor jwtProcessor = new DefaultJWTProcessor<>();
+
+ final JWSKeySelector keySelector =
+ new JWSVerificationKeySelector<>(jwsAlgorithm, keySource);
+ jwtProcessor.setJWSKeySelector(keySelector);
+
+ //TODO: would it make sense to inject it? and make it configurable or even allow to provide own implementation
+ jwtProcessor.setJWTClaimsSetVerifier(new DefaultJWTClaimsVerifier() {
+ @Override
+ public void verify(JWTClaimsSet claimsSet, SecurityContext ctx) throws BadJWTException {
+ super.verify(claimsSet, ctx);
+ final String issuer = claimsSet.getIssuer();
+ if (issuer == null || !(issuer.startsWith(LOGIN_MICROSOFT_ONLINE_ISSUER)
+ || issuer.startsWith(STS_WINDOWS_ISSUER)
+ || issuer.startsWith(STS_CHINA_CLOUD_API_ISSUER))) {
+ throw new BadJWTException("Invalid token issuer");
+ }
+ if (explicitAudienceCheck) {
+ final Optional matchedAudience = claimsSet.getAudience().stream()
+ .filter(UserPrincipalManager.this.validAudiences::contains).findFirst();
+ if (matchedAudience.isPresent()) {
+ LOGGER.debug("Matched audience [{}]", matchedAudience.get());
+ } else {
+ throw new BadJWTException("Invalid token audience. Provided value " + claimsSet.getAudience()
+ + "does not match neither client-id nor AppIdUri.");
+ }
+ }
+ }
+ });
+ return jwtProcessor;
+ }
+}
diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/support/GetHashMac.java b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/support/GetHashMac.java
new file mode 100644
index 000000000000..16e6f692712b
--- /dev/null
+++ b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/support/GetHashMac.java
@@ -0,0 +1,113 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+/*
+ * Disclaimer:
+ * This class is copied from https://github.com/Microsoft/azure-tools-for-java/ with minor modification (fixing
+ * static analysis error).
+ * Location in the repo: /Utils/azuretools-core/src/com/microsoft/azuretools/azurecommons/util/GetHashMac.java
+ */
+
+package com.microsoft.azure.spring.support;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.Locale;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public final class GetHashMac {
+ public static final String MAC_REGEX = "([0-9A-Fa-f]{2}[:-]){5}[0-9A-Fa-f]{2}";
+ public static final String MAC_REGEX_ZERO = "([0]{2}[:-]){5}[0]{2}";
+ public static final String HASHED_MAC_REGEX = "[0-9a-f]{64}";
+
+ private GetHashMac() {
+ super();
+ }
+
+ public static boolean isValidHashMacFormat(String hashMac) {
+ if (hashMac == null || hashMac.isEmpty()) {
+ return false;
+ }
+
+ final Pattern hashedMacPattern = Pattern.compile(HASHED_MAC_REGEX);
+ final Matcher matcher = hashedMacPattern.matcher(hashMac);
+ return matcher.matches();
+ }
+
+ public static String getHashMac() {
+ final String rawMac = getRawMac();
+ if (rawMac == null || rawMac.isEmpty()) {
+ return null;
+ }
+
+ final Pattern pattern = Pattern.compile(MAC_REGEX);
+ final Pattern patternZero = Pattern.compile(MAC_REGEX_ZERO);
+ final Matcher matcher = pattern.matcher(rawMac);
+ String mac = "";
+ while (matcher.find()) {
+ mac = matcher.group(0);
+ if (!patternZero.matcher(mac).matches()) {
+ break;
+ }
+ }
+
+ return hash(mac);
+ }
+
+ private static String getRawMac() {
+ final StringBuilder ret = new StringBuilder();
+
+ final String os = System.getProperty("os.name");
+ String[] command = {"ifconfig", "-a"};
+ if (os != null && !os.isEmpty() && os.toLowerCase(Locale.US).startsWith("win")) {
+ command = new String[]{"getmac"};
+ }
+
+ try {
+ final ProcessBuilder builder = new ProcessBuilder(command);
+ final Process process = builder.start();
+
+ try (InputStream inputStream = process.getInputStream();
+ InputStreamReader inputStreamReader = new InputStreamReader(inputStream, StandardCharsets.UTF_8);
+ BufferedReader br = new BufferedReader(inputStreamReader)) {
+ String tmp;
+ while ((tmp = br.readLine()) != null) {
+ ret.append(tmp);
+ }
+ }
+ } catch (IOException e) {
+ return null;
+ }
+
+ return ret.toString();
+ }
+
+ private static String hash(String mac) {
+ if (mac == null || mac.isEmpty()) {
+ return null;
+ }
+
+ final String ret;
+ try {
+ final MessageDigest md = MessageDigest.getInstance("SHA-256");
+ final byte[] bytes = mac.getBytes(StandardCharsets.UTF_8);
+ md.update(bytes);
+ final byte[] bytesAfterDigest = md.digest();
+ final StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < bytesAfterDigest.length; i++) {
+ sb.append(Integer.toString((bytesAfterDigest[i] & 0xff) + 0x100, 16).substring(1));
+ }
+
+ ret = sb.toString();
+ } catch (NoSuchAlgorithmException ex) {
+ return null;
+ }
+
+ return ret;
+ }
+}
diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/support/UserAgent.java b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/support/UserAgent.java
new file mode 100644
index 000000000000..7dd24b4da4d4
--- /dev/null
+++ b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/support/UserAgent.java
@@ -0,0 +1,25 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.microsoft.azure.spring.support;
+
+/**
+ * Util class to generate user agent.
+ */
+public class UserAgent {
+ /**
+ * Generate UserAgent string for given service.
+ *
+ * @param serviceName Name of the service from which called this method.
+ * @param allowTelemetry Whether allows telemtry
+ * @return generated UserAgent string
+ */
+ public static String getUserAgent(String serviceName, boolean allowTelemetry) {
+ String macAddress = "Not Collected";
+ if (allowTelemetry) {
+ macAddress = GetHashMac.getHashMac();
+ }
+
+ return String.format(serviceName + " MacAddressHash:%s", macAddress);
+ }
+}
diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/telemetry/TelemetryData.java b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/telemetry/TelemetryData.java
new file mode 100644
index 000000000000..2c590973fb48
--- /dev/null
+++ b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/telemetry/TelemetryData.java
@@ -0,0 +1,25 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.microsoft.azure.telemetry;
+
+/**
+ * This class contains constants like telemetry keys and methods to retrieve telemetry info.
+ */
+public class TelemetryData {
+
+ public static final String INSTALLATION_ID = "installationId";
+ public static final String PROJECT_VERSION = "version";
+ public static final String SERVICE_NAME = "serviceName";
+ public static final String HASHED_ACCOUNT_NAME = "hashedAccountName";
+ public static final String HASHED_NAMESPACE = "hashedNamespace";
+ public static final String TENANT_NAME = "tenantName";
+
+ public static String getClassPackageSimpleName(Class> clazz) {
+ if (clazz == null) {
+ return "unknown";
+ }
+
+ return clazz.getPackage().getName().replaceAll("\\w+\\.", "");
+ }
+}
diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/telemetry/TelemetryEventData.java b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/telemetry/TelemetryEventData.java
new file mode 100644
index 000000000000..d77319258a7c
--- /dev/null
+++ b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/telemetry/TelemetryEventData.java
@@ -0,0 +1,130 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.microsoft.azure.telemetry;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.microsoft.azure.utils.PropertyLoader;
+import org.springframework.lang.NonNull;
+import org.springframework.util.Assert;
+
+import java.time.Instant;
+import java.util.Map;
+
+/**
+ * Telemetry data will be sent to Application Insights.
+ */
+public class TelemetryEventData {
+
+ private final String name;
+
+ @JsonProperty("iKey")
+ private final String instrumentationKey;
+
+ private final Tags tags = new Tags("Spring-on-azure", "Java-maven-plugin");
+
+ private final EventData data = new EventData("EventData");
+
+ private final String time;
+
+ public TelemetryEventData(String eventName, @NonNull Map properties) {
+ Assert.hasText(eventName, "Event name should contain text.");
+
+ name = "Microsoft.ApplicationInsights.Event";
+ instrumentationKey = PropertyLoader.getTelemetryInstrumentationKey();
+
+ data.getBaseData().setName(eventName);
+ data.getBaseData().setProperties(properties);
+ time = Instant.now().toString();
+ }
+
+ private static class Tags {
+
+ @JsonProperty("ai.cloud.roleInstance")
+ private final String aiCloudRoleInstance;
+
+ @JsonProperty("ai.internal.sdkVersion")
+ private final String aiInternalSdkVersion;
+
+ Tags(String instance, String sdkVersion) {
+ aiCloudRoleInstance = instance;
+ aiInternalSdkVersion = sdkVersion;
+ }
+
+ public String getAiCloudRoleInstance() {
+ return aiCloudRoleInstance;
+ }
+
+ public String getAiInternalSdkVersion() {
+ return aiInternalSdkVersion;
+ }
+ }
+
+ private static class EventData {
+
+ private final String baseType;
+
+ private final CustomData baseData = new CustomData();
+
+ EventData(String baseType) {
+ this.baseType = baseType;
+ }
+
+ private static class CustomData {
+
+ private final Integer ver = 2;
+
+ private String name;
+
+ private Map properties;
+
+ public Integer getVer() {
+ return ver;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public Map getProperties() {
+ return properties;
+ }
+
+ private void setName(String name) {
+ this.name = name;
+ }
+
+ private void setProperties(Map properties) {
+ this.properties = properties;
+ }
+ }
+
+ public String getBaseType() {
+ return baseType;
+ }
+
+ public CustomData getBaseData() {
+ return baseData;
+ }
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public String getInstrumentationKey() {
+ return instrumentationKey;
+ }
+
+ public Tags getTags() {
+ return tags;
+ }
+
+ public EventData getData() {
+ return data;
+ }
+
+ public String getTime() {
+ return time;
+ }
+}
diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/telemetry/TelemetryProperties.java b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/telemetry/TelemetryProperties.java
new file mode 100644
index 000000000000..858a866b8eaf
--- /dev/null
+++ b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/telemetry/TelemetryProperties.java
@@ -0,0 +1,23 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.microsoft.azure.telemetry;
+
+import org.springframework.boot.context.properties.ConfigurationProperties;
+
+/**
+ * Telemetry configuration's properties.
+ */
+@ConfigurationProperties("telemetry")
+public class TelemetryProperties {
+
+ private String instrumentationKey;
+
+ public String getInstrumentationKey() {
+ return instrumentationKey;
+ }
+
+ public void setInstrumentationKey(String instrumentationKey) {
+ this.instrumentationKey = instrumentationKey;
+ }
+}
diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/telemetry/TelemetrySender.java b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/telemetry/TelemetrySender.java
new file mode 100644
index 000000000000..4037d650645c
--- /dev/null
+++ b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/telemetry/TelemetrySender.java
@@ -0,0 +1,86 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.microsoft.azure.telemetry;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.microsoft.azure.spring.support.GetHashMac;
+import com.microsoft.azure.utils.PropertyLoader;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.http.HttpEntity;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.lang.NonNull;
+import org.springframework.util.Assert;
+import org.springframework.web.client.RestClientException;
+import org.springframework.web.client.RestTemplate;
+
+import java.util.Map;
+
+import static org.springframework.util.MimeTypeUtils.APPLICATION_JSON;
+
+/**
+ * Client used for sending telemetry info.
+ */
+public class TelemetrySender {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(TelemetrySender.class);
+
+ private static final String TELEMETRY_TARGET_URL = "https://dc.services.visualstudio.com/v2/track";
+
+ private static final String PROJECT_INFO = "spring-boot-starter/" + PropertyLoader.getProjectVersion();
+
+ private static final int RETRY_LIMIT = 3; // Align the retry times with sdk
+
+ private static final RestTemplate REST_TEMPLATE = new RestTemplate();
+
+ private static final ObjectMapper MAPPER = new ObjectMapper();
+
+ private static final HttpHeaders HEADERS = new HttpHeaders();
+
+ static {
+ HEADERS.add(HttpHeaders.CONTENT_TYPE, APPLICATION_JSON.toString());
+ }
+
+ private ResponseEntity executeRequest(final TelemetryEventData eventData) {
+ try {
+ final HttpEntity body = new HttpEntity<>(MAPPER.writeValueAsString(eventData), HEADERS);
+
+ return REST_TEMPLATE.exchange(TELEMETRY_TARGET_URL, HttpMethod.POST, body, String.class);
+ } catch (RestClientException | JsonProcessingException e) {
+ LOGGER.warn("Failed to exchange telemetry request, {}.", e.getMessage());
+ }
+
+ return null;
+ }
+
+ private void sendTelemetryData(@NonNull TelemetryEventData eventData) {
+ ResponseEntity response = null;
+
+ for (int i = 0; i < RETRY_LIMIT; i++) {
+ response = executeRequest(eventData);
+
+ if (response != null && response.getStatusCode() == HttpStatus.OK) {
+ return;
+ }
+ }
+
+ if (response != null && response.getStatusCode() != HttpStatus.OK) {
+ LOGGER.warn("Failed to send telemetry data, response status code {}.", response.getStatusCode().toString());
+ }
+ }
+
+ public void send(String name, @NonNull Map properties) {
+ Assert.hasText(name, "Event name should contain text.");
+
+ properties.putIfAbsent(TelemetryData.INSTALLATION_ID, GetHashMac.getHashMac());
+ properties.putIfAbsent(TelemetryData.PROJECT_VERSION, PROJECT_INFO);
+
+ sendTelemetryData(new TelemetryEventData(name, properties));
+ }
+}
+
diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/utils/PropertyLoader.java b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/utils/PropertyLoader.java
new file mode 100644
index 000000000000..f9d951046811
--- /dev/null
+++ b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/utils/PropertyLoader.java
@@ -0,0 +1,40 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.microsoft.azure.utils;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Properties;
+
+/**
+ * Util class to load property files.
+ */
+public class PropertyLoader {
+ private static final String PROJECT_PROPERTY_FILE = "/META-INF/project.properties";
+
+ private static final String TELEMETRY_CONFIG_FILE = "/telemetry.config";
+
+ private static String getProperty(String file, String property) {
+ try (InputStream inputStream = PropertyLoader.class.getResourceAsStream(file)) {
+ if (inputStream != null) {
+ final Properties properties = new Properties();
+ properties.load(inputStream);
+
+ return properties.getProperty(property);
+ }
+ } catch (IOException e) {
+ // Omitted
+ }
+
+ return "unknown";
+ }
+
+ public static String getProjectVersion() {
+ return getProperty(PROJECT_PROPERTY_FILE, "project.version");
+ }
+
+ public static String getTelemetryInstrumentationKey() {
+ return getProperty(TELEMETRY_CONFIG_FILE, "telemetry.instrumentationKey");
+ }
+}
diff --git a/sdk/spring/azure-spring-boot/src/main/resources/META-INF/project.properties b/sdk/spring/azure-spring-boot/src/main/resources/META-INF/project.properties
new file mode 100644
index 000000000000..90d42083480f
--- /dev/null
+++ b/sdk/spring/azure-spring-boot/src/main/resources/META-INF/project.properties
@@ -0,0 +1 @@
+project.version=@project.version@
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
new file mode 100644
index 000000000000..603d44a26bf0
--- /dev/null
+++ b/sdk/spring/azure-spring-boot/src/main/resources/META-INF/spring.factories
@@ -0,0 +1,15 @@
+org.springframework.boot.env.EnvironmentPostProcessor=com.microsoft.azure.spring.cloudfoundry.environment.VcapProcessor,\
+com.microsoft.azure.spring.keyvault.KeyVaultEnvironmentPostProcessor
+org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.microsoft.azure.spring.autoconfigure.cosmosdb.CosmosAutoConfiguration,\
+com.microsoft.azure.spring.autoconfigure.cosmosdb.CosmosDbRepositoriesAutoConfiguration,\
+com.microsoft.azure.spring.autoconfigure.cosmosdb.CosmosDbReactiveRepositoriesAutoConfiguration,\
+com.microsoft.azure.spring.autoconfigure.gremlin.GremlinAutoConfiguration,\
+com.microsoft.azure.spring.autoconfigure.gremlin.GremlinRepositoriesAutoConfiguration,\
+com.azure.spring.autoconfigure.mediaservices.MediaServicesAutoConfiguration,\
+com.microsoft.azure.spring.autoconfigure.servicebus.ServiceBusAutoConfiguration,\
+com.microsoft.azure.spring.autoconfigure.storage.StorageAutoConfiguration,\
+com.microsoft.azure.spring.autoconfigure.aad.AADAuthenticationFilterAutoConfiguration,\
+com.microsoft.azure.spring.autoconfigure.aad.AADOAuth2AutoConfiguration,\
+com.microsoft.azure.spring.autoconfigure.btoc.AADB2CAutoConfiguration,\
+com.microsoft.azure.spring.autoconfigure.metrics.AzureMonitorMetricsExportAutoConfiguration,\
+com.microsoft.azure.spring.autoconfigure.jms.ServiceBusJMSAutoConfiguration
diff --git a/sdk/spring/azure-spring-boot/src/main/resources/aad-oauth2-common.properties b/sdk/spring/azure-spring-boot/src/main/resources/aad-oauth2-common.properties
new file mode 100644
index 000000000000..ff355a06c5de
--- /dev/null
+++ b/sdk/spring/azure-spring-boot/src/main/resources/aad-oauth2-common.properties
@@ -0,0 +1,12 @@
+spring.security.oauth2.client.provider.azure.authorization-uri=https://login.microsoftonline.com/common/oauth2/authorize
+spring.security.oauth2.client.provider.azure.token-uri=https://login.microsoftonline.com/common/oauth2/token
+spring.security.oauth2.client.provider.azure.user-info-uri=https://login.microsoftonline.com/common/openid/userinfo
+spring.security.oauth2.client.provider.azure.jwk-set-uri=https://login.microsoftonline.com/common/discovery/keys
+spring.security.oauth2.client.provider.azure.user-name-attribute=name
+
+spring.security.oauth2.client.registration.azure.client-authentication-method=post
+spring.security.oauth2.client.registration.azure.authorization-grant-type=authorization_code
+spring.security.oauth2.client.registration.azure.redirect-uri={baseUrl}/login/oauth2/code/{registrationId}
+spring.security.oauth2.client.registration.azure.scope=openid, https://graph.microsoft.com/user.read
+spring.security.oauth2.client.registration.azure.client-name=Azure
+spring.security.oauth2.client.registration.azure.provider=azure
diff --git a/sdk/spring/azure-spring-boot/src/main/resources/serviceEndpoints.properties b/sdk/spring/azure-spring-boot/src/main/resources/serviceEndpoints.properties
new file mode 100644
index 000000000000..236d2ff8b308
--- /dev/null
+++ b/sdk/spring/azure-spring-boot/src/main/resources/serviceEndpoints.properties
@@ -0,0 +1,16 @@
+azure.service.endpoints.cn.aadSigninUri=https://login.partner.microsoftonline.cn/
+azure.service.endpoints.cn.aadGraphApiUri=https://graph.chinacloudapi.cn/
+azure.service.endpoints.cn.aadKeyDiscoveryUri=https://login.partner.microsoftonline.cn/common/discovery/keys
+azure.service.endpoints.cn.aadMembershipRestUri=https://graph.chinacloudapi.cn/me/memberOf?api-version=1.6
+azure.service.endpoints.cn-v2-graph.aadSigninUri=https://login.partner.microsoftonline.cn/
+azure.service.endpoints.cn-v2-graph.aadGraphApiUri=https://microsoftgraph.chinacloudapi.cn/
+azure.service.endpoints.cn-v2-graph.aadKeyDiscoveryUri=https://login.partner.microsoftonline.cn/common/discovery/keys
+azure.service.endpoints.cn-v2-graph.aadMembershipRestUri=https://microsoftgraph.chinacloudapi.cn/v1.0/me/memberOf
+azure.service.endpoints.global.aadSigninUri=https://login.microsoftonline.com/
+azure.service.endpoints.global.aadGraphApiUri=https://graph.windows.net/
+azure.service.endpoints.global.aadKeyDiscoveryUri=https://login.microsoftonline.com/common/discovery/keys/
+azure.service.endpoints.global.aadMembershipRestUri=https://graph.windows.net/me/memberOf?api-version=1.6
+azure.service.endpoints.global-v2-graph.aadSigninUri=https://login.microsoftonline.com/
+azure.service.endpoints.global-v2-graph.aadGraphApiUri=https://graph.microsoft.com/
+azure.service.endpoints.global-v2-graph.aadKeyDiscoveryUri=https://login.microsoftonline.com/common/discovery/keys/
+azure.service.endpoints.global-v2-graph.aadMembershipRestUri=https://graph.microsoft.com/v1.0/me/memberOf
diff --git a/sdk/spring/azure-spring-boot/src/main/resources/telemetry.config b/sdk/spring/azure-spring-boot/src/main/resources/telemetry.config
new file mode 100644
index 000000000000..61b615fa0ef1
--- /dev/null
+++ b/sdk/spring/azure-spring-boot/src/main/resources/telemetry.config
@@ -0,0 +1 @@
+telemetry.instrumentationKey=@telemetry.instrumentationKey@
\ No newline at end of file
diff --git a/sdk/spring/azure-spring-boot/src/samples/java/com/azure/spring/aad/AADAppRoleStatelessAuthenticationFilterConfigSample.java b/sdk/spring/azure-spring-boot/src/samples/java/com/azure/spring/aad/AADAppRoleStatelessAuthenticationFilterConfigSample.java
new file mode 100644
index 000000000000..94ee2cd2e2e5
--- /dev/null
+++ b/sdk/spring/azure-spring-boot/src/samples/java/com/azure/spring/aad/AADAppRoleStatelessAuthenticationFilterConfigSample.java
@@ -0,0 +1,37 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.azure.spring.aad;
+
+import com.microsoft.azure.spring.autoconfigure.aad.AADAppRoleStatelessAuthenticationFilter;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
+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.config.http.SessionCreationPolicy;
+import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
+
+/**
+ * WARNING: MODIFYING THIS FILE WILL REQUIRE CORRESPONDING UPDATES TO README.md FILE. LINE NUMBERS
+ * ARE USED TO EXTRACT APPROPRIATE CODE SEGMENTS FROM THIS FILE. ADD NEW CODE AT THE BOTTOM TO AVOID CHANGING
+ * LINE NUMBERS OF EXISTING CODE SAMPLES.
+ *
+ * Code samples for the AADAppRoleStatelessAuthenticationFilter in README.md
+ */
+@EnableWebSecurity
+@EnableGlobalMethodSecurity(prePostEnabled = true)
+public class AADAppRoleStatelessAuthenticationFilterConfigSample extends WebSecurityConfigurerAdapter {
+
+ @Autowired
+ private AADAppRoleStatelessAuthenticationFilter appRoleAuthFilter;
+
+ @Override
+ protected void configure(HttpSecurity http) throws Exception {
+ http.csrf().disable()
+ .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
+ .and()
+ .addFilterBefore(appRoleAuthFilter, UsernamePasswordAuthenticationFilter.class);
+ }
+
+}
diff --git a/sdk/spring/azure-spring-boot/src/samples/java/com/azure/spring/aad/AADAuthenticationFilterConfigSample.java b/sdk/spring/azure-spring-boot/src/samples/java/com/azure/spring/aad/AADAuthenticationFilterConfigSample.java
new file mode 100644
index 000000000000..2e0747dedb8f
--- /dev/null
+++ b/sdk/spring/azure-spring-boot/src/samples/java/com/azure/spring/aad/AADAuthenticationFilterConfigSample.java
@@ -0,0 +1,24 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.azure.spring.aad;
+
+import com.microsoft.azure.spring.autoconfigure.aad.AADAuthenticationFilter;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
+import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
+
+/**
+ * WARNING: MODIFYING THIS FILE WILL REQUIRE CORRESPONDING UPDATES TO README.md FILE. LINE NUMBERS
+ * ARE USED TO EXTRACT APPROPRIATE CODE SEGMENTS FROM THIS FILE. ADD NEW CODE AT THE BOTTOM TO AVOID CHANGING
+ * LINE NUMBERS OF EXISTING CODE SAMPLES.
+ *
+ * Code samples for AADAuthenticationFilter in README.md
+ */
+@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
+public class AADAuthenticationFilterConfigSample extends WebSecurityConfigurerAdapter {
+
+ @Autowired
+ private AADAuthenticationFilter aadAuthFilter;
+
+}
diff --git a/sdk/spring/azure-spring-boot/src/samples/java/com/azure/spring/aad/AADOAuth2LoginConditionalPolicyConfigSample.java b/sdk/spring/azure-spring-boot/src/samples/java/com/azure/spring/aad/AADOAuth2LoginConditionalPolicyConfigSample.java
new file mode 100644
index 000000000000..1ca114f1d768
--- /dev/null
+++ b/sdk/spring/azure-spring-boot/src/samples/java/com/azure/spring/aad/AADOAuth2LoginConditionalPolicyConfigSample.java
@@ -0,0 +1,53 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.azure.spring.aad;
+
+import com.microsoft.azure.spring.autoconfigure.aad.AADAuthenticationFailureHandler;
+import com.microsoft.azure.spring.autoconfigure.aad.AADOAuth2AuthorizationRequestResolver;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.ApplicationContext;
+import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
+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.ClientRegistrationRepository;
+import org.springframework.security.oauth2.client.userinfo.OAuth2UserService;
+import org.springframework.security.oauth2.core.oidc.user.OidcUser;
+
+/**
+ * WARNING: MODIFYING THIS FILE WILL REQUIRE CORRESPONDING UPDATES TO README.md FILE. LINE NUMBERS
+ * ARE USED TO EXTRACT APPROPRIATE CODE SEGMENTS FROM THIS FILE. ADD NEW CODE AT THE BOTTOM TO AVOID CHANGING
+ * LINE NUMBERS OF EXISTING CODE SAMPLES.
+ *
+ * Code samples for the AAD OAuth2.0 login in README.md, including handling for AAD conditional policy
+ */
+@EnableWebSecurity
+@EnableGlobalMethodSecurity(prePostEnabled = true)
+public class AADOAuth2LoginConditionalPolicyConfigSample extends WebSecurityConfigurerAdapter {
+
+ @Autowired
+ private OAuth2UserService oidcUserService;
+
+ @Autowired
+ ApplicationContext applicationContext;
+
+ @Override
+ protected void configure(HttpSecurity http) throws Exception {
+ final ClientRegistrationRepository clientRegistrationRepository =
+ applicationContext.getBean(ClientRegistrationRepository.class);
+
+ http.authorizeRequests()
+ .anyRequest().authenticated()
+ .and()
+ .oauth2Login()
+ .userInfoEndpoint()
+ .oidcUserService(oidcUserService)
+ .and()
+ .authorizationEndpoint()
+ .authorizationRequestResolver(new AADOAuth2AuthorizationRequestResolver(clientRegistrationRepository))
+ .and()
+ .failureHandler(new AADAuthenticationFailureHandler());
+ }
+}
diff --git a/sdk/spring/azure-spring-boot/src/samples/java/com/azure/spring/aad/AADOAuth2LoginConfigSample.java b/sdk/spring/azure-spring-boot/src/samples/java/com/azure/spring/aad/AADOAuth2LoginConfigSample.java
new file mode 100644
index 000000000000..402b0953d87b
--- /dev/null
+++ b/sdk/spring/azure-spring-boot/src/samples/java/com/azure/spring/aad/AADOAuth2LoginConfigSample.java
@@ -0,0 +1,38 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.azure.spring.aad;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
+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.userinfo.OAuth2UserService;
+import org.springframework.security.oauth2.core.oidc.user.OidcUser;
+
+/**
+ * WARNING: MODIFYING THIS FILE WILL REQUIRE CORRESPONDING UPDATES TO README.md FILE. LINE NUMBERS
+ * ARE USED TO EXTRACT APPROPRIATE CODE SEGMENTS FROM THIS FILE. ADD NEW CODE AT THE BOTTOM TO AVOID CHANGING
+ * LINE NUMBERS OF EXISTING CODE SAMPLES.
+ *
+ * Code samples for the AAD OAuth2.0 login in README.md
+ */
+@EnableWebSecurity
+@EnableGlobalMethodSecurity(prePostEnabled = true)
+public class AADOAuth2LoginConfigSample extends WebSecurityConfigurerAdapter {
+
+ @Autowired
+ private OAuth2UserService oidcUserService;
+
+ @Override
+ protected void configure(HttpSecurity http) throws Exception {
+ http.authorizeRequests()
+ .anyRequest().authenticated()
+ .and()
+ .oauth2Login()
+ .userInfoEndpoint()
+ .oidcUserService(oidcUserService);
+ }
+}
diff --git a/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/aad/AADAppRoleAuthenticationFilterTest.java b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/aad/AADAppRoleAuthenticationFilterTest.java
new file mode 100644
index 000000000000..ea893e087099
--- /dev/null
+++ b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/aad/AADAppRoleAuthenticationFilterTest.java
@@ -0,0 +1,156 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.microsoft.azure.spring.autoconfigure.aad;
+
+import com.nimbusds.jose.JOSEException;
+import com.nimbusds.jose.JWSAlgorithm;
+import com.nimbusds.jose.JWSHeader.Builder;
+import com.nimbusds.jose.JWSObject;
+import com.nimbusds.jose.Payload;
+import com.nimbusds.jose.proc.BadJOSEException;
+import com.nimbusds.jwt.JWTClaimsSet;
+import com.nimbusds.jwt.proc.BadJWTException;
+import net.minidev.json.JSONArray;
+import org.assertj.core.api.Assertions;
+import org.hamcrest.CoreMatchers;
+import org.junit.Test;
+import org.springframework.http.HttpHeaders;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.authority.SimpleGrantedAuthority;
+import org.springframework.security.core.context.SecurityContext;
+import org.springframework.security.core.context.SecurityContextHolder;
+
+import javax.servlet.FilterChain;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.text.ParseException;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Set;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+public class AADAppRoleAuthenticationFilterTest {
+
+ public static final String TOKEN = "dummy-token";
+
+ private final UserPrincipalManager userPrincipalManager;
+ private final HttpServletRequest request;
+ private final HttpServletResponse response;
+ private final SimpleGrantedAuthority roleAdmin;
+ private final SimpleGrantedAuthority roleUser;
+ private final AADAppRoleStatelessAuthenticationFilter filter;
+
+ private UserPrincipal createUserPrincipal(Collection roles) {
+ final JSONArray claims = new JSONArray();
+ claims.addAll(roles);
+ final JWTClaimsSet jwtClaimsSet = new JWTClaimsSet.Builder()
+ .subject("john doe")
+ .claim("roles", claims)
+ .build();
+ final JWSObject jwsObject = new JWSObject(new Builder(JWSAlgorithm.RS256).build(),
+ new Payload(jwtClaimsSet.toString()));
+ return new UserPrincipal(jwsObject, jwtClaimsSet);
+ }
+
+ public AADAppRoleAuthenticationFilterTest() {
+ userPrincipalManager = mock(UserPrincipalManager.class);
+ request = mock(HttpServletRequest.class);
+ response = mock(HttpServletResponse.class);
+ roleAdmin = new SimpleGrantedAuthority("ROLE_admin");
+ roleUser = new SimpleGrantedAuthority("ROLE_user");
+ filter = new AADAppRoleStatelessAuthenticationFilter(userPrincipalManager);
+ }
+
+ @Test
+ public void testDoFilterGoodCase()
+ throws ParseException, JOSEException, BadJOSEException, ServletException, IOException {
+ final UserPrincipal dummyPrincipal = createUserPrincipal(Arrays.asList("user", "admin"));
+
+ when(request.getHeader(HttpHeaders.AUTHORIZATION)).thenReturn("Bearer " + TOKEN);
+ when(userPrincipalManager.buildUserPrincipal(TOKEN)).thenReturn(dummyPrincipal);
+
+ // Check in subsequent filter that authentication is available!
+ final FilterChain filterChain = (request, response) -> {
+ final SecurityContext context = SecurityContextHolder.getContext();
+ assertNotNull(context);
+ final Authentication authentication = context.getAuthentication();
+ assertNotNull(authentication);
+ assertTrue("User should be authenticated!", authentication.isAuthenticated());
+ assertEquals(dummyPrincipal, authentication.getPrincipal());
+
+ @SuppressWarnings("unchecked")
+ final Collection authorities = (Collection) authentication
+ .getAuthorities();
+ Assertions.assertThat(authorities).containsExactlyInAnyOrder(roleAdmin, roleUser);
+ };
+
+ filter.doFilterInternal(request, response, filterChain);
+
+ verify(userPrincipalManager).buildUserPrincipal(TOKEN);
+ assertNull("Authentication has not been cleaned up!", SecurityContextHolder.getContext().getAuthentication());
+ }
+
+ @Test(expected = ServletException.class)
+ public void testDoFilterShouldRethrowJWTException()
+ throws ParseException, JOSEException, BadJOSEException, ServletException, IOException {
+
+ when(request.getHeader(HttpHeaders.AUTHORIZATION)).thenReturn("Bearer " + TOKEN);
+ when(userPrincipalManager.buildUserPrincipal(any())).thenThrow(new BadJWTException("bad token"));
+
+ filter.doFilterInternal(request, response, mock(FilterChain.class));
+ }
+
+ @Test
+ public void testDoFilterAddsDefaultRole()
+ throws ParseException, JOSEException, BadJOSEException, ServletException, IOException {
+
+ final UserPrincipal dummyPrincipal = createUserPrincipal(Collections.emptyList());
+
+ when(request.getHeader(HttpHeaders.AUTHORIZATION)).thenReturn("Bearer " + TOKEN);
+ when(userPrincipalManager.buildUserPrincipal(TOKEN)).thenReturn(dummyPrincipal);
+
+ // Check in subsequent filter that authentication is available and default roles are filled.
+ final FilterChain filterChain = (request, response) -> {
+ final SecurityContext context = SecurityContextHolder.getContext();
+ assertNotNull(context);
+ final Authentication authentication = context.getAuthentication();
+ assertNotNull(authentication);
+ assertTrue("User should be authenticated!", authentication.isAuthenticated());
+ final SimpleGrantedAuthority expectedDefaultRole = new SimpleGrantedAuthority("ROLE_USER");
+
+ @SuppressWarnings("unchecked")
+ final Collection authorities = (Collection) authentication
+ .getAuthorities();
+ Assertions.assertThat(authorities).containsExactlyInAnyOrder(expectedDefaultRole);
+ };
+
+ filter.doFilterInternal(request, response, filterChain);
+
+ verify(userPrincipalManager).buildUserPrincipal(TOKEN);
+ assertNull("Authentication has not been cleaned up!", SecurityContextHolder.getContext().getAuthentication());
+ }
+
+ @Test
+ public void testRolesToGrantedAuthoritiesShouldConvertRolesAndFilterNulls() {
+ final JSONArray roles = new JSONArray().appendElement("user").appendElement(null).appendElement("ADMIN");
+ final AADAppRoleStatelessAuthenticationFilter filter = new AADAppRoleStatelessAuthenticationFilter(null);
+ final Set result = filter.rolesToGrantedAuthorities(roles);
+ assertThat("Set should contain the two granted authority 'ROLE_user' and 'ROLE_ADMIN'", result,
+ CoreMatchers.hasItems(new SimpleGrantedAuthority("ROLE_user"),
+ new SimpleGrantedAuthority("ROLE_ADMIN")));
+ }
+
+}
diff --git a/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/aad/AADAuthenticationAutoConfigurationTest.java b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/aad/AADAuthenticationAutoConfigurationTest.java
new file mode 100644
index 000000000000..24c137fc77f4
--- /dev/null
+++ b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/aad/AADAuthenticationAutoConfigurationTest.java
@@ -0,0 +1,93 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.microsoft.azure.spring.autoconfigure.aad;
+
+import org.junit.Test;
+import org.springframework.boot.autoconfigure.AutoConfigurations;
+import org.springframework.boot.test.context.runner.WebApplicationContextRunner;
+import org.springframework.core.env.Environment;
+
+import java.util.Map;
+
+import static com.microsoft.azure.spring.autoconfigure.aad.AADAuthenticationProperties.UserGroupProperties;
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class AADAuthenticationAutoConfigurationTest {
+ private final WebApplicationContextRunner contextRunner = new WebApplicationContextRunner()
+ .withConfiguration(AutoConfigurations.of(AADAuthenticationFilterAutoConfiguration.class))
+ .withPropertyValues("azure.activedirectory.client-id=fake-client-id",
+ "azure.activedirectory.client-secret=fake-client-secret",
+ "azure.activedirectory.active-directory-groups=fake-group",
+ "azure.service.endpoints.global.aadKeyDiscoveryUri=http://fake.aad.discovery.uri",
+ Constants.ALLOW_TELEMETRY_PROPERTY + "=false");
+
+ @Test
+ public void createAADAuthenticationFilter() {
+ this.contextRunner.run(context -> {
+ final AADAuthenticationFilter azureADJwtTokenFilter = context.getBean(AADAuthenticationFilter.class);
+ assertThat(azureADJwtTokenFilter).isNotNull();
+ assertThat(azureADJwtTokenFilter).isExactlyInstanceOf(AADAuthenticationFilter.class);
+ });
+ }
+
+ @Test
+ public void serviceEndpointsCanBeOverridden() {
+ this.contextRunner.withPropertyValues("azure.service.endpoints.global.aadKeyDiscoveryUri=https://test/",
+ "azure.service.endpoints.global.aadSigninUri=https://test/",
+ "azure.service.endpoints.global.aadGraphApiUri=https://test/",
+ "azure.service.endpoints.global.aadKeyDiscoveryUri=https://test/",
+ "azure.service.endpoints.global.aadMembershipRestUri=https://test/")
+ .run(context -> {
+ final Environment environment = context.getEnvironment();
+ assertThat(environment.getProperty("azure.service.endpoints.global.aadSigninUri"))
+ .isEqualTo("https://test/");
+ assertThat(environment.getProperty("azure.service.endpoints.global.aadGraphApiUri"))
+ .isEqualTo("https://test/");
+ assertThat(environment.getProperty("azure.service.endpoints.global.aadKeyDiscoveryUri"))
+ .isEqualTo("https://test/");
+ assertThat(environment.getProperty("azure.service.endpoints.global.aadMembershipRestUri"))
+ .isEqualTo("https://test/");
+ final ServiceEndpointsProperties serviceEndpointsProperties =
+ context.getBean(ServiceEndpointsProperties.class);
+ assertThat(serviceEndpointsProperties).isNotNull();
+ assertThat(serviceEndpointsProperties.getEndpoints()).isNotEmpty();
+
+ final Map endpoints = serviceEndpointsProperties.getEndpoints();
+ assertThat(endpoints).hasSize(4);
+ assertThat(endpoints.get("cn")).isNotNull()
+ .extracting(ServiceEndpoints::getAadGraphApiUri, ServiceEndpoints::getAadKeyDiscoveryUri,
+ ServiceEndpoints::getAadMembershipRestUri, ServiceEndpoints::getAadSigninUri)
+ .containsExactly("https://graph.chinacloudapi.cn/",
+ "https://login.partner.microsoftonline.cn/common/discovery/keys",
+ "https://graph.chinacloudapi.cn/me/memberOf?api-version=1.6",
+ "https://login.partner.microsoftonline.cn/");
+ assertThat(endpoints.get("global")).isNotNull()
+ .extracting(ServiceEndpoints::getAadGraphApiUri, ServiceEndpoints::getAadKeyDiscoveryUri,
+ ServiceEndpoints::getAadMembershipRestUri, ServiceEndpoints::getAadSigninUri)
+ .containsExactly("https://test/", "https://test/", "https://test/", "https://test/");
+ });
+ }
+
+ @Test
+ public void testUserGroupPropertiesAreOverridden() {
+ contextRunner.withPropertyValues("azure.activedirectory.user-group.allowed-groups=another_group,third_group",
+ "azure.activedirectory.user-group.key=key", "azure.activedirectory.user-group.value=value",
+ "azure.activedirectory.user-group.object-id-key=objidk")
+ .run(context -> {
+ assertThat(context.getBean(AADAuthenticationProperties.class)).isNotNull();
+
+ final UserGroupProperties userGroupProperties = context
+ .getBean(AADAuthenticationProperties.class).getUserGroup();
+
+ assertThat(userGroupProperties).hasNoNullFieldsOrProperties()
+ .extracting(UserGroupProperties::getKey, UserGroupProperties::getValue,
+ UserGroupProperties::getObjectIDKey).containsExactly("key", "value", "objidk");
+
+ assertThat(userGroupProperties.getAllowedGroups()).isNotEmpty()
+ .hasSize(2).containsExactly("another_group", "third_group");
+ }
+ );
+
+ }
+}
diff --git a/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/aad/AADAuthenticationFilterPropertiesTest.java b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/aad/AADAuthenticationFilterPropertiesTest.java
new file mode 100644
index 000000000000..85604cca1e36
--- /dev/null
+++ b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/aad/AADAuthenticationFilterPropertiesTest.java
@@ -0,0 +1,123 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.microsoft.azure.spring.autoconfigure.aad;
+
+import org.junit.After;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.springframework.boot.context.properties.ConfigurationPropertiesBindException;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.boot.context.properties.bind.validation.BindValidationException;
+import org.springframework.context.annotation.AnnotationConfigApplicationContext;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.validation.ObjectError;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import static com.microsoft.azure.spring.autoconfigure.aad.Constants.ALLOW_TELEMETRY_PROPERTY;
+import static com.microsoft.azure.spring.autoconfigure.aad.Constants.CLIENT_ID_PROPERTY;
+import static com.microsoft.azure.spring.autoconfigure.aad.Constants.CLIENT_SECRET_PROPERTY;
+import static com.microsoft.azure.spring.autoconfigure.aad.Constants.TARGETED_GROUPS_PROPERTY;
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class AADAuthenticationFilterPropertiesTest {
+ @After
+ public void clearAllProperties() {
+ System.clearProperty(Constants.SERVICE_ENVIRONMENT_PROPERTY);
+ System.clearProperty(CLIENT_ID_PROPERTY);
+ System.clearProperty(CLIENT_SECRET_PROPERTY);
+ System.clearProperty(TARGETED_GROUPS_PROPERTY);
+ }
+
+ @Test
+ public void canSetProperties() {
+ configureAllRequiredProperties();
+
+ try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext()) {
+ context.register(Config.class);
+ context.refresh();
+
+ final AADAuthenticationProperties properties = context.getBean(AADAuthenticationProperties.class);
+
+ assertThat(properties.getClientId()).isEqualTo(Constants.CLIENT_ID);
+ assertThat(properties.getClientSecret()).isEqualTo(Constants.CLIENT_SECRET);
+ assertThat(properties.getActiveDirectoryGroups()
+ .toString()).isEqualTo(Constants.TARGETED_GROUPS.toString());
+ }
+ }
+
+ @Test
+ public void defaultEnvironmentIsGlobal() {
+ configureAllRequiredProperties();
+ assertThat(System.getProperty(Constants.SERVICE_ENVIRONMENT_PROPERTY)).isNullOrEmpty();
+
+ try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext()) {
+ context.register(Config.class);
+ context.refresh();
+
+ final AADAuthenticationProperties properties = context.getBean(AADAuthenticationProperties.class);
+
+ assertThat(properties.getEnvironment()).isEqualTo(Constants.DEFAULT_ENVIRONMENT);
+ }
+ }
+
+ private void configureAllRequiredProperties() {
+ System.setProperty(CLIENT_ID_PROPERTY, Constants.CLIENT_ID);
+ System.setProperty(CLIENT_SECRET_PROPERTY, Constants.CLIENT_SECRET);
+ System.setProperty(TARGETED_GROUPS_PROPERTY,
+ Constants.TARGETED_GROUPS.toString().replace("[", "").replace("]", ""));
+ System.setProperty(ALLOW_TELEMETRY_PROPERTY, "false");
+ }
+
+ @Test
+ @Ignore // TODO (wepa) clientId and clientSecret can also be configured in oauth2 config, test to be refactored
+ public void emptySettingsNotAllowed() {
+ System.setProperty(CLIENT_ID_PROPERTY, "");
+ System.setProperty(CLIENT_SECRET_PROPERTY, "");
+
+ try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext()) {
+ Exception exception = null;
+
+ context.register(Config.class);
+
+ try {
+ context.refresh();
+ } catch (Exception e) {
+ exception = e;
+ }
+
+ assertThat(exception).isNotNull();
+ assertThat(exception).isExactlyInstanceOf(ConfigurationPropertiesBindException.class);
+
+ final BindValidationException bindException = (BindValidationException) exception.getCause().getCause();
+ final List errors = bindException.getValidationErrors().getAllErrors();
+
+ final List errorStrings = errors.stream().map(e -> e.toString()).collect(Collectors.toList());
+
+ final List errorStringsExpected = Arrays.asList(
+ "Field error in object 'azure.activedirectory' on field 'activeDirectoryGroups': "
+ + "rejected value [null];",
+ "Field error in object 'azure.activedirectory' on field 'clientId': rejected value [];",
+ "Field error in object 'azure.activedirectory' on field 'clientSecret': rejected value [];"
+ );
+
+ Collections.sort(errorStrings);
+
+ assertThat(errors.size()).isEqualTo(errorStringsExpected.size());
+
+ for (int i = 0; i < errorStrings.size(); i++) {
+ assertThat(errorStrings.get(i)).contains(errorStringsExpected.get(i));
+ }
+ }
+ }
+
+ @Configuration
+ @EnableConfigurationProperties(AADAuthenticationProperties.class)
+ static class Config {
+ }
+}
+
diff --git a/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/aad/AADAuthenticationFilterTest.java b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/aad/AADAuthenticationFilterTest.java
new file mode 100644
index 000000000000..70954873eb73
--- /dev/null
+++ b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/aad/AADAuthenticationFilterTest.java
@@ -0,0 +1,82 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.microsoft.azure.spring.autoconfigure.aad;
+
+import org.junit.Assume;
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.springframework.boot.autoconfigure.AutoConfigurations;
+import org.springframework.boot.test.context.runner.ApplicationContextRunner;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.authority.SimpleGrantedAuthority;
+import org.springframework.security.core.context.SecurityContextHolder;
+
+import javax.servlet.FilterChain;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.util.Map;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class AADAuthenticationFilterTest {
+ private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
+ .withConfiguration(AutoConfigurations.of(AADAuthenticationFilterAutoConfiguration.class));
+
+ @Before
+ @Ignore
+ public void beforeEveryMethod() {
+ Assume.assumeTrue(!Constants.CLIENT_ID.contains("real_client_id"));
+ Assume.assumeTrue(!Constants.CLIENT_SECRET.contains("real_client_secret"));
+ Assume.assumeTrue(!Constants.BEARER_TOKEN.contains("real_jtw_bearer_token"));
+ }
+
+ //TODO (Zhou Liu): current test case is out of date, a new test case need to cover here, do it later.
+ @Test
+ @Ignore
+ public void doFilterInternal() {
+ this.contextRunner.withPropertyValues(Constants.CLIENT_ID_PROPERTY, Constants.CLIENT_ID)
+ .withPropertyValues(Constants.CLIENT_SECRET_PROPERTY, Constants.CLIENT_SECRET)
+ .withPropertyValues(Constants.TARGETED_GROUPS_PROPERTY,
+ Constants.TARGETED_GROUPS.toString()
+ .replace("[", "").replace("]", ""));
+
+ this.contextRunner.run(context -> {
+ final HttpServletRequest request = mock(HttpServletRequest.class);
+ when(request.getHeader(Constants.TOKEN_HEADER)).thenReturn(Constants.BEARER_TOKEN);
+
+ final HttpServletResponse response = mock(HttpServletResponse.class);
+ final FilterChain filterChain = mock(FilterChain.class);
+
+
+ final AADAuthenticationFilter azureADJwtTokenFilter = context.getBean(AADAuthenticationFilter.class);
+ assertThat(azureADJwtTokenFilter).isNotNull();
+ assertThat(azureADJwtTokenFilter).isExactlyInstanceOf(AADAuthenticationFilter.class);
+
+ azureADJwtTokenFilter.doFilterInternal(request, response, filterChain);
+
+ final Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
+ assertThat(authentication.getPrincipal()).isNotNull();
+ assertThat(authentication.getPrincipal()).isExactlyInstanceOf(UserPrincipal.class);
+ assertThat(authentication.getAuthorities()).isNotNull();
+ assertThat(authentication.getAuthorities().size()).isEqualTo(2);
+ assertThat(authentication.getAuthorities().contains(new SimpleGrantedAuthority("ROLE_group1"))
+ && authentication.getAuthorities().contains(new SimpleGrantedAuthority("ROLE_group2"))
+ ).isTrue();
+
+ final UserPrincipal principal = (UserPrincipal) authentication.getPrincipal();
+ assertThat(principal.getIssuer()).isNotNull().isNotEmpty();
+ assertThat(principal.getKid()).isNotNull().isNotEmpty();
+ assertThat(principal.getSubject()).isNotNull().isNotEmpty();
+
+ assertThat(principal.getClaims()).isNotNull().isNotEmpty();
+ final Map claims = principal.getClaims();
+ assertThat(claims.get("iss")).isEqualTo(principal.getIssuer());
+ assertThat(claims.get("sub")).isEqualTo(principal.getSubject());
+ });
+ }
+
+}
diff --git a/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/aad/AADOAuth2ConfigTest.java b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/aad/AADOAuth2ConfigTest.java
new file mode 100644
index 000000000000..8181b0580ad5
--- /dev/null
+++ b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/aad/AADOAuth2ConfigTest.java
@@ -0,0 +1,129 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.microsoft.azure.spring.autoconfigure.aad;
+
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.springframework.beans.factory.NoSuchBeanDefinitionException;
+import org.springframework.core.env.Environment;
+import org.springframework.core.io.ClassPathResource;
+import org.springframework.core.io.Resource;
+import org.springframework.core.io.support.ResourcePropertySource;
+import org.springframework.mock.env.MockPropertySource;
+import org.springframework.security.oauth2.client.userinfo.OAuth2UserService;
+import org.springframework.test.context.support.TestPropertySourceUtils;
+import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
+
+import java.util.Map;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class AADOAuth2ConfigTest {
+ private static final String AAD_OAUTH2_MINIMUM_PROPS = "aad-backend-oauth2-minimum.properties";
+ private Resource testResource;
+ private ResourcePropertySource testPropResource;
+
+ @Rule
+ public ExpectedException exception = ExpectedException.none();
+
+ private AnnotationConfigWebApplicationContext testContext;
+
+ @Before
+ public void setup() throws Exception {
+ testResource = new ClassPathResource(AAD_OAUTH2_MINIMUM_PROPS);
+ testPropResource = new ResourcePropertySource("test", testResource);
+ }
+
+ @After
+ public void clear() {
+ if (testContext != null) {
+ testContext.close();
+ }
+ }
+
+ @Test
+ public void noOAuth2UserServiceBeanCreatedIfPropsNotConfigured() {
+ final AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
+ context.register(AADOAuth2AutoConfiguration.class);
+ context.refresh();
+
+ exception.expect(NoSuchBeanDefinitionException.class);
+ context.getBean(OAuth2UserService.class);
+ }
+
+ @Test
+ public void testOAuth2UserServiceBeanCreatedIfPropsConfigured() {
+ testContext = initTestContext();
+ Assert.assertNotNull(testContext.getBean(OAuth2UserService.class));
+ }
+
+ @Test
+ public void noOAuth2UserServiceBeanCreatedIfTenantIdNotConfigured() {
+ testPropResource.getSource().remove(Constants.TENANT_ID_PROPERTY);
+ testContext = initTestContext();
+
+ exception.expect(NoSuchBeanDefinitionException.class);
+ testContext.getBean(OAuth2UserService.class);
+ }
+
+ @Test
+ public void testEndpointsPropertiesLoadAndOverridable() {
+ testContext = initTestContext("azure.service.endpoints.global.aadKeyDiscoveryUri=https://test/",
+ "azure.service.endpoints.global.aadSigninUri=https://test/",
+ "azure.service.endpoints.global.aadGraphApiUri=https://test/",
+ "azure.service.endpoints.global.aadKeyDiscoveryUri=https://test/",
+ "azure.service.endpoints.global.aadMembershipRestUri=https://test/",
+ Constants.ALLOW_TELEMETRY_PROPERTY + "=false");
+
+
+ final Environment environment = testContext.getEnvironment();
+ assertThat(environment.getProperty("azure.service.endpoints.global.aadSigninUri"))
+ .isEqualTo("https://test/");
+ assertThat(environment.getProperty("azure.service.endpoints.global.aadGraphApiUri"))
+ .isEqualTo("https://test/");
+ assertThat(environment.getProperty("azure.service.endpoints.global.aadKeyDiscoveryUri"))
+ .isEqualTo("https://test/");
+ assertThat(environment.getProperty("azure.service.endpoints.global.aadMembershipRestUri"))
+ .isEqualTo("https://test/");
+ final ServiceEndpointsProperties serviceEndpointsProperties =
+ testContext.getBean(ServiceEndpointsProperties.class);
+ assertThat(serviceEndpointsProperties).isNotNull();
+ assertThat(serviceEndpointsProperties.getEndpoints()).isNotEmpty();
+
+ final Map endpoints = serviceEndpointsProperties.getEndpoints();
+ assertThat(endpoints).hasSize(4);
+ assertThat(endpoints.get("cn")).isNotNull()
+ .extracting(ServiceEndpoints::getAadGraphApiUri, ServiceEndpoints::getAadKeyDiscoveryUri,
+ ServiceEndpoints::getAadMembershipRestUri, ServiceEndpoints::getAadSigninUri)
+ .containsExactly("https://graph.chinacloudapi.cn/",
+ "https://login.partner.microsoftonline.cn/common/discovery/keys",
+ "https://graph.chinacloudapi.cn/me/memberOf?api-version=1.6",
+ "https://login.partner.microsoftonline.cn/");
+ assertThat(endpoints.get("global")).isNotNull()
+ .extracting(ServiceEndpoints::getAadGraphApiUri, ServiceEndpoints::getAadKeyDiscoveryUri,
+ ServiceEndpoints::getAadMembershipRestUri, ServiceEndpoints::getAadSigninUri)
+ .containsExactly("https://test/", "https://test/", "https://test/", "https://test/");
+
+ }
+
+ private AnnotationConfigWebApplicationContext initTestContext(String... environment) {
+ final AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
+
+ context.getEnvironment().getPropertySources().addLast(testPropResource);
+ context.getEnvironment().getPropertySources().addLast(new MockPropertySource()
+ .withProperty(Constants.ALLOW_TELEMETRY_PROPERTY, "false"));
+ if (environment.length > 0) {
+ TestPropertySourceUtils.addInlinedPropertiesToEnvironment(context, environment);
+ }
+
+ context.register(AADOAuth2AutoConfiguration.class);
+ context.refresh();
+
+ return context;
+ }
+}
diff --git a/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/aad/AADUserGroupsPropertyValidatorTest.java b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/aad/AADUserGroupsPropertyValidatorTest.java
new file mode 100644
index 000000000000..fc4ecee8ecd9
--- /dev/null
+++ b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/aad/AADUserGroupsPropertyValidatorTest.java
@@ -0,0 +1,48 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.microsoft.azure.spring.autoconfigure.aad;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.Collections;
+
+import static org.assertj.core.api.Assertions.assertThatCode;
+
+public class AADUserGroupsPropertyValidatorTest {
+
+ private AADAuthenticationProperties aadAuthenticationProperties;
+
+ @Before
+ public void setUp() {
+ aadAuthenticationProperties = new AADAuthenticationProperties();
+ }
+
+ @Test
+ public void isValidNoGroupsDefined() {
+ assertThatCode(() -> aadAuthenticationProperties.validateUserGroupProperties())
+ .isInstanceOf(IllegalStateException.class).hasMessage(
+ "One of the User Group Properties must be populated. "
+ + "Please populate azure.activedirectory.user-group.allowed-groups");
+ }
+
+ @Test
+ public void isValidDeprecatedPropertySet() {
+ aadAuthenticationProperties.setActiveDirectoryGroups(Collections.singletonList("user-group"));
+ assertThatCode(() -> aadAuthenticationProperties.validateUserGroupProperties()).doesNotThrowAnyException();
+ }
+
+ @Test
+ public void isValidUserGroupPropertySet() {
+ aadAuthenticationProperties.getUserGroup().setAllowedGroups(Collections.singletonList("user-group"));
+ assertThatCode(() -> aadAuthenticationProperties.validateUserGroupProperties()).doesNotThrowAnyException();
+ }
+
+ @Test
+ public void isValidBothUserGroupPropertiesSet() {
+ aadAuthenticationProperties.setActiveDirectoryGroups(Collections.singletonList("user-group"));
+ aadAuthenticationProperties.getUserGroup().setAllowedGroups(Collections.singletonList("user-group"));
+ assertThatCode(() -> aadAuthenticationProperties.validateUserGroupProperties()).doesNotThrowAnyException();
+ }
+}
diff --git a/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/aad/AzureADGraphClientTest.java b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/aad/AzureADGraphClientTest.java
new file mode 100644
index 000000000000..34b8ecd7b9e1
--- /dev/null
+++ b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/aad/AzureADGraphClientTest.java
@@ -0,0 +1,61 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.microsoft.azure.spring.autoconfigure.aad;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+import org.springframework.security.core.GrantedAuthority;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+
+import static org.assertj.core.api.Java6Assertions.assertThat;
+
+@RunWith(MockitoJUnitRunner.class)
+public class AzureADGraphClientTest {
+
+ private AzureADGraphClient adGraphClient;
+
+ private AADAuthenticationProperties aadAuthProps;
+
+ @Mock
+ private ServiceEndpointsProperties endpointsProps;
+
+ @Before
+ public void setup() {
+ final List activeDirectoryGroups = new ArrayList<>();
+ activeDirectoryGroups.add("Test_Group");
+ aadAuthProps = new AADAuthenticationProperties();
+ aadAuthProps.setActiveDirectoryGroups(activeDirectoryGroups);
+ adGraphClient = new AzureADGraphClient("client", "pass", aadAuthProps, endpointsProps);
+ }
+
+ @Test
+ public void testConvertGroupToGrantedAuthorities() {
+
+ final List userGroups = Collections.singletonList(
+ new UserGroup("testId", "Test_Group"));
+
+ final Set authorities = adGraphClient.convertGroupsToGrantedAuthorities(userGroups);
+ assertThat(authorities).hasSize(1).extracting(GrantedAuthority::getAuthority)
+ .containsExactly("ROLE_Test_Group");
+ }
+
+ @Test
+ public void testConvertGroupToGrantedAuthoritiesUsingAllowedGroups() {
+ final List userGroups = Arrays
+ .asList(new UserGroup("testId", "Test_Group"),
+ new UserGroup("testId", "Another_Group"));
+ aadAuthProps.getUserGroup().getAllowedGroups().add("Another_Group");
+ final Set authorities = adGraphClient.convertGroupsToGrantedAuthorities(userGroups);
+ assertThat(authorities).hasSize(2).extracting(GrantedAuthority::getAuthority)
+ .containsExactly("ROLE_Test_Group", "ROLE_Another_Group");
+ }
+}
diff --git a/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/aad/Constants.java b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/aad/Constants.java
new file mode 100644
index 000000000000..84dc5e4c0452
--- /dev/null
+++ b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/aad/Constants.java
@@ -0,0 +1,38 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.microsoft.azure.spring.autoconfigure.aad;
+
+import java.util.Arrays;
+import java.util.List;
+
+public class Constants {
+ public static final String SERVICE_ENVIRONMENT_PROPERTY = "azure.activedirectory.environment";
+ public static final String CLIENT_ID_PROPERTY = "azure.activedirectory.client-id";
+ public static final String CLIENT_SECRET_PROPERTY = "azure.activedirectory.client-secret";
+ public static final String TARGETED_GROUPS_PROPERTY = "azure.activedirectory.active-directory-groups";
+ public static final String TENANT_ID_PROPERTY = "azure.activedirectory.tenant-id";
+ public static final String ALLOW_TELEMETRY_PROPERTY = "azure.activedirectory.allow-telemetry";
+
+
+ public static final String DEFAULT_ENVIRONMENT = "global";
+ public static final String CLIENT_ID = "real_client_id";
+ public static final String CLIENT_SECRET = "real_client_secret";
+ public static final List TARGETED_GROUPS = Arrays.asList("group1", "group2", "group3");
+
+ public static final String TOKEN_HEADER = "Authorization";
+ public static final String BEARER_TOKEN = "Bearer real_jtw_bearer_token";
+
+ /** Token from https://docs.microsoft.com/azure/active-directory/develop/v2-id-and-access-tokens */
+ public static final String JWT_TOKEN = "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6Ik1uQ19WWmNBVGZNNXBPWWlKSE1"
+ + "iYTlnb0VLWSJ9.eyJhdWQiOiI2NzMxZGU3Ni0xNGE2LTQ5YWUtOTdiYy02ZWJhNjkxNDM5MWUiLCJpc3MiOiJodHRwczovL2xvZ2lu"
+ + "Lm1pY3Jvc29mdG9ubGluZS5jb20vYjk0MTk4MTgtMDlhZi00OWMyLWIwYzMtNjUzYWRjMWYzNzZlL3YyLjAiLCJpYXQiOjE0NTIyOD"
+ + "UzMzEsIm5iZiI6MTQ1MjI4NTMzMSwiZXhwIjoxNDUyMjg5MjMxLCJuYW1lIjoiQmFiZSBSdXRoIiwibm9uY2UiOiIxMjM0NSIsIm9p"
+ + "ZCI6ImExZGJkZGU4LWU0ZjktNDU3MS1hZDkzLTMwNTllMzc1MGQyMyIsInByZWZlcnJlZF91c2VybmFtZSI6InRoZWdyZWF0YmFtYm"
+ + "lub0BueXkub25taWNyb3NvZnQuY29tIiwic3ViIjoiTUY0Zi1nZ1dNRWppMTJLeW5KVU5RWnBoYVVUdkxjUXVnNWpkRjJubDAxUSIs"
+ + "InRpZCI6ImI5NDE5ODE4LTA5YWYtNDljMi1iMGMzLTY1M2FkYzFmMzc2ZSIsInZlciI6IjIuMCJ9.p_rYdrtJ1oCmgDBggNHB9O38K"
+ + "TnLCMGbMDODdirdmZbmJcTHiZDdtTc-hguu3krhbtOsoYM2HJeZM3Wsbp_YcfSKDY--X_NobMNsxbT7bqZHxDnA2jTMyrmt5v2EKUn"
+ + "EeVtSiJXyO3JWUq9R0dO-m4o9_8jGP6zHtR62zLaotTBYHmgeKpZgTFB9WtUq8DVdyMn_HSvQEfz-LWqckbcTwM_9RNKoGRVk38KCh"
+ + "VJo4z5LkksYRarDo8QgQ7xEKmYmPvRr_I7gvM2bmlZQds2OeqWLB1NSNbFZqyFOCgYn3bAQ-nEQSKwBaA36jYGPOVG2r2Qv1uKcpSO"
+ + "xzxaQybzYpQ";
+}
diff --git a/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/aad/MicrosoftGraphConstants.java b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/aad/MicrosoftGraphConstants.java
new file mode 100644
index 000000000000..58d0ba2cc390
--- /dev/null
+++ b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/aad/MicrosoftGraphConstants.java
@@ -0,0 +1,37 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.microsoft.azure.spring.autoconfigure.aad;
+
+import java.util.Arrays;
+import java.util.List;
+
+public class MicrosoftGraphConstants {
+
+ public static final String SERVICE_ENVIRONMENT_PROPERTY = "azure.activedirectory.environment";
+ public static final String CLIENT_ID_PROPERTY = "azure.activedirectory.client-id";
+ public static final String CLIENT_SECRET_PROPERTY = "azure.activedirectory.client-secret";
+ public static final String TARGETED_GROUPS_PROPERTY = "azure.activedirectory.active-directory-groups";
+ public static final String TENANT_ID_PROPERTY = "azure.activedirectory.tenant-id";
+
+ public static final String DEFAULT_ENVIRONMENT = "global";
+ public static final String CLIENT_ID = "real_client_id";
+ public static final String CLIENT_SECRET = "real_client_secret";
+ public static final List TARGETED_GROUPS = Arrays.asList("group1", "group2", "group3");
+
+ public static final String TOKEN_HEADER = "Authorization";
+ public static final String BEARER_TOKEN = "Bearer real_jtw_bearer_token";
+
+ /** Token from https://docs.microsoft.com/azure/active-directory/develop/v2-id-and-access-tokens */
+ public static final String JWT_TOKEN = "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6Ik1uQ19WWmNBVGZNNXBPWWlKSE1"
+ + "iYTlnb0VLWSJ9.eyJhdWQiOiI2NzMxZGU3Ni0xNGE2LTQ5YWUtOTdiYy02ZWJhNjkxNDM5MWUiLCJpc3MiOiJodHRwczovL2xvZ2lu"
+ + "Lm1pY3Jvc29mdG9ubGluZS5jb20vYjk0MTk4MTgtMDlhZi00OWMyLWIwYzMtNjUzYWRjMWYzNzZlL3YyLjAiLCJpYXQiOjE0NTIyOD"
+ + "UzMzEsIm5iZiI6MTQ1MjI4NTMzMSwiZXhwIjoxNDUyMjg5MjMxLCJuYW1lIjoiQmFiZSBSdXRoIiwibm9uY2UiOiIxMjM0NSIsIm9p"
+ + "ZCI6ImExZGJkZGU4LWU0ZjktNDU3MS1hZDkzLTMwNTllMzc1MGQyMyIsInByZWZlcnJlZF91c2VybmFtZSI6InRoZWdyZWF0YmFtYm"
+ + "lub0BueXkub25taWNyb3NvZnQuY29tIiwic3ViIjoiTUY0Zi1nZ1dNRWppMTJLeW5KVU5RWnBoYVVUdkxjUXVnNWpkRjJubDAxUSIs"
+ + "InRpZCI6ImI5NDE5ODE4LTA5YWYtNDljMi1iMGMzLTY1M2FkYzFmMzc2ZSIsInZlciI6IjIuMCJ9.p_rYdrtJ1oCmgDBggNHB9O38K"
+ + "TnLCMGbMDODdirdmZbmJcTHiZDdtTc-hguu3krhbtOsoYM2HJeZM3Wsbp_YcfSKDY--X_NobMNsxbT7bqZHxDnA2jTMyrmt5v2EKUn"
+ + "EeVtSiJXyO3JWUq9R0dO-m4o9_8jGP6zHtR62zLaotTBYHmgeKpZgTFB9WtUq8DVdyMn_HSvQEfz-LWqckbcTwM_9RNKoGRVk38KCh"
+ + "VJo4z5LkksYRarDo8QgQ7xEKmYmPvRr_I7gvM2bmlZQds2OeqWLB1NSNbFZqyFOCgYn3bAQ-nEQSKwBaA36jYGPOVG2r2Qv1uKcpSO"
+ + "xzxaQybzYpQ";
+}
diff --git a/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/aad/ResourceRetrieverTest.java b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/aad/ResourceRetrieverTest.java
new file mode 100644
index 000000000000..a6959a20d933
--- /dev/null
+++ b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/aad/ResourceRetrieverTest.java
@@ -0,0 +1,54 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.microsoft.azure.spring.autoconfigure.aad;
+
+import com.nimbusds.jose.jwk.source.RemoteJWKSet;
+import com.nimbusds.jose.util.DefaultResourceRetriever;
+import com.nimbusds.jose.util.ResourceRetriever;
+import org.junit.Test;
+import org.springframework.boot.autoconfigure.AutoConfigurations;
+import org.springframework.boot.test.context.runner.WebApplicationContextRunner;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class ResourceRetrieverTest {
+ private final WebApplicationContextRunner contextRunner = new WebApplicationContextRunner()
+ .withConfiguration(AutoConfigurations.of(AADAuthenticationFilterAutoConfiguration.class))
+ .withPropertyValues("azure.activedirectory.client-id=fake-client-id",
+ "azure.activedirectory.client-secret=fake-client-secret",
+ "azure.activedirectory.active-directory-groups=fake-group",
+ "azure.service.endpoints.global.aadKeyDiscoveryUri=http://fake.aad.discovery.uri");
+
+ @Test
+ public void resourceRetrieverDefaultConfig() {
+ this.contextRunner.run(context -> {
+ assertThat(context).hasSingleBean(ResourceRetriever.class);
+ final ResourceRetriever retriever = context.getBean(ResourceRetriever.class);
+ assertThat(retriever).isInstanceOf(DefaultResourceRetriever.class);
+
+ final DefaultResourceRetriever defaultRetriever = (DefaultResourceRetriever) retriever;
+ assertThat(defaultRetriever.getConnectTimeout()).isEqualTo(RemoteJWKSet.DEFAULT_HTTP_CONNECT_TIMEOUT);
+ assertThat(defaultRetriever.getReadTimeout()).isEqualTo(RemoteJWKSet.DEFAULT_HTTP_READ_TIMEOUT);
+ assertThat(defaultRetriever.getSizeLimit()).isEqualTo(RemoteJWKSet.DEFAULT_HTTP_SIZE_LIMIT);
+ });
+ }
+
+ @Test
+ public void resourceRetriverIsConfigurable() {
+ this.contextRunner.withPropertyValues("azure.activedirectory.jwt-connect-timeout=1234",
+ "azure.activedirectory.jwt-read-timeout=1234",
+ "azure.activedirectory.jwt-size-limit=123400",
+ "azure.service.endpoints.global.aadKeyDiscoveryUri=http://fake.aad.discovery.uri")
+ .run(context -> {
+ assertThat(context).hasSingleBean(ResourceRetriever.class);
+ final ResourceRetriever retriever = context.getBean(ResourceRetriever.class);
+ assertThat(retriever).isInstanceOf(DefaultResourceRetriever.class);
+
+ final DefaultResourceRetriever defaultRetriever = (DefaultResourceRetriever) retriever;
+ assertThat(defaultRetriever.getConnectTimeout()).isEqualTo(1234);
+ assertThat(defaultRetriever.getReadTimeout()).isEqualTo(1234);
+ assertThat(defaultRetriever.getSizeLimit()).isEqualTo(123400);
+ });
+ }
+}
diff --git a/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/aad/UserGroupTest.java b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/aad/UserGroupTest.java
new file mode 100644
index 000000000000..08906553218c
--- /dev/null
+++ b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/aad/UserGroupTest.java
@@ -0,0 +1,33 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.microsoft.azure.spring.autoconfigure.aad;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+public class UserGroupTest {
+ private static final UserGroup GROUP_1 = new UserGroup("12345", "test");
+
+ @Test
+ public void getDisplayName() {
+ Assert.assertEquals("test", GROUP_1.getDisplayName());
+ }
+
+ @Test
+ public void getObjectID() {
+ Assert.assertEquals("12345", GROUP_1.getObjectID());
+ }
+
+ @Test
+ public void equals() {
+ final UserGroup group2 = new UserGroup("12345", "test");
+ Assert.assertEquals(GROUP_1, group2);
+ }
+
+ @Test
+ public void hashCodeTest() {
+ final UserGroup group2 = new UserGroup("12345", "test");
+ Assert.assertEquals(GROUP_1.hashCode(), group2.hashCode());
+ }
+}
diff --git a/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/aad/UserPrincipalAzureADGraphTest.java b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/aad/UserPrincipalAzureADGraphTest.java
new file mode 100644
index 000000000000..5826ad4fb4d0
--- /dev/null
+++ b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/aad/UserPrincipalAzureADGraphTest.java
@@ -0,0 +1,157 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.microsoft.azure.spring.autoconfigure.aad;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.github.tomakehurst.wiremock.junit.WireMockRule;
+import com.nimbusds.jose.JWSObject;
+import com.nimbusds.jwt.JWTClaimsSet;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.springframework.http.HttpHeaders;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.util.StringUtils;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.nio.file.Files;
+import java.text.ParseException;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
+import static com.github.tomakehurst.wiremock.client.WireMock.equalTo;
+import static com.github.tomakehurst.wiremock.client.WireMock.get;
+import static com.github.tomakehurst.wiremock.client.WireMock.getRequestedFor;
+import static com.github.tomakehurst.wiremock.client.WireMock.stubFor;
+import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo;
+import static com.github.tomakehurst.wiremock.client.WireMock.urlMatching;
+import static com.github.tomakehurst.wiremock.client.WireMock.verify;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.springframework.http.HttpHeaders.ACCEPT;
+import static org.springframework.http.HttpHeaders.CONTENT_TYPE;
+import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
+
+
+public class UserPrincipalAzureADGraphTest {
+ @Rule
+ public WireMockRule wireMockRule = new WireMockRule(9519);
+
+ private AzureADGraphClient graphClientMock;
+ private String clientId;
+ private String clientSecret;
+ private AADAuthenticationProperties aadAuthProps;
+ private ServiceEndpointsProperties endpointsProps;
+ private String accessToken;
+ private static String userGroupsJson;
+
+ static {
+ try {
+ final ObjectMapper objectMapper = new ObjectMapper();
+ final Map json = objectMapper.readValue(UserPrincipalAzureADGraphTest.class.getClassLoader()
+ .getResourceAsStream("aad/azure-ad-graph-user-groups.json"),
+ new TypeReference>() { });
+ userGroupsJson = objectMapper.writeValueAsString(json);
+ } catch (IOException e) {
+ e.printStackTrace();
+ userGroupsJson = null;
+ }
+ Assert.assertNotNull(userGroupsJson);
+ }
+
+ @Before
+ public void setup() {
+ accessToken = Constants.BEARER_TOKEN;
+ aadAuthProps = new AADAuthenticationProperties();
+ endpointsProps = new ServiceEndpointsProperties();
+ final ServiceEndpoints serviceEndpoints = new ServiceEndpoints();
+ serviceEndpoints.setAadMembershipRestUri("http://localhost:9519/memberOf");
+ endpointsProps.getEndpoints().put("global", serviceEndpoints);
+ clientId = "client";
+ clientSecret = "pass";
+ }
+
+ @Test
+ public void getAuthoritiesByUserGroups() throws Exception {
+ aadAuthProps.getUserGroup().setAllowedGroups(Collections.singletonList("group1"));
+ this.graphClientMock = new AzureADGraphClient(clientId, clientSecret, aadAuthProps, endpointsProps);
+
+ stubFor(get(urlEqualTo("/memberOf"))
+ .withHeader(ACCEPT, equalTo("application/json;odata=minimalmetadata"))
+ .willReturn(aResponse()
+ .withStatus(200)
+ .withHeader(CONTENT_TYPE, APPLICATION_JSON_VALUE)
+ .withBody(userGroupsJson)));
+
+ assertThat(graphClientMock.getGrantedAuthorities(Constants.BEARER_TOKEN)).isNotEmpty()
+ .extracting(GrantedAuthority::getAuthority).containsExactly("ROLE_group1");
+
+ verify(getRequestedFor(urlMatching("/memberOf"))
+ .withHeader(HttpHeaders.AUTHORIZATION, equalTo(String.format("%s", accessToken)))
+ .withHeader(ACCEPT, equalTo("application/json;odata=minimalmetadata"))
+ .withHeader("api-version", equalTo("1.6")));
+ }
+
+ @Test
+ public void getGroups() throws Exception {
+ aadAuthProps.setActiveDirectoryGroups(Arrays.asList("group1", "group2", "group3"));
+ this.graphClientMock = new AzureADGraphClient(clientId, clientSecret, aadAuthProps, endpointsProps);
+
+ stubFor(get(urlEqualTo("/memberOf"))
+ .withHeader(ACCEPT, equalTo("application/json;odata=minimalmetadata"))
+ .willReturn(aResponse()
+ .withStatus(200)
+ .withHeader(CONTENT_TYPE, APPLICATION_JSON_VALUE)
+ .withBody(userGroupsJson)));
+
+ final Collection extends GrantedAuthority> authorities = graphClientMock
+ .getGrantedAuthorities(Constants.BEARER_TOKEN);
+
+ assertThat(authorities).isNotEmpty().extracting(GrantedAuthority::getAuthority)
+ .containsExactly("ROLE_group1", "ROLE_group2", "ROLE_group3");
+
+ verify(getRequestedFor(urlMatching("/memberOf"))
+ .withHeader(HttpHeaders.AUTHORIZATION, equalTo(String.format("%s", accessToken)))
+ .withHeader(ACCEPT, equalTo("application/json;odata=minimalmetadata"))
+ .withHeader("api-version", equalTo("1.6")));
+ }
+
+ @Test
+ public void userPrincipalIsSerializable() throws ParseException, IOException, ClassNotFoundException {
+ final File tmpOutputFile = File.createTempFile("test-user-principal", "txt");
+
+ try (FileOutputStream fileOutputStream = new FileOutputStream(tmpOutputFile);
+ ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
+ FileInputStream fileInputStream = new FileInputStream(tmpOutputFile);
+ ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream)) {
+
+ final JWSObject jwsObject = JWSObject.parse(Constants.JWT_TOKEN);
+ final JWTClaimsSet jwtClaimsSet = new JWTClaimsSet.Builder().subject("fake-subject").build();
+ final UserPrincipal principal = new UserPrincipal(jwsObject, jwtClaimsSet);
+
+ objectOutputStream.writeObject(principal);
+
+ final UserPrincipal serializedPrincipal = (UserPrincipal) objectInputStream.readObject();
+
+ Assert.assertNotNull("Serialized UserPrincipal not null", serializedPrincipal);
+ Assert.assertFalse("Serialized UserPrincipal kid not empty",
+ StringUtils.isEmpty(serializedPrincipal.getKid()));
+ Assert.assertNotNull("Serialized UserPrincipal claims not null.", serializedPrincipal.getClaims());
+ Assert.assertTrue("Serialized UserPrincipal claims not empty.",
+ serializedPrincipal.getClaims().size() > 0);
+ } finally {
+ Files.deleteIfExists(tmpOutputFile.toPath());
+ }
+ }
+}
diff --git a/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/aad/UserPrincipalManagerAudienceTest.java b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/aad/UserPrincipalManagerAudienceTest.java
new file mode 100644
index 000000000000..8a3b23caac0f
--- /dev/null
+++ b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/aad/UserPrincipalManagerAudienceTest.java
@@ -0,0 +1,149 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.microsoft.azure.spring.autoconfigure.aad;
+
+import static org.assertj.core.api.Assertions.assertThatCode;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import com.nimbusds.jose.JOSEException;
+import com.nimbusds.jose.JWSAlgorithm;
+import com.nimbusds.jose.JWSHeader;
+import com.nimbusds.jose.JWSSigner;
+import com.nimbusds.jose.crypto.RSASSASigner;
+import com.nimbusds.jose.jwk.JWKSet;
+import com.nimbusds.jose.jwk.RSAKey;
+import com.nimbusds.jose.util.Resource;
+import com.nimbusds.jose.util.ResourceRetriever;
+import com.nimbusds.jwt.JWTClaimsSet;
+import com.nimbusds.jwt.SignedJWT;
+
+import java.io.IOException;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.NoSuchAlgorithmException;
+import java.security.interfaces.RSAPrivateKey;
+import java.security.interfaces.RSAPublicKey;
+import java.time.Instant;
+import java.util.Date;
+
+import org.junit.Before;
+import org.junit.Test;
+
+public class UserPrincipalManagerAudienceTest {
+
+ private static final String FAKE_CLIENT_ID = "dsflkjsdflkjsdf";
+ private static final String FAKE_APPLICATION_URI = "https://oihiugjuzfvbhg";
+
+ private JWSSigner signer;
+ private String jwkString;
+ private ResourceRetriever resourceRetriever;
+
+ private ServiceEndpointsProperties serviceEndpointsProperties;
+ private AADAuthenticationProperties aadAuthenticationProperties;
+ private UserPrincipalManager userPrincipalManager;
+
+ @Before
+ public void setupKeys() throws NoSuchAlgorithmException, IOException {
+ final KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA");
+ kpg.initialize(2048);
+
+ final KeyPair kp = kpg.genKeyPair();
+ final RSAPrivateKey privateKey = (RSAPrivateKey) kp.getPrivate();
+
+ signer = new RSASSASigner(privateKey);
+
+ final RSAKey rsaJWK = new RSAKey.Builder((RSAPublicKey) kp.getPublic())
+ .privateKey((RSAPrivateKey) kp.getPrivate())
+ .keyID("1")
+ .build();
+ final JWKSet jwkSet = new JWKSet(rsaJWK);
+ jwkString = jwkSet.toString();
+
+ resourceRetriever = url -> new Resource(jwkString, "application/json");
+
+ serviceEndpointsProperties = mock(ServiceEndpointsProperties.class);
+ aadAuthenticationProperties = new AADAuthenticationProperties();
+ aadAuthenticationProperties.setClientId(FAKE_CLIENT_ID);
+ aadAuthenticationProperties.setAppIdUri(FAKE_APPLICATION_URI);
+ final ServiceEndpoints serviceEndpoints = new ServiceEndpoints();
+ serviceEndpoints.setAadKeyDiscoveryUri("file://dummy");
+ when(serviceEndpointsProperties.getServiceEndpoints(anyString())).thenReturn(serviceEndpoints);
+ }
+
+ @Test
+ public void allowApplicationUriAsAudience() throws JOSEException {
+ final JWTClaimsSet claimsSetOne = new JWTClaimsSet.Builder()
+ .subject("foo")
+ .issueTime(Date.from(Instant.now().minusSeconds(60)))
+ .issuer("https://sts.windows.net/")
+ .audience(FAKE_CLIENT_ID)
+ .build();
+ final SignedJWT signedJWT = new SignedJWT(new JWSHeader(JWSAlgorithm.RS256), claimsSetOne);
+ signedJWT.sign(signer);
+
+ final String orderTwo = signedJWT.serialize();
+ userPrincipalManager = new UserPrincipalManager(serviceEndpointsProperties, aadAuthenticationProperties,
+ resourceRetriever, true);
+ assertThatCode(() -> userPrincipalManager.buildUserPrincipal(orderTwo))
+ .doesNotThrowAnyException();
+ }
+
+ @Test
+ public void allowClientIdAsAudience() throws JOSEException {
+ final JWTClaimsSet claimsSetOne = new JWTClaimsSet.Builder()
+ .subject("foo")
+ .issueTime(Date.from(Instant.now().minusSeconds(60)))
+ .issuer("https://sts.windows.net/")
+ .audience(FAKE_APPLICATION_URI)
+ .build();
+ final SignedJWT signedJWT = new SignedJWT(new JWSHeader(JWSAlgorithm.RS256), claimsSetOne);
+ signedJWT.sign(signer);
+
+ final String orderTwo = signedJWT.serialize();
+ userPrincipalManager = new UserPrincipalManager(serviceEndpointsProperties, aadAuthenticationProperties,
+ resourceRetriever, true);
+ assertThatCode(() -> userPrincipalManager.buildUserPrincipal(orderTwo))
+ .doesNotThrowAnyException();
+ }
+
+ @Test
+ public void failWithUnkownAudience() throws JOSEException {
+ final JWTClaimsSet claimsSetOne = new JWTClaimsSet.Builder()
+ .subject("foo")
+ .issueTime(Date.from(Instant.now().minusSeconds(60)))
+ .issuer("https://sts.windows.net/")
+ .audience("unknown audience")
+ .build();
+ final SignedJWT signedJWT = new SignedJWT(new JWSHeader(JWSAlgorithm.RS256), claimsSetOne);
+ signedJWT.sign(signer);
+
+ final String orderTwo = signedJWT.serialize();
+ userPrincipalManager = new UserPrincipalManager(serviceEndpointsProperties, aadAuthenticationProperties,
+ resourceRetriever, true);
+ assertThatCode(() -> userPrincipalManager.buildUserPrincipal(orderTwo))
+ .hasMessageContaining("Invalid token audience.");
+ }
+
+ @Test
+ public void failOnInvalidSiganture() throws JOSEException {
+ final JWTClaimsSet claimsSetOne = new JWTClaimsSet.Builder()
+ .subject("foo")
+ .issueTime(Date.from(Instant.now().minusSeconds(60)))
+ .issuer("https://sts.windows.net/")
+ .audience(FAKE_APPLICATION_URI)
+ .build();
+ final SignedJWT signedJWT = new SignedJWT(new JWSHeader(JWSAlgorithm.RS256), claimsSetOne);
+ signedJWT.sign(signer);
+
+ final String orderTwo = signedJWT.serialize();
+ final String invalidToken = orderTwo.substring(0, orderTwo.length() - 5);
+
+ userPrincipalManager = new UserPrincipalManager(serviceEndpointsProperties, aadAuthenticationProperties,
+ resourceRetriever, true);
+ assertThatCode(() -> userPrincipalManager.buildUserPrincipal(invalidToken))
+ .hasMessageContaining("JWT rejected: Invalid signature");
+ }
+}
diff --git a/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/aad/UserPrincipalManagerTest.java b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/aad/UserPrincipalManagerTest.java
new file mode 100644
index 000000000000..ebff6c0c5080
--- /dev/null
+++ b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/aad/UserPrincipalManagerTest.java
@@ -0,0 +1,78 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.microsoft.azure.spring.autoconfigure.aad;
+
+import com.nimbusds.jose.jwk.JWK;
+import com.nimbusds.jose.jwk.JWKSet;
+import com.nimbusds.jose.jwk.source.ImmutableJWKSet;
+import com.nimbusds.jose.proc.SecurityContext;
+import com.nimbusds.jwt.proc.BadJWTException;
+import junitparams.FileParameters;
+import junitparams.JUnitParamsRunner;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatCode;
+
+@RunWith(JUnitParamsRunner.class)
+public class UserPrincipalManagerTest {
+
+ private static ImmutableJWKSet immutableJWKSet;
+
+ @BeforeClass
+ public static void setupClass() throws Exception {
+ final X509Certificate cert = (X509Certificate) CertificateFactory.getInstance("X.509")
+ .generateCertificate(Files.newInputStream(Paths.get("src/test/resources/test-public-key.txt")));
+ immutableJWKSet = new ImmutableJWKSet<>(new JWKSet(JWK.parse(
+ cert)));
+ }
+
+ private UserPrincipalManager userPrincipalManager;
+
+
+ @Test
+ public void testAlgIsTakenFromJWT() throws Exception {
+ userPrincipalManager = new UserPrincipalManager(immutableJWKSet);
+ final UserPrincipal userPrincipal = userPrincipalManager.buildUserPrincipal(
+ new String(Files.readAllBytes(
+ Paths.get("src/test/resources/jwt-signed.txt")), StandardCharsets.UTF_8));
+ assertThat(userPrincipal).isNotNull().extracting(UserPrincipal::getIssuer, UserPrincipal::getSubject)
+ .containsExactly("https://sts.windows.net/test", "test@example.com");
+ }
+
+ @Test
+ public void invalidIssuer() {
+ userPrincipalManager = new UserPrincipalManager(immutableJWKSet);
+ assertThatCode(() -> userPrincipalManager.buildUserPrincipal(
+ new String(Files.readAllBytes(
+ Paths.get("src/test/resources/jwt-bad-issuer.txt")), StandardCharsets.UTF_8)))
+ .isInstanceOf(BadJWTException.class);
+ }
+
+ @Test
+ //TODO: add more generated tokens with other valid issuers to this file. Didn't manage to generate them
+ @FileParameters("src/test/resources/jwt-valid-issuer.txt")
+ public void validIssuer(final String token) {
+ userPrincipalManager = new UserPrincipalManager(immutableJWKSet);
+ assertThatCode(() -> userPrincipalManager.buildUserPrincipal(token))
+ .doesNotThrowAnyException();
+ }
+
+ @Test
+ public void nullIssuer() {
+ userPrincipalManager = new UserPrincipalManager(immutableJWKSet);
+ assertThatCode(() -> userPrincipalManager.buildUserPrincipal(
+ new String(Files.readAllBytes(
+ Paths.get("src/test/resources/jwt-null-issuer.txt")), StandardCharsets.UTF_8)))
+ .isInstanceOf(BadJWTException.class);
+ }
+}
diff --git a/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/aad/UserPrincipalMicrosoftGraphTest.java b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/aad/UserPrincipalMicrosoftGraphTest.java
new file mode 100644
index 000000000000..88fd36f442d6
--- /dev/null
+++ b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/aad/UserPrincipalMicrosoftGraphTest.java
@@ -0,0 +1,160 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.microsoft.azure.spring.autoconfigure.aad;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.github.tomakehurst.wiremock.junit.WireMockRule;
+import com.nimbusds.jose.JWSObject;
+import com.nimbusds.jwt.JWTClaimsSet;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.util.StringUtils;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.nio.file.Files;
+import java.text.ParseException;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
+import static com.github.tomakehurst.wiremock.client.WireMock.equalTo;
+import static com.github.tomakehurst.wiremock.client.WireMock.get;
+import static com.github.tomakehurst.wiremock.client.WireMock.getRequestedFor;
+import static com.github.tomakehurst.wiremock.client.WireMock.stubFor;
+import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo;
+import static com.github.tomakehurst.wiremock.client.WireMock.urlMatching;
+import static com.github.tomakehurst.wiremock.client.WireMock.verify;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.springframework.http.HttpHeaders.ACCEPT;
+import static org.springframework.http.HttpHeaders.AUTHORIZATION;
+import static org.springframework.http.HttpHeaders.CONTENT_TYPE;
+import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
+
+public class UserPrincipalMicrosoftGraphTest {
+ @Rule
+ public WireMockRule wireMockRule = new WireMockRule(9519);
+
+ private AzureADGraphClient graphClientMock;
+ private String clientId;
+ private String clientSecret;
+ private AADAuthenticationProperties aadAuthProps;
+ private ServiceEndpointsProperties endpointsProps;
+ private String accessToken;
+ private static String userGroupsJson;
+
+ static {
+ try {
+ final ObjectMapper objectMapper = new ObjectMapper();
+ final Map json = objectMapper.readValue(UserPrincipalMicrosoftGraphTest.class
+ .getClassLoader().getResourceAsStream("aad/microsoft-graph-user-groups.json"),
+ new TypeReference>() { });
+ userGroupsJson = objectMapper.writeValueAsString(json);
+ } catch (IOException e) {
+ e.printStackTrace();
+ userGroupsJson = null;
+ }
+ Assert.assertNotNull(userGroupsJson);
+ }
+
+ @Before
+ public void setup() {
+ accessToken = MicrosoftGraphConstants.BEARER_TOKEN;
+ aadAuthProps = new AADAuthenticationProperties();
+ aadAuthProps.setEnvironment("global-v2-graph");
+ aadAuthProps.getUserGroup().setKey("@odata.type");
+ aadAuthProps.getUserGroup().setValue("#microsoft.graph.group");
+ aadAuthProps.getUserGroup().setObjectIDKey("id");
+ endpointsProps = new ServiceEndpointsProperties();
+ final ServiceEndpoints serviceEndpoints = new ServiceEndpoints();
+ serviceEndpoints.setAadMembershipRestUri("http://localhost:9519/memberOf");
+ endpointsProps.getEndpoints().put("global-v2-graph", serviceEndpoints);
+ clientId = "client";
+ clientSecret = "pass";
+ }
+
+ @Test
+ public void getAuthoritiesByUserGroups() throws Exception {
+ aadAuthProps.getUserGroup().setAllowedGroups(Collections.singletonList("group1"));
+ this.graphClientMock = new AzureADGraphClient(clientId, clientSecret, aadAuthProps, endpointsProps);
+
+ stubFor(get(urlEqualTo("/memberOf"))
+ .withHeader(ACCEPT, equalTo(APPLICATION_JSON_VALUE))
+ .willReturn(aResponse()
+ .withStatus(200)
+ .withHeader(CONTENT_TYPE, APPLICATION_JSON_VALUE)
+ .withBody(userGroupsJson)));
+
+ assertThat(graphClientMock.getGrantedAuthorities(MicrosoftGraphConstants.BEARER_TOKEN))
+ .isNotEmpty()
+ .extracting(GrantedAuthority::getAuthority)
+ .containsExactly("ROLE_group1");
+
+ verify(getRequestedFor(urlMatching("/memberOf"))
+ .withHeader(AUTHORIZATION, equalTo(String.format("Bearer %s", accessToken)))
+ .withHeader(ACCEPT, equalTo(APPLICATION_JSON_VALUE)));
+ }
+
+ @Test
+ public void getGroups() throws Exception {
+ aadAuthProps.setActiveDirectoryGroups(Arrays.asList("group1", "group2", "group3"));
+ this.graphClientMock = new AzureADGraphClient(clientId, clientSecret, aadAuthProps, endpointsProps);
+
+ stubFor(get(urlEqualTo("/memberOf"))
+ .withHeader(ACCEPT, equalTo(APPLICATION_JSON_VALUE))
+ .willReturn(aResponse()
+ .withStatus(200)
+ .withHeader(CONTENT_TYPE, APPLICATION_JSON_VALUE)
+ .withBody(userGroupsJson)));
+
+ final Collection extends GrantedAuthority> authorities = graphClientMock
+ .getGrantedAuthorities(MicrosoftGraphConstants.BEARER_TOKEN);
+
+ assertThat(authorities).isNotEmpty().extracting(GrantedAuthority::getAuthority)
+ .containsExactly("ROLE_group1", "ROLE_group2", "ROLE_group3");
+
+ verify(getRequestedFor(urlMatching("/memberOf"))
+ .withHeader(AUTHORIZATION, equalTo(String.format("Bearer %s", accessToken)))
+ .withHeader(ACCEPT, equalTo(APPLICATION_JSON_VALUE)));
+ }
+
+ @Test
+ public void userPrincipalIsSerializable() throws ParseException, IOException, ClassNotFoundException {
+ final File tmpOutputFile = File.createTempFile("test-user-principal", "txt");
+
+ try (FileOutputStream fileOutputStream = new FileOutputStream(tmpOutputFile);
+ ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
+ FileInputStream fileInputStream = new FileInputStream(tmpOutputFile);
+ ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream)) {
+
+ final JWSObject jwsObject = JWSObject.parse(MicrosoftGraphConstants.JWT_TOKEN);
+ final JWTClaimsSet jwtClaimsSet = new JWTClaimsSet.Builder().subject("fake-subject").build();
+ final UserPrincipal principal = new UserPrincipal(jwsObject, jwtClaimsSet);
+
+ objectOutputStream.writeObject(principal);
+
+ final UserPrincipal serializedPrincipal = (UserPrincipal) objectInputStream.readObject();
+
+ Assert.assertNotNull("Serialized UserPrincipal not null", serializedPrincipal);
+ Assert.assertFalse("Serialized UserPrincipal kid not empty",
+ StringUtils.isEmpty(serializedPrincipal.getKid()));
+ Assert.assertNotNull("Serialized UserPrincipal claims not null.", serializedPrincipal.getClaims());
+ Assert.assertTrue("Serialized UserPrincipal claims not empty.",
+ serializedPrincipal.getClaims().size() > 0);
+ } finally {
+ Files.deleteIfExists(tmpOutputFile.toPath());
+ }
+ }
+}
diff --git a/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/utils/TestUtils.java b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/utils/TestUtils.java
new file mode 100644
index 000000000000..2bfae28b6710
--- /dev/null
+++ b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/utils/TestUtils.java
@@ -0,0 +1,20 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+package com.microsoft.azure.utils;
+
+public final class TestUtils {
+
+ private TestUtils() {
+ }
+
+ /**
+ *
+ * @param propName property name
+ * @param propValue value of property
+ * @return property name and value pair. e.g., prop.name=prop.value
+ */
+ public static String propPair(String propName, String propValue) {
+ return propName + "=" + propValue;
+ }
+}
diff --git a/sdk/spring/azure-spring-boot/src/test/resources/aad-backend-oauth2-minimum.properties b/sdk/spring/azure-spring-boot/src/test/resources/aad-backend-oauth2-minimum.properties
new file mode 100644
index 000000000000..0931172601a1
--- /dev/null
+++ b/sdk/spring/azure-spring-boot/src/test/resources/aad-backend-oauth2-minimum.properties
@@ -0,0 +1,5 @@
+spring.security.oauth2.client.registration.azure.client-id=abcd
+spring.security.oauth2.client.registration.azure.client-secret=password
+
+azure.activedirectory.tenant-id=my-tanant-id
+azure.activedirectory.active-directory-groups=my-aad-group1, my-aad-group2
diff --git a/sdk/spring/azure-spring-boot/src/test/resources/aad.enable.config b/sdk/spring/azure-spring-boot/src/test/resources/aad.enable.config
new file mode 100644
index 000000000000..2995a4d0e749
--- /dev/null
+++ b/sdk/spring/azure-spring-boot/src/test/resources/aad.enable.config
@@ -0,0 +1 @@
+dummy
\ No newline at end of file
diff --git a/sdk/spring/azure-spring-boot/src/test/resources/aad/azure-ad-graph-user-groups.json b/sdk/spring/azure-spring-boot/src/test/resources/aad/azure-ad-graph-user-groups.json
new file mode 100644
index 000000000000..baf41c7408b2
--- /dev/null
+++ b/sdk/spring/azure-spring-boot/src/test/resources/aad/azure-ad-graph-user-groups.json
@@ -0,0 +1,65 @@
+{
+ "odata.metadata": "https://graph.windows.net/myorganization/$metadata#directoryObjects",
+ "value": [
+ {
+ "odata.type": "Microsoft.DirectoryServices.Group",
+ "objectType": "Group",
+ "objectId": "12345678-7baf-48ce-96f4-a2d60c26391e",
+ "deletionTimestamp": null,
+ "description": "this is group1",
+ "dirSyncEnabled": true,
+ "displayName": "group1",
+ "lastDirSyncTime": "2017-08-02T12:54:37Z",
+ "mail": null,
+ "mailNickname": "something",
+ "mailEnabled": false,
+ "onPremisesDomainName": null,
+ "onPremisesNetBiosName": null,
+ "onPremisesSamAccountName": null,
+ "onPremisesSecurityIdentifier": "S-1-5-21-1234567885-903363285-719344707-285039",
+ "provisioningErrors": [],
+ "proxyAddresses": [],
+ "securityEnabled": true
+ },
+ {
+ "odata.type": "Microsoft.DirectoryServices.Group",
+ "objectType": "Group",
+ "objectId": "12345678-e757-4474-b9c4-3f00a9ac17a0",
+ "deletionTimestamp": null,
+ "description": null,
+ "dirSyncEnabled": true,
+ "displayName": "group2",
+ "lastDirSyncTime": "2017-08-09T13:45:03Z",
+ "mail": null,
+ "mailNickname": "somethingelse",
+ "mailEnabled": false,
+ "onPremisesDomainName": null,
+ "onPremisesNetBiosName": null,
+ "onPremisesSamAccountName": null,
+ "onPremisesSecurityIdentifier": "S-1-5-21-1234567885-903363285-719344707-28565",
+ "provisioningErrors": [],
+ "proxyAddresses": [],
+ "securityEnabled": true
+ },
+ {
+ "odata.type": "Microsoft.DirectoryServices.Group",
+ "objectType": "Group",
+ "objectId": "12345678-86a4-4237-aeb0-60bad29c1de0",
+ "deletionTimestamp": null,
+ "description": "this is group3",
+ "dirSyncEnabled": true,
+ "displayName": "group3",
+ "lastDirSyncTime": "2017-08-09T05:41:43Z",
+ "mail": null,
+ "mailNickname": "somethingelse",
+ "mailEnabled": false,
+ "onPremisesDomainName": null,
+ "onPremisesNetBiosName": null,
+ "onPremisesSamAccountName": null,
+ "onPremisesSecurityIdentifier": "S-1-5-21-1234567884-1604012920-1887927527-14401381",
+ "provisioningErrors": [],
+ "proxyAddresses": [],
+ "securityEnabled": true
+ }],
+ "odata.nextLink": "directoryObjects/$/Microsoft.DirectoryServices.User/12345678-2898-434a-a370-8ec974c2fb57/memberOf?$skiptoken=X'445370740700010000000000000000100000009D29CBA7B45D854A84FF7F9B636BD9DC000000000000000000000017312E322E3834302E3131333535362E312E342E3233333100000000'"
+}
diff --git a/sdk/spring/azure-spring-boot/src/test/resources/aad/microsoft-graph-user-groups.json b/sdk/spring/azure-spring-boot/src/test/resources/aad/microsoft-graph-user-groups.json
new file mode 100644
index 000000000000..93497a317b70
--- /dev/null
+++ b/sdk/spring/azure-spring-boot/src/test/resources/aad/microsoft-graph-user-groups.json
@@ -0,0 +1,80 @@
+{
+ "odata.metadata": "https://graph.windows.net/myorganization/$metadata#directoryObjects",
+ "value": [
+ {
+ "@odata.type": "#microsoft.graph.group",
+ "id": "12345678-7baf-48ce-96f4-a2d60c26391e",
+ "deletedDateTime": null,
+ "classification": null,
+ "createdDateTime": "2017-08-02T12:54:37Z",
+ "creationOptions": [],
+ "description": "this is group1",
+ "displayName": "group1",
+ "groupTypes": [],
+ "mail": null,
+ "mailEnabled": false,
+ "mailNickname": "something",
+ "onPremisesLastSyncDateTime": null,
+ "onPremisesSecurityIdentifier": null,
+ "onPremisesSyncEnabled": null,
+ "preferredDataLocation": null,
+ "proxyAddresses": [],
+ "renewedDateTime": "2017-08-02T12:54:37Z",
+ "resourceBehaviorOptions": [],
+ "resourceProvisioningOptions": [],
+ "securityEnabled": true,
+ "visibility": null,
+ "onPremisesProvisioningErrors": []
+ },
+ {
+ "@odata.type": "#microsoft.graph.group",
+ "id": "12345678-e757-4474-b9c4-3f00a9ac17a0",
+ "deletedDateTime": null,
+ "classification": null,
+ "createdDateTime": "2017-08-09T13:45:03Z",
+ "creationOptions": [],
+ "description": "this is group2",
+ "displayName": "group2",
+ "groupTypes": [],
+ "mail": null,
+ "mailEnabled": false,
+ "mailNickname": "somethingelse",
+ "onPremisesLastSyncDateTime": null,
+ "onPremisesSecurityIdentifier": null,
+ "onPremisesSyncEnabled": null,
+ "preferredDataLocation": null,
+ "proxyAddresses": [],
+ "renewedDateTime": "2017-08-09T13:45:03Z",
+ "resourceBehaviorOptions": [],
+ "resourceProvisioningOptions": [],
+ "securityEnabled": true,
+ "visibility": null,
+ "onPremisesProvisioningErrors": []
+ },
+ {
+ "@odata.type": "#microsoft.graph.group",
+ "id": "12345678-86a4-4237-aeb0-60bad29c1de0",
+ "deletedDateTime": null,
+ "classification": null,
+ "createdDateTime": "2017-08-09T05:41:43Z",
+ "creationOptions": [],
+ "description": "this is group3",
+ "displayName": "group3",
+ "groupTypes": [],
+ "mail": null,
+ "mailEnabled": false,
+ "mailNickname": "somethingelse",
+ "onPremisesLastSyncDateTime": null,
+ "onPremisesSecurityIdentifier": null,
+ "onPremisesSyncEnabled": null,
+ "preferredDataLocation": null,
+ "proxyAddresses": [],
+ "renewedDateTime": "2017-08-09T05:41:43Z",
+ "resourceBehaviorOptions": [],
+ "resourceProvisioningOptions": [],
+ "securityEnabled": true,
+ "visibility": null,
+ "onPremisesProvisioningErrors": []
+ }],
+ "odata.nextLink": "directoryObjects/$/Microsoft.DirectoryServices.User/12345678-2898-434a-a370-8ec974c2fb57/memberOf?$skiptoken=X'445370740700010000000000000000100000009D29CBA7B45D854A84FF7F9B636BD9DC000000000000000000000017312E322E3834302E3131333535362E312E342E3233333100000000'"
+}
diff --git a/sdk/spring/azure-spring-boot/src/test/resources/aadb2c.enable.config b/sdk/spring/azure-spring-boot/src/test/resources/aadb2c.enable.config
new file mode 100644
index 000000000000..2995a4d0e749
--- /dev/null
+++ b/sdk/spring/azure-spring-boot/src/test/resources/aadb2c.enable.config
@@ -0,0 +1 @@
+dummy
\ No newline at end of file
diff --git a/sdk/spring/azure-spring-boot/src/test/resources/cosmosdb.enable.config b/sdk/spring/azure-spring-boot/src/test/resources/cosmosdb.enable.config
new file mode 100644
index 000000000000..2995a4d0e749
--- /dev/null
+++ b/sdk/spring/azure-spring-boot/src/test/resources/cosmosdb.enable.config
@@ -0,0 +1 @@
+dummy
\ No newline at end of file
diff --git a/sdk/spring/azure-spring-boot/src/test/resources/fake-pfx-cert.pfx b/sdk/spring/azure-spring-boot/src/test/resources/fake-pfx-cert.pfx
new file mode 100644
index 000000000000..ea44792776c8
--- /dev/null
+++ b/sdk/spring/azure-spring-boot/src/test/resources/fake-pfx-cert.pfx
@@ -0,0 +1 @@
+for test file extension only
\ No newline at end of file
diff --git a/sdk/spring/azure-spring-boot/src/test/resources/gremlin.enable.config b/sdk/spring/azure-spring-boot/src/test/resources/gremlin.enable.config
new file mode 100644
index 000000000000..2995a4d0e749
--- /dev/null
+++ b/sdk/spring/azure-spring-boot/src/test/resources/gremlin.enable.config
@@ -0,0 +1 @@
+dummy
\ No newline at end of file
diff --git a/sdk/spring/azure-spring-boot/src/test/resources/jwt-bad-issuer.txt b/sdk/spring/azure-spring-boot/src/test/resources/jwt-bad-issuer.txt
new file mode 100644
index 000000000000..28afc8898855
--- /dev/null
+++ b/sdk/spring/azure-spring-boot/src/test/resources/jwt-bad-issuer.txt
@@ -0,0 +1 @@
+eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL3dyb25naXNzdWVyLm5ldHQvdGVzdCIsInN1YiI6InRlc3RAZXhhbXBsZS5jb20iLCJuYmYiOjE1NDUwMDg5MDYsImV4cCI6OTk5OTk5OTk5OTksImlhdCI6MTU0NTAwODkwNiwianRpIjoidGVzdGlkIiwidHlwIjoiaHR0cHM6Ly9leGFtcGxlLmNvbS9yZWdpc3RlciJ9.lK-pSsNMHHui8IxSCYs7e4gH9I29ug7CyNnT8GIJBWEZCeLs3IRnlkLG923Zn7eT_4A0aWFyEnjCBYIKtqX7AoaLBwCa08yB9x7c0DbJQvdKwFjGzc5zkpNxnzBZxXLJr7D9nMAjeQrYLkgoy4XXHL_m_Z6PTf9Jwl6tTYqUS06gd5ZokV1DtBTTPeDJj7KKzNhY3PQ1Hh_-RLoCspqIiZFZ8dfPgDCc2OXVCsH8_2tUFCktuPuVYD11Ws7_hFG6sq8AF1jyugrtYnwMhbzpMCtkL-SoZsmBtmAUFW20vTNYV6Vri-VEqz5VkHef9ZqZmlNPR0vH8hcZVj0IX8t7yA
\ No newline at end of file
diff --git a/sdk/spring/azure-spring-boot/src/test/resources/jwt-null-issuer.txt b/sdk/spring/azure-spring-boot/src/test/resources/jwt-null-issuer.txt
new file mode 100644
index 000000000000..ac1382b4f5eb
--- /dev/null
+++ b/sdk/spring/azure-spring-boot/src/test/resources/jwt-null-issuer.txt
@@ -0,0 +1 @@
+eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ0ZXN0QGV4YW1wbGUuY29tIiwibmJmIjoxNTQ1MDA5MjQ5LCJleHAiOjk5OTk5OTk5OTk5LCJpYXQiOjE1NDUwMDkyNDksImp0aSI6InRlc3RpZCIsInR5cCI6Imh0dHBzOi8vZXhhbXBsZS5jb20vcmVnaXN0ZXIifQ.2rHU8-_UWjE0U2Vq9KDmtt1ztlj_G9OW777O-kZ_di7dOBZPt6H2eMba34Qf5wILfs1bHBubMNIs64B9mLffJzXp_FKyMdcCsYecJAOaSscrSLjHYdnZqhRIETOloz-nbxiH_AhaJP6Hb482Hu7It4XhcxWU_tZ9kRD1brfoyb_-8Qh4vmrR4eddtfLZDlr3xFfTSD9FKDeECDWu59wGLBVS_32Y42XYV82f5PD1FsAG62vC-t2XdVS-y6aQIT1QElsKcc66xY21XgXq4fkFGxyoYPB1hCLIPz_QMJxRXql7AnVoxkueQxMzH4NCT64i1Aj7texhHbZh4-_jG29-zg
\ No newline at end of file
diff --git a/sdk/spring/azure-spring-boot/src/test/resources/jwt-signed.txt b/sdk/spring/azure-spring-boot/src/test/resources/jwt-signed.txt
new file mode 100644
index 000000000000..0ca89f3afca7
--- /dev/null
+++ b/sdk/spring/azure-spring-boot/src/test/resources/jwt-signed.txt
@@ -0,0 +1 @@
+eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL3N0cy53aW5kb3dzLm5ldC90ZXN0Iiwic3ViIjoidGVzdEBleGFtcGxlLmNvbSIsIm5iZiI6MTU0NTAwODE5NiwiZXhwIjo5OTk5OTk5OTk5OSwiaWF0IjoxNTQ1MDA4MTk2LCJqdGkiOiJ0ZXN0aWQiLCJ0eXAiOiJodHRwczovL2V4YW1wbGUuY29tL3JlZ2lzdGVyIn0.ZQceiSqNKiEHrNaPhKCKW2EVEnhGbyh4TjbhqB-P7E70NRS3Ad89ISBaSyhpwRS6lwdpMrwNEETFloGm8H6nv623gcWzTCnb7bqaOWKCNTV9TjvhecjIe69AkNHfvkqyopbyRktKosWm89e2nAgiGtp-Y1Pyrt1_iiwOtvahtGyaWqs82-WkFY61DFI1e4iRBI6WSIGLUUpc4vXCGdQ33OyN6wAQ2IYeHCURmB-stVT-GcoMcDZKJBqnerQsu5WDbSwkZfcVTWDK-l_sz1WSdFGTdSWATZJ_LKvxa8IPX--s0-JRmZf-0dwadjcbCNLwYtYDvtaZyczouZKGGBoWZA
\ No newline at end of file
diff --git a/sdk/spring/azure-spring-boot/src/test/resources/jwt-valid-issuer.txt b/sdk/spring/azure-spring-boot/src/test/resources/jwt-valid-issuer.txt
new file mode 100644
index 000000000000..0ca89f3afca7
--- /dev/null
+++ b/sdk/spring/azure-spring-boot/src/test/resources/jwt-valid-issuer.txt
@@ -0,0 +1 @@
+eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL3N0cy53aW5kb3dzLm5ldC90ZXN0Iiwic3ViIjoidGVzdEBleGFtcGxlLmNvbSIsIm5iZiI6MTU0NTAwODE5NiwiZXhwIjo5OTk5OTk5OTk5OSwiaWF0IjoxNTQ1MDA4MTk2LCJqdGkiOiJ0ZXN0aWQiLCJ0eXAiOiJodHRwczovL2V4YW1wbGUuY29tL3JlZ2lzdGVyIn0.ZQceiSqNKiEHrNaPhKCKW2EVEnhGbyh4TjbhqB-P7E70NRS3Ad89ISBaSyhpwRS6lwdpMrwNEETFloGm8H6nv623gcWzTCnb7bqaOWKCNTV9TjvhecjIe69AkNHfvkqyopbyRktKosWm89e2nAgiGtp-Y1Pyrt1_iiwOtvahtGyaWqs82-WkFY61DFI1e4iRBI6WSIGLUUpc4vXCGdQ33OyN6wAQ2IYeHCURmB-stVT-GcoMcDZKJBqnerQsu5WDbSwkZfcVTWDK-l_sz1WSdFGTdSWATZJ_LKvxa8IPX--s0-JRmZf-0dwadjcbCNLwYtYDvtaZyczouZKGGBoWZA
\ No newline at end of file
diff --git a/sdk/spring/azure-spring-boot/src/test/resources/logback-test.xml b/sdk/spring/azure-spring-boot/src/test/resources/logback-test.xml
new file mode 100644
index 000000000000..47e7acf72ba1
--- /dev/null
+++ b/sdk/spring/azure-spring-boot/src/test/resources/logback-test.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/sdk/spring/azure-spring-boot/src/test/resources/mediaservices.enable.config b/sdk/spring/azure-spring-boot/src/test/resources/mediaservices.enable.config
new file mode 100644
index 000000000000..2995a4d0e749
--- /dev/null
+++ b/sdk/spring/azure-spring-boot/src/test/resources/mediaservices.enable.config
@@ -0,0 +1 @@
+dummy
\ No newline at end of file
diff --git a/sdk/spring/azure-spring-boot/src/test/resources/metrics.enable.config b/sdk/spring/azure-spring-boot/src/test/resources/metrics.enable.config
new file mode 100644
index 000000000000..2995a4d0e749
--- /dev/null
+++ b/sdk/spring/azure-spring-boot/src/test/resources/metrics.enable.config
@@ -0,0 +1 @@
+dummy
\ No newline at end of file
diff --git a/sdk/spring/azure-spring-boot/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/sdk/spring/azure-spring-boot/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
new file mode 100644
index 000000000000..1f0955d450f0
--- /dev/null
+++ b/sdk/spring/azure-spring-boot/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker
@@ -0,0 +1 @@
+mock-maker-inline
diff --git a/sdk/spring/azure-spring-boot/src/test/resources/nopwdcert.pfx b/sdk/spring/azure-spring-boot/src/test/resources/nopwdcert.pfx
new file mode 100644
index 000000000000..fd42b8ccd78e
Binary files /dev/null and b/sdk/spring/azure-spring-boot/src/test/resources/nopwdcert.pfx differ
diff --git a/sdk/spring/azure-spring-boot/src/test/resources/servicebus.enable.config b/sdk/spring/azure-spring-boot/src/test/resources/servicebus.enable.config
new file mode 100644
index 000000000000..2995a4d0e749
--- /dev/null
+++ b/sdk/spring/azure-spring-boot/src/test/resources/servicebus.enable.config
@@ -0,0 +1 @@
+dummy
\ No newline at end of file
diff --git a/sdk/spring/azure-spring-boot/src/test/resources/servicebusjms.enable.config b/sdk/spring/azure-spring-boot/src/test/resources/servicebusjms.enable.config
new file mode 100644
index 000000000000..2995a4d0e749
--- /dev/null
+++ b/sdk/spring/azure-spring-boot/src/test/resources/servicebusjms.enable.config
@@ -0,0 +1 @@
+dummy
\ No newline at end of file
diff --git a/sdk/spring/azure-spring-boot/src/test/resources/storage.enable.config b/sdk/spring/azure-spring-boot/src/test/resources/storage.enable.config
new file mode 100644
index 000000000000..2995a4d0e749
--- /dev/null
+++ b/sdk/spring/azure-spring-boot/src/test/resources/storage.enable.config
@@ -0,0 +1 @@
+dummy
\ No newline at end of file
diff --git a/sdk/spring/azure-spring-boot/src/test/resources/test-public-key.txt b/sdk/spring/azure-spring-boot/src/test/resources/test-public-key.txt
new file mode 100644
index 000000000000..458bcc5fe8cc
--- /dev/null
+++ b/sdk/spring/azure-spring-boot/src/test/resources/test-public-key.txt
@@ -0,0 +1,19 @@
+-----BEGIN CERTIFICATE-----
+MIIC/zCCAeegAwIBAgIBATANBgkqhkiG9w0BAQUFADAaMQswCQYDVQQGEwJVUzEL
+MAkGA1UECgwCWjQwHhcNMTMwODI4MTgyODM0WhcNMjMwODI4MTgyODM0WjAaMQsw
+CQYDVQQGEwJVUzELMAkGA1UECgwCWjQwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw
+ggEKAoIBAQDfdOqotHd55SYO0dLz2oXengw/tZ+q3ZmOPeVmMuOMIYO/Cv1wk2U0
+OK4pug4OBSJPhl09Zs6IwB8NwPOU7EDTgMOcQUYB/6QNCI1J7Zm2oLtuchzz4pIb
++o4ZAhVprLhRyvqi8OTKQ7kfGfs5Tuwmn1M/0fQkfzMxADpjOKNgf0uy6lN6utjd
+TrPKKFUQNdc6/Ty8EeTnQEwUlsT2LAXCfEKxTn5RlRljDztS7Sfgs8VL0FPy1Qi8
+B+dFcgRYKFrcpsVaZ1lBmXKsXDRu5QR/Rg3f9DRq4GR1sNH8RLY9uApMl2SNz+sR
+4zRPG85R/se5Q06Gu0BUQ3UPm67ETVZLAgMBAAGjUDBOMB0GA1UdDgQWBBQHZPTE
+yQVu/0I/3QWhlTyW7WoTzTAfBgNVHSMEGDAWgBQHZPTEyQVu/0I/3QWhlTyW7WoT
+zTAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBQUAA4IBAQDHxqJ9y8alTH7agVMW
+Zfic/RbrdvHwyq+IOrgDToqyo0w+IZ6BCn9vjv5iuhqu4ForOWDAFpQKZW0DLBJE
+Qy/7/0+9pk2DPhK1XzdOovlSrkRt+GcEpGnUXnzACXDBbO0+Wrk+hcjEkQRRK1bW
+2rknARIEJG9GS+pShP9Bq/0BmNsMepdNcBa0z3a5B0fzFyCQoUlX6RTqxRw1h1Qt
+5F00pfsp7SjXVIvYcewHaNASbto1n5hrSz1VY9hLba11ivL1N4WoWbmzAL6BWabs
+C2D/MenST2/X6hTKyGXpg3Eg2h3iLvUtwcNny0hRKstc73Jl9xR3qXfXKJH0ThTl
+q0gq
+-----END CERTIFICATE-----
diff --git a/sdk/spring/azure-spring-boot/src/test/resources/testkeyvault.pfx b/sdk/spring/azure-spring-boot/src/test/resources/testkeyvault.pfx
new file mode 100644
index 000000000000..7754333d91e0
Binary files /dev/null and b/sdk/spring/azure-spring-boot/src/test/resources/testkeyvault.pfx differ
diff --git a/sdk/spring/ci.yml b/sdk/spring/ci.yml
index 9c31a0e88d2c..450120cde9ef 100644
--- a/sdk/spring/ci.yml
+++ b/sdk/spring/ci.yml
@@ -10,11 +10,12 @@ resources:
type: github
name: Azure/azure-sdk-tools
endpoint: azure
-
+
trigger:
branches:
include:
- master
+ - feature/*
- hotfix/*
- release/*
paths:
@@ -31,11 +32,23 @@ pr:
paths:
include:
- sdk/spring/
-
+
stages:
- template: ../../eng/pipelines/templates/stages/archetype-sdk-client.yml
parameters:
ServiceDirectory: spring
Artifacts:
- - name: azure-spring-something
- safeName: azurespringsomething
+ - name: azure-spring-boot
+ groupId: com.microsoft.azure
+ safeName: azurespringboot
+ - name: azure-spring-boot-starter
+ groupId: com.microsoft.azure
+ safeName: azurespringbootstarter
+ - name: azure-active-directory-spring-boot-starter
+ groupId: com.microsoft.azure
+ safeName: azurespringbootstarteractivedirectory
+
+
+
+
+
diff --git a/sdk/spring/pom.xml b/sdk/spring/pom.xml
new file mode 100644
index 000000000000..84980d7d61a7
--- /dev/null
+++ b/sdk/spring/pom.xml
@@ -0,0 +1,17 @@
+
+
+ 4.0.0
+ com.azure
+ azure-spring-boot-service
+ pom
+ 1.0.0
+
+ azure-spring-boot
+ azure-spring-boot-starter
+ azure-spring-boot-starter-active-directory
+
+
+