diff --git a/eng/jacoco-test-coverage/pom.xml b/eng/jacoco-test-coverage/pom.xml index 46ff5323c16a..5aa8c39c69a0 100644 --- a/eng/jacoco-test-coverage/pom.xml +++ b/eng/jacoco-test-coverage/pom.xml @@ -232,6 +232,26 @@ azure-servicebus-jms-spring-boot-starter 2.2.5-beta.1 + + com.microsoft.azure + azure-spring-boot + 2.2.5-beta.1 + + + com.microsoft.azure + azure-spring-boot-starter + 2.2.5-beta.1 + + + com.microsoft.azure + azure-active-directory-spring-boot-starter + 2.2.5-beta.1 + + + com.microsoft.azure + azure-active-directory-b2c-spring-boot-starter + 2.2.5-beta.1 + diff --git a/eng/versioning/version_client.txt b/eng/versioning/version_client.txt index 8fcc84649f77..a6bac276a0cc 100644 --- a/eng/versioning/version_client.txt +++ b/eng/versioning/version_client.txt @@ -44,6 +44,7 @@ com.azure:perf-test-core;1.0.0-beta.1;1.0.0-beta.1 com.azure:azure-test-watcher;1.0.0-beta.1;1.0.0-beta.1 com.microsoft.azure:azure-spring-boot;2.2.4;2.2.5-beta.1 com.microsoft.azure:azure-spring-boot-starter;2.2.4;2.2.5-beta.1 +com.microsoft.azure:azure-active-directory-b2c-spring-boot-starter;2.2.4;2.2.5-beta.1 com.microsoft.azure:azure-active-directory-spring-boot-starter;2.2.4;2.2.5-beta.1 com.microsoft.azure:azure-keyvault-secrets-spring-boot-starter;2.2.4;2.2.5-beta.1 com.microsoft.azure:azure-servicebus-jms-spring-boot-starter;2.2.4;2.2.5-beta.1 diff --git a/eng/versioning/version_data.txt b/eng/versioning/version_data.txt index 6794c70904d4..80e37b6320a7 100644 --- a/eng/versioning/version_data.txt +++ b/eng/versioning/version_data.txt @@ -39,4 +39,4 @@ com.microsoft.azure:azure-storage-blob;11.0.2;11.0.2 com.microsoft.azure.msi_auth_token_provider:azure-authentication-msi-token-provider;1.1.0-beta.1;1.1.0-beta.1 com.microsoft.azure:azure-eventgrid;1.4.0-beta.1;1.4.0-beta.1 com.microsoft.azure:azure-loganalytics;1.0.0-beta-2;1.0.0-beta.2 -com.microsoft.azure:azure-media;1.0.0-beta.1;1.0.0-beta.1 +com.microsoft.azure:azure-media;1.0.0-beta.1;1.0.0-beta.1 \ No newline at end of file diff --git a/sdk/spring/azure-spring-boot-starter-active-directory-b2c/CHANGELOG.md b/sdk/spring/azure-spring-boot-starter-active-directory-b2c/CHANGELOG.md new file mode 100644 index 000000000000..d51e263177a9 --- /dev/null +++ b/sdk/spring/azure-spring-boot-starter-active-directory-b2c/CHANGELOG.md @@ -0,0 +1,3 @@ +# Release History + +## 2.2.5-beta.1 (Unreleased) diff --git a/sdk/spring/azure-spring-boot-starter-active-directory-b2c/README.md b/sdk/spring/azure-spring-boot-starter-active-directory-b2c/README.md new file mode 100644 index 000000000000..a11017a652da --- /dev/null +++ b/sdk/spring/azure-spring-boot-starter-active-directory-b2c/README.md @@ -0,0 +1,220 @@ +# Azure AD B2C Spring Boot Starter client library for Java +## Overview + +Azure Active Directory (Azure AD) B2C is an identity management service that enables you to customize and control how +customers sign up, sign in, and manage their profiles when using your applications. Azure AD B2C enables these actions +while protecting the identities of your customers at the same time. + +## Prerequisites + +The following prerequisites are required in order to complete the steps in this article: + +* A supported Java Development Kit (JDK). For more information about the JDKs available for use when developing on Azure, see . +* [Apache Maven](http://maven.apache.org/), version 3.0 or later. +* Azure subscription. + +If you don't have an Azure subscription, create a [free account](https://azure.microsoft.com/free/?WT.mc_id=A261C142F) before you begin. + +## Getting started + +### Create the Active Directory instance + +1. Log into . + +2. Click **+Create a resource**, then **Identity**, and then **Azure Active Directory B2C**. + +3. Enter your **Organization name** and your **Initial domain name**, record the **domain name** as your +`${your-tenant-name}` and click **Create**. + +4. Select your account name on the top-right of the Azure portal toolbar, then click **Switch directory**. + +5. Select your new Azure Active Directory from the drop-down menu. + +6. Search `b2c` and click `Azure AD B2C` service. + +### Add an application registration for your Spring Boot app + +1. Select **Azure AD B2C** from the portal menu, click **Applications**, and then click **Add**. + +2. Specify your application **Name**, add `http://localhost:8080/home` for the **Reply URL**, record the +**Application ID** as your `${your-client-id}` and then click **Save**. + +3. Select **Keys** from your application, click **Generate key** to generate `${your-client-secret}` and then **Save**. + +4. Select **User flows** on your left, and then **Click** **New user flow **. + +5. Choose **Sign up or in**, **Profile editing** and **Password reset** to create user flows +respectively. Specify your user flow **Name** and **User attributes and claims**, click **Create**. + +## Examples +### Configure and compile your app + +1. Extract the files from the project archive you created and downloaded earlier in this tutorial into a directory. + +2. Navigate to the parent folder for your project, and open the `pom.xml` Maven project file in a text editor. + +3. Add the dependencies for Spring OAuth2 security to the `pom.xml`: + + ```xml + + com.azure + azure-spring-boot-starter-active-directory-b2c + + + org.springframework.boot + spring-boot-starter-thymeleaf + + + org.thymeleaf.extras + thymeleaf-extras-springsecurity5 + + ``` + +4. Save and close the *pom.xml* file. + +5. Navigate to the *src/main/resources* folder in your project and open the *application.yml* file in a text editor. + +6. Specify the settings for your app registration using the values you created earlier; for example: + + ```yaml + azure: + activedirectory: + b2c: + tenant: ${your-tenant-name} + client-id: ${your-client-id} + client-secret: ${your-client-secret} + reply-url: ${your-reply-url-from-aad} # should be absolute url. + logout-success-url: ${you-logout-success-url} + user-flows: + sign-up-or-sign-in: ${your-sign-up-or-in-user-flow} + profile-edit: ${your-profile-edit-user-flow} # optional + password-reset: ${your-password-reset-user-flow} # optional + ``` + Where: + + | Parameter | Description | + |---|---| + | `azure.activedirectory.b2c.tenant` | Contains your AD B2C's `${your-tenant-name` from earlier. | + | `azure.activedirectory.b2c.client-id` | Contains the `${your-client-id}` from your application that you completed earlier. | + | `azure.activedirectory.b2c.client-secret` | Contains the `${your-client-secret}` from your application that you completed earlier. | + | `azure.activedirectory.b2c.reply-url` | Contains one of the **Reply URL** from your application that you completed earlier. | + | `azure.activedirectory.b2c.logout-success-url` | Specify the URL when your application logout successfully. | + | `azure.activedirectory.b2c.user-flows` | Contains the name of the user flows that you completed earlier. + +7. Save and close the *application.yml* file. + +8. Create a folder named *controller* in the Java source folder for your application. + +9. Create a new Java file named *AADB2CWebController.java* in the *controller* folder and open it in a text editor. + +10. Enter the following code, then save and close the file: + +```java +@Controller +public class AADB2CWebController { + + private void initializeModel(Model model, OAuth2AuthenticationToken token) { + if (token != null) { + final OAuth2User user = token.getPrincipal(); + + model.addAttribute("grant_type", user.getAuthorities()); + model.addAllAttributes(user.getAttributes()); + } + } + + @GetMapping(value = "/") + public String index(Model model, OAuth2AuthenticationToken token) { + initializeModel(model, token); + + return "home"; + } + + @GetMapping(value = "/greeting") + public String greeting(Model model, OAuth2AuthenticationToken token) { + initializeModel(model, token); + + return "greeting"; + } + + @GetMapping(value = "/home") + public String home(Model model, OAuth2AuthenticationToken token) { + initializeModel(model, token); + + return "home"; + } +} +``` + +11. Create a folder named *security* in the Java source folder for your application. + +12. Create a new Java file named *AADB2COidcLoginConfigSample.java* in the *security* folder and open it in a text editor. + +13. Enter the following code, then save and close the file: + +```java +@EnableWebSecurity +public class AADB2COidcLoginConfigSample extends WebSecurityConfigurerAdapter { + + private final AADB2COidcLoginConfigurer configurer; + + public AADB2COidcLoginConfigSample(AADB2COidcLoginConfigurer configurer) { + this.configurer = configurer; + } + + @Override + protected void configure(HttpSecurity http) throws Exception { + http.authorizeRequests() + .anyRequest() + .authenticated() + .and() + .apply(configurer); + } +} +``` +14. Copy the `greeting.html` and `home.html` from [Azure AD B2C Spring Boot Sample](../azure-spring-boot-samples/azure-spring-boot-sample-active-directory-b2c-oidc/src/main/resources/templates), and replace the +`${your-profile-edit-user-flow}` and `${your-password-reset-user-flow}` with your user flow name +respectively that completed earlier. + +### Build and test your app + +1. Open a command prompt and change directory to the folder where your app's *pom.xml* file is located. + +2. Build your Spring Boot application with Maven and run it; for example: + + ```shell + mvn clean package + mvn spring-boot:run + ``` + +3. After your application is built and started by Maven, open in a web browser; +you should be redirected to login page. + +4. Click linke with name of `${your-sign-up-or-in}` user flow, you should be rediected Azure AD B2C to start the authentication process. + +4. After you have logged in successfully, you should see the sample `home page` from the browser. + +## Key concepts + +## Troubleshooting + +## Next steps +#### Allow telemetry + +Microsoft would like to collect data about how users use this Spring boot starter. Microsoft uses this information to improve our tooling experience. Participation is voluntary. If you don't want to participate, just simply disable it by setting below configuration in `application.properties`. + +``` +azure.activedirectory.b2c.allow-telemetry=false +``` + +When telemetry is enabled, an HTTP request will be sent to URL `https://dc.services.visualstudio.com/v2/track`. So please make sure it's not blocked by your firewall. + +Find more information about Azure Service Privacy Statement, please check [Microsoft Online Services Privacy Statement](https://www.microsoft.com/en-us/privacystatement/OnlineServices/Default.aspx). + +## Contributing + +## Summary + +In this documentation, you created a new Java web application using the Azure Active Directory B2C starter, +configured a new Azure AD B2C tenant and registered a new application in it, and then configured your +application to use the Spring annotations and classes to protect the web app. + diff --git a/sdk/spring/azure-spring-boot-starter-active-directory-b2c/pom.xml b/sdk/spring/azure-spring-boot-starter-active-directory-b2c/pom.xml new file mode 100644 index 000000000000..3b5520ed32d4 --- /dev/null +++ b/sdk/spring/azure-spring-boot-starter-active-directory-b2c/pom.xml @@ -0,0 +1,189 @@ + + + 4.0.0 + + + com.azure + azure-client-sdk-parent + 1.7.0 + ../../parents/azure-client-sdk-parent + + + com.microsoft.azure + azure-active-directory-b2c-spring-boot-starter + 2.2.5-beta.1 + + Azure AD B2C Spring Security Integration Spring Boot Starter + Spring Boot Starter for Azure AD B2C and Spring Security Integration + + + + org.springframework.boot + spring-boot-starter + 2.2.0.RELEASE + + + + org.springframework.boot + spring-boot-starter-validation + 2.2.0.RELEASE + + + + com.microsoft.azure + azure-spring-boot + 2.2.5-beta.1 + + + + + org.springframework + spring-web + 5.2.5.RELEASE + + + + javax.validation + validation-api + 2.0.1.Final + + + + + org.springframework.security + spring-security-core + 5.2.0.RELEASE + + + + org.springframework.security + spring-security-web + 5.2.0.RELEASE + + + + org.springframework.security + spring-security-config + 5.2.0.RELEASE + + + + org.springframework.security + spring-security-oauth2-core + 5.2.0.RELEASE + + + + org.springframework.security + spring-security-oauth2-client + 5.2.0.RELEASE + + + + org.springframework.security + spring-security-oauth2-jose + 5.2.0.RELEASE + + + + + + + org.apache.maven.plugins + maven-enforcer-plugin + 3.0.0-M3 + + + + + com.microsoft.azure:* + javax.validation:validation-api:[2.0.1.Final] + org.springframework:spring-web:[5.2.5.RELEASE] + org.springframework.boot:spring-boot-starter:[2.2.0.RELEASE] + org.springframework.boot:spring-boot-starter-validation:[2.2.0.RELEASE] + org.springframework.security:spring-security-config:[5.2.0.RELEASE] + org.springframework.security:spring-security-core:[5.2.0.RELEASE] + org.springframework.security:spring-security-oauth2-client:[5.2.0.RELEASE] + org.springframework.security:spring-security-oauth2-core:[5.2.0.RELEASE] + org.springframework.security:spring-security-oauth2-jose:[5.2.0.RELEASE] + org.springframework.security:spring-security-web:[5.2.0.RELEASE] + + + + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + 3.1.1 + + + attach-javadocs + + jar + + + true + + + + + + org.apache.maven.plugins + maven-jar-plugin + 3.1.2 + + + empty-javadoc-jar-with-readme + package + + jar + + + javadoc + ${project.basedir}/javadocTemp + + + + + + org.apache.maven.plugins + maven-antrun-plugin + 1.8 + + + copy-readme-to-javadocTemp + prepare-package + + + Deleting existing ${project.basedir}/javadocTemp + + + + Copying ${project.basedir}/README.md to + ${project.basedir}/javadocTemp/README.md + + + + + + run + + + + + + + + diff --git a/sdk/spring/azure-spring-boot-starter-active-directory-b2c/src/main/resources/aadb2c.enable.config b/sdk/spring/azure-spring-boot-starter-active-directory-b2c/src/main/resources/aadb2c.enable.config new file mode 100644 index 000000000000..2995a4d0e749 --- /dev/null +++ b/sdk/spring/azure-spring-boot-starter-active-directory-b2c/src/main/resources/aadb2c.enable.config @@ -0,0 +1 @@ +dummy \ No newline at end of file diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/btoc/AADB2CAuthorizationRequestResolver.java b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/btoc/AADB2CAuthorizationRequestResolver.java new file mode 100644 index 000000000000..c7e10953e45d --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/btoc/AADB2CAuthorizationRequestResolver.java @@ -0,0 +1,120 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.autoconfigure.btoc; + +import org.springframework.lang.NonNull; +import org.springframework.lang.Nullable; +import org.springframework.security.core.context.SecurityContextHolder; +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.security.web.util.matcher.AntPathRequestMatcher; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +import javax.servlet.http.HttpServletRequest; +import java.util.HashMap; +import java.util.Map; + +/** + * This class handles the OAuth2 request procession for AAD B2C authorization. + *

+ * Userflow name is added in the request link and forgotten password redirection to password-reset page is added on the base of default OAuth2 authorization resolve. + */ +public class AADB2CAuthorizationRequestResolver implements OAuth2AuthorizationRequestResolver { + + private static final String REQUEST_BASE_URI = + OAuth2AuthorizationRequestRedirectFilter.DEFAULT_AUTHORIZATION_REQUEST_BASE_URI; + + private static final String REGISTRATION_ID_NAME = "registrationId"; + + private static final String PARAMETER_X_CLIENT_SKU = "x-client-SKU"; + + private static final String AAD_B2C_USER_AGENT = "spring-boot-starter"; + + private static final String MATCHER_PATTERN = String.format("%s/{%s}", REQUEST_BASE_URI, REGISTRATION_ID_NAME); + + private static final AntPathRequestMatcher REQUEST_MATCHER = new AntPathRequestMatcher(MATCHER_PATTERN); + + private final OAuth2AuthorizationRequestResolver defaultResolver; + + private final String passwordResetUserFlow; + + public AADB2CAuthorizationRequestResolver(@NonNull ClientRegistrationRepository repository) { + this.passwordResetUserFlow = null; + this.defaultResolver = new DefaultOAuth2AuthorizationRequestResolver(repository, REQUEST_BASE_URI); + } + + public AADB2CAuthorizationRequestResolver(@NonNull ClientRegistrationRepository repository, + @Nullable String passwordResetUserFlow) { + this.passwordResetUserFlow = passwordResetUserFlow; + this.defaultResolver = new DefaultOAuth2AuthorizationRequestResolver(repository, REQUEST_BASE_URI); + } + + @Override + public OAuth2AuthorizationRequest resolve(@NonNull HttpServletRequest request) { + return resolve(request, getRegistrationId(request)); + } + + @Override + public OAuth2AuthorizationRequest resolve(@NonNull HttpServletRequest request, String registrationId) { + if (StringUtils.hasText(passwordResetUserFlow) && isForgotPasswordAuthorizationRequest(request)) { + final OAuth2AuthorizationRequest authRequest = defaultResolver.resolve(request, passwordResetUserFlow); + return getB2CAuthorizationRequest(authRequest, passwordResetUserFlow); + } + + if (StringUtils.hasText(registrationId) && REQUEST_MATCHER.matches(request)) { + return getB2CAuthorizationRequest(defaultResolver.resolve(request), registrationId); + } + + // Return null may not be the good practice, but we need to align with oauth2.client.web + // DefaultOAuth2AuthorizationRequestResolver. + return null; + } + + private void cleanupSecurityContextAuthentication() { + SecurityContextHolder.getContext().setAuthentication(null); + } + + private OAuth2AuthorizationRequest getB2CAuthorizationRequest(@Nullable OAuth2AuthorizationRequest request, + String userFlow) { + Assert.hasText(userFlow, "User flow should contain text."); + + if (request == null) { + return null; + } + + cleanupSecurityContextAuthentication(); + + final Map parameters = new HashMap<>(request.getAdditionalParameters()); + + parameters.put("p", userFlow); + parameters.put(PARAMETER_X_CLIENT_SKU, AAD_B2C_USER_AGENT); + + return OAuth2AuthorizationRequest.from(request).additionalParameters(parameters).build(); + } + + private String getRegistrationId(HttpServletRequest request) { + if (REQUEST_MATCHER.matches(request)) { + return REQUEST_MATCHER.extractUriTemplateVariables(request).get(REGISTRATION_ID_NAME); + } + + return null; + } + + // Handle the forgot password of sign-up-or-in page cannot redirect user to password-reset page. + // The B2C service will enhance that, and then related code will be removed. + private boolean isForgotPasswordAuthorizationRequest(@NonNull HttpServletRequest request) { + final String error = request.getParameter("error"); + final String description = request.getParameter("error_description"); + + if ("access_denied".equals(error)) { + Assert.hasText(description, "description should contain text."); + return description.startsWith("AADB2C90118:"); + } + + return false; + } +} diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/btoc/AADB2CAutoConfiguration.java b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/btoc/AADB2CAutoConfiguration.java new file mode 100644 index 000000000000..37ad3ec63e4b --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/btoc/AADB2CAutoConfiguration.java @@ -0,0 +1,149 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.autoconfigure.btoc; + +import com.microsoft.azure.telemetry.TelemetrySender; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +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.lang.NonNull; +import org.springframework.security.oauth2.client.registration.ClientRegistration; +import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; +import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.security.oauth2.core.ClientAuthenticationMethod; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.StringUtils; + +import javax.annotation.PostConstruct; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static com.microsoft.azure.spring.autoconfigure.btoc.AADB2CProperties.PREFIX; +import static com.microsoft.azure.spring.autoconfigure.btoc.AADB2CProperties.USER_FLOW_SIGN_UP_OR_SIGN_IN; +import static com.microsoft.azure.telemetry.TelemetryData.SERVICE_NAME; +import static com.microsoft.azure.telemetry.TelemetryData.TENANT_NAME; +import static com.microsoft.azure.telemetry.TelemetryData.getClassPackageSimpleName; + +/** + * {@link EnableAutoConfiguration Auto-configuration} for AAD B2C Authentication. + *

+ * The configuration will not be activated if no {@literal azure.activedirectory.b2c.tenant-id, client-id, client-secret, reply-url and sign-up-or-sign-in} property provided. + *

+ * A client registration repository service {@link InMemoryClientRegistrationRepository} will be auto-configured by specifying + * {@literal azure.activedirectory.b2c.oidc-enabled} property as true or ignore it. + */ +@Configuration +@ConditionalOnWebApplication +@ConditionalOnResource(resources = "classpath:aadb2c.enable.config") +@ConditionalOnProperty( + prefix = PREFIX, + value = { + "tenant", + "client-id", + "client-secret", + "reply-url", + USER_FLOW_SIGN_UP_OR_SIGN_IN + } +) +@EnableConfigurationProperties(AADB2CProperties.class) +public class AADB2CAutoConfiguration { + + private final ClientRegistrationRepository repository; + + private final AADB2CProperties properties; + + public AADB2CAutoConfiguration(@NonNull ClientRegistrationRepository repository, + @NonNull AADB2CProperties properties) { + this.repository = repository; + this.properties = properties; + } + + @Bean + @ConditionalOnMissingBean + public AADB2CAuthorizationRequestResolver b2cOAuth2AuthorizationRequestResolver() { + return new AADB2CAuthorizationRequestResolver(repository, properties.getUserFlows().getPasswordReset()); + } + + @Bean + @ConditionalOnMissingBean + public AADB2CLogoutSuccessHandler b2cLogoutSuccessHandler() { + return new AADB2CLogoutSuccessHandler(properties); + } + + @Bean + @ConditionalOnMissingBean + public AADB2COidcLoginConfigurer b2cLoginConfigurer(AADB2CLogoutSuccessHandler handler, + AADB2CAuthorizationRequestResolver resolver) { + return new AADB2COidcLoginConfigurer(properties, handler, resolver); + } + + @PostConstruct + private void sendTelemetry() { + if (properties.isAllowTelemetry()) { + final Map events = new HashMap<>(); + final TelemetrySender sender = new TelemetrySender(); + + events.put(SERVICE_NAME, getClassPackageSimpleName(AADB2CAutoConfiguration.class)); + events.put(TENANT_NAME, properties.getTenant()); + + sender.send(ClassUtils.getUserClass(getClass()).getSimpleName(), events); + } + } + + @Configuration + @ConditionalOnResource(resources = "classpath:aadb2c.enable.config") + @ConditionalOnProperty(prefix = PREFIX, value = "oidc-enabled", havingValue = "true", matchIfMissing = true) + public static class AADB2COidcAutoConfiguration { + + private final AADB2CProperties properties; + + public AADB2COidcAutoConfiguration(@NonNull AADB2CProperties properties) { + this.properties = properties; + } + + private void addB2CClientRegistration(@NonNull List registrations, String userFlow) { + if (StringUtils.hasText(userFlow)) { + registrations.add(b2cClientRegistration(userFlow)); + } + } + + @Bean + @ConditionalOnMissingBean + public ClientRegistrationRepository clientRegistrationRepository() { + final List registrations = new ArrayList<>(); + + addB2CClientRegistration(registrations, properties.getUserFlows().getSignUpOrSignIn()); + addB2CClientRegistration(registrations, properties.getUserFlows().getProfileEdit()); + addB2CClientRegistration(registrations, properties.getUserFlows().getPasswordReset()); + + return new InMemoryClientRegistrationRepository(registrations); + } + + private ClientRegistration b2cClientRegistration(String userFlow) { + Assert.hasText(userFlow, "User flow should contains text."); + + return ClientRegistration.withRegistrationId(userFlow) // Use flow as registration Id. + .clientId(properties.getClientId()) + .clientSecret(properties.getClientSecret()) + .clientAuthenticationMethod(ClientAuthenticationMethod.POST) + .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) + .redirectUriTemplate(properties.getReplyUrl()) + .scope(properties.getClientId(), "openid") + .authorizationUri(AADB2CURL.getAuthorizationUrl(properties.getTenant())) + .tokenUri(AADB2CURL.getTokenUrl(properties.getTenant(), userFlow)) + .jwkSetUri(AADB2CURL.getJwkSetUrl(properties.getTenant(), userFlow)) + .userNameAttributeName("name") + .clientName(userFlow) + .build(); + } + } +} diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/btoc/AADB2CConfigurationException.java b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/btoc/AADB2CConfigurationException.java new file mode 100644 index 000000000000..274d83843b25 --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/btoc/AADB2CConfigurationException.java @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.autoconfigure.btoc; + +/** + * Throw runtime exception for configuration. + */ +public class AADB2CConfigurationException extends RuntimeException { + + public AADB2CConfigurationException(String message) { + super(message); + } + + public AADB2CConfigurationException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/btoc/AADB2CLogoutSuccessHandler.java b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/btoc/AADB2CLogoutSuccessHandler.java new file mode 100644 index 000000000000..c2ded434692a --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/btoc/AADB2CLogoutSuccessHandler.java @@ -0,0 +1,39 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.autoconfigure.btoc; + +import org.springframework.lang.NonNull; +import org.springframework.security.core.Authentication; +import org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * Get the url of successful logout and handle the navigation on logout. + */ +public class AADB2CLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler { + + private final AADB2CProperties properties; + + public AADB2CLogoutSuccessHandler(@NonNull AADB2CProperties properties) { + this.properties = properties; + + super.setDefaultTargetUrl(getAADB2CEndSessionUrl()); + } + + private String getAADB2CEndSessionUrl() { + final String userFlow = properties.getUserFlows().getSignUpOrSignIn(); + final String logoutSuccessUrl = properties.getLogoutSuccessUrl(); + + return AADB2CURL.getEndSessionUrl(properties.getTenant(), logoutSuccessUrl, userFlow); + } + + @Override + public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, + Authentication authentication) throws IOException, ServletException { + super.onLogoutSuccess(request, response, authentication); + } +} diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/btoc/AADB2COidcLoginConfigurer.java b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/btoc/AADB2COidcLoginConfigurer.java new file mode 100644 index 000000000000..d07bebfba0a6 --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/btoc/AADB2COidcLoginConfigurer.java @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.autoconfigure.btoc; + +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; + +/** + * Configure B2C OAUTH2 login properties. + */ +public class AADB2COidcLoginConfigurer extends AbstractHttpConfigurer { + + private final AADB2CProperties properties; + + private final AADB2CLogoutSuccessHandler handler; + + private final AADB2CAuthorizationRequestResolver resolver; + + public AADB2COidcLoginConfigurer(AADB2CProperties properties, + AADB2CLogoutSuccessHandler handler, AADB2CAuthorizationRequestResolver resolver) { + this.properties = properties; + this.handler = handler; + this.resolver = resolver; + } + + @Override + public void init(HttpSecurity http) throws Exception { + http.logout() + .logoutSuccessHandler(handler) + .and() + .oauth2Login() + .loginProcessingUrl(properties.getLoginProcessingUrl()) + .authorizationEndpoint().authorizationRequestResolver(resolver); + } +} diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/btoc/AADB2CProperties.java b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/btoc/AADB2CProperties.java new file mode 100644 index 000000000000..ba51aa078570 --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/btoc/AADB2CProperties.java @@ -0,0 +1,202 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.autoconfigure.btoc; + +import org.hibernate.validator.constraints.URL; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.lang.NonNull; +import org.springframework.security.oauth2.client.authentication.OAuth2AuthorizationCodeAuthenticationProvider; +import org.springframework.security.oauth2.client.oidc.authentication.OidcAuthorizationCodeAuthenticationProvider; +import org.springframework.validation.annotation.Validated; + +import javax.validation.constraints.NotBlank; +import java.net.MalformedURLException; + +/** + * Configuration properties for Azure Active Directory B2C. + */ +@Validated +@ConfigurationProperties(prefix = AADB2CProperties.PREFIX) +public class AADB2CProperties { + + private static final String USER_FLOWS = "user-flows"; + + /** + * We do not use ${@link String#format(String, Object...)} + * as it's not real constant, which cannot be referenced in annotation. + */ + public static final String USER_FLOW_PASSWORD_RESET = USER_FLOWS + ".password-reset"; + + public static final String USER_FLOW_PROFILE_EDIT = USER_FLOWS + ".profile-edit"; + + public static final String USER_FLOW_SIGN_UP_OR_SIGN_IN = USER_FLOWS + ".sign-up-or-sign-in"; + + public static final String DEFAULT_LOGOUT_SUCCESS_URL = "http://localhost:8080/login"; + + public static final String PREFIX = "azure.activedirectory.b2c"; + + /** + * The name of the b2c tenant. + */ + @NotBlank(message = "tenant name should not be blank") + private String tenant; + + /** + * Use OIDC ${@link OidcAuthorizationCodeAuthenticationProvider} by default. If set to false, + * will use Oauth2 ${@link OAuth2AuthorizationCodeAuthenticationProvider}. + */ + private Boolean oidcEnabled = true; + + /** + * The application ID that registered under b2c tenant. + */ + @NotBlank(message = "client ID should not be blank") + private String clientId; + + /** + * The application secret that registered under b2c tenant. + */ + @NotBlank(message = "client secret should not be blank") + private String clientSecret; + + @URL(message = "reply URL should be valid URL") + private String replyUrl; + + @URL(message = "logout success should be valid URL") + private String logoutSuccessUrl = DEFAULT_LOGOUT_SUCCESS_URL; + + /** + * The all user flows which is created under b2c tenant. + */ + private UserFlows userFlows = new UserFlows(); + + /** + * Telemetry data will be collected if true, or disable data collection. + */ + private boolean allowTelemetry = true; + + private String getReplyURLPath(@URL String replyURL) { + try { + return new java.net.URL(replyURL).getPath(); + } catch (MalformedURLException e) { + throw new AADB2CConfigurationException("Failed to get path of given URL.", e); + } + } + + @NonNull + public String getLoginProcessingUrl() { + return getReplyURLPath(replyUrl); + } + + @Validated + protected static class UserFlows { + + protected UserFlows() { + + } + + /** + * The sign-up-or-sign-in user flow which is created under b2c tenant. + */ + @NotBlank(message = "sign-up-or-in value should not be blank") + private String signUpOrSignIn; + + /** + * The profile-edit user flow which is created under b2c tenant. + */ + private String profileEdit; + + /** + * The password-reset user flow which is created under b2c tenant. + */ + private String passwordReset; + + public String getSignUpOrSignIn() { + return signUpOrSignIn; + } + + public void setSignUpOrSignIn(String signUpOrSignIn) { + this.signUpOrSignIn = signUpOrSignIn; + } + + public String getProfileEdit() { + return profileEdit; + } + + public void setProfileEdit(String profileEdit) { + this.profileEdit = profileEdit; + } + + public String getPasswordReset() { + return passwordReset; + } + + public void setPasswordReset(String passwordReset) { + this.passwordReset = passwordReset; + } + } + + public String getTenant() { + return tenant; + } + + public void setTenant(String tenant) { + this.tenant = tenant; + } + + public Boolean getOidcEnabled() { + return oidcEnabled; + } + + public void setOidcEnabled(Boolean oidcEnabled) { + this.oidcEnabled = oidcEnabled; + } + + 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 String getReplyUrl() { + return replyUrl; + } + + public void setReplyUrl(String replyUrl) { + this.replyUrl = replyUrl; + } + + public String getLogoutSuccessUrl() { + return logoutSuccessUrl; + } + + public void setLogoutSuccessUrl(String logoutSuccessUrl) { + this.logoutSuccessUrl = logoutSuccessUrl; + } + + public UserFlows getUserFlows() { + return userFlows; + } + + public void setUserFlows(UserFlows userFlows) { + this.userFlows = userFlows; + } + + public boolean isAllowTelemetry() { + return allowTelemetry; + } + + public void setAllowTelemetry(boolean allowTelemetry) { + this.allowTelemetry = allowTelemetry; + } +} diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/btoc/AADB2CURL.java b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/btoc/AADB2CURL.java new file mode 100644 index 000000000000..84b075de97d8 --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/main/java/com/microsoft/azure/spring/autoconfigure/btoc/AADB2CURL.java @@ -0,0 +1,68 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.autoconfigure.btoc; + +import org.springframework.util.Assert; + +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; + +/** + * To get AAD B2C URLs for configuration. + */ +public final class AADB2CURL { + + private AADB2CURL() { + + } + + private static final String AUTHORIZATION_URL_PATTERN = + "https://%s.b2clogin.com/%s.onmicrosoft.com/oauth2/v2.0/authorize"; + + private static final String TOKEN_URL_PATTERN = + "https://%s.b2clogin.com/%s.onmicrosoft.com/oauth2/v2.0/token?p=%s"; + + private static final String JWKSET_URL_PATTERN = + "https://%s.b2clogin.com/%s.onmicrosoft.com/discovery/v2.0/keys?p=%s"; + + private static final String END_SESSION_URL_PATTERN = + "https://%s.b2clogin.com/%s.onmicrosoft.com/oauth2/v2.0/logout?post_logout_redirect_uri=%s&p=%s"; + + public static String getAuthorizationUrl(String tenant) { + Assert.hasText(tenant, "tenant should have text."); + + return String.format(AUTHORIZATION_URL_PATTERN, tenant, tenant); + } + + public static String getTokenUrl(String tenant, String userFlow) { + Assert.hasText(tenant, "tenant should have text."); + Assert.hasText(userFlow, "user flow should have text."); + + return String.format(TOKEN_URL_PATTERN, tenant, tenant, userFlow); + } + + public static String getJwkSetUrl(String tenant, String userFlow) { + Assert.hasText(tenant, "tenant should have text."); + Assert.hasText(userFlow, "user flow should have text."); + + return String.format(JWKSET_URL_PATTERN, tenant, tenant, userFlow); + } + + public static String getEndSessionUrl(String tenant, String logoutUrl, String userFlow) { + Assert.hasText(tenant, "tenant should have text."); + Assert.hasText(logoutUrl, "logoutUrl should have text."); + Assert.hasText(userFlow, "user flow should have text."); + + return String.format(END_SESSION_URL_PATTERN, tenant, tenant, getEncodedURL(logoutUrl), userFlow); + } + + private static String getEncodedURL(String url) { + Assert.hasText(url, "url should have text."); + + try { + return URLEncoder.encode(url, "utf-8"); + } catch (UnsupportedEncodingException e) { + throw new AADB2CConfigurationException("failed to encode url: " + url, e); + } + } +} diff --git a/sdk/spring/azure-spring-boot/src/samples/java/com/azure/spring/btoc/AADB2COidcLoginConfigSample.java b/sdk/spring/azure-spring-boot/src/samples/java/com/azure/spring/btoc/AADB2COidcLoginConfigSample.java new file mode 100644 index 000000000000..23ed8f070cfb --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/samples/java/com/azure/spring/btoc/AADB2COidcLoginConfigSample.java @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.btoc; + +import com.microsoft.azure.spring.autoconfigure.btoc.AADB2COidcLoginConfigurer; +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; + +/** + * 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 AADB2COidcLoginConfigurer in README.md + */ +@EnableWebSecurity +public class AADB2COidcLoginConfigSample extends WebSecurityConfigurerAdapter { + + private final AADB2COidcLoginConfigurer configurer; + + public AADB2COidcLoginConfigSample(AADB2COidcLoginConfigurer configurer) { + this.configurer = configurer; + } + + @Override + protected void configure(HttpSecurity http) throws Exception { + http.authorizeRequests() + .anyRequest() + .authenticated() + .and() + .apply(configurer); + } +} diff --git a/sdk/spring/azure-spring-boot/src/samples/java/com/azure/spring/btoc/AADB2CWebController.java b/sdk/spring/azure-spring-boot/src/samples/java/com/azure/spring/btoc/AADB2CWebController.java new file mode 100644 index 000000000000..c06ee37b0da8 --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/samples/java/com/azure/spring/btoc/AADB2CWebController.java @@ -0,0 +1,50 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.btoc; + +import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; +import org.springframework.security.oauth2.core.user.OAuth2User; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.GetMapping; + +/** + * 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 AADB2COidcLoginConfigurer in README.md + */ +@Controller +public class AADB2CWebController { + + private void initializeModel(Model model, OAuth2AuthenticationToken token) { + if (token != null) { + final OAuth2User user = token.getPrincipal(); + + model.addAttribute("grant_type", user.getAuthorities()); + model.addAllAttributes(user.getAttributes()); + } + } + + @GetMapping(value = "/") + public String index(Model model, OAuth2AuthenticationToken token) { + initializeModel(model, token); + + return "home"; + } + + @GetMapping(value = "/greeting") + public String greeting(Model model, OAuth2AuthenticationToken token) { + initializeModel(model, token); + + return "greeting"; + } + + @GetMapping(value = "/home") + public String home(Model model, OAuth2AuthenticationToken token) { + initializeModel(model, token); + + return "home"; + } +} diff --git a/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/btoc/AADB2CAuthorizationRequestResolverTest.java b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/btoc/AADB2CAuthorizationRequestResolverTest.java new file mode 100644 index 000000000000..367c69e719ca --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/btoc/AADB2CAuthorizationRequestResolverTest.java @@ -0,0 +1,66 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.autoconfigure.btoc; + +import org.junit.Test; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.runner.WebApplicationContextRunner; +import org.springframework.http.HttpMethod; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.security.oauth2.core.AuthorizationGrantType; +import org.springframework.util.Assert; + +import javax.servlet.http.HttpServletRequest; + +import static com.microsoft.azure.spring.autoconfigure.btoc.AADB2CConstants.*; +import static org.assertj.core.api.Java6Assertions.assertThat; + +public class AADB2CAuthorizationRequestResolverTest { + + private final WebApplicationContextRunner contextRunner = new WebApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(AADB2CAutoConfiguration.class)) + .withPropertyValues( + String.format("%s=%s", TENANT, TEST_TENANT), + String.format("%s=%s", CLIENT_ID, TEST_CLIENT_ID), + String.format("%s=%s", CLIENT_SECRET, TEST_CLIENT_SECRET), + String.format("%s=%s", REPLY_URL, TEST_REPLY_URL), + String.format("%s=%s", LOGOUT_SUCCESS_URL, TEST_LOGOUT_SUCCESS_URL), + String.format("%s=%s", SIGN_UP_OR_SIGN_IN, TEST_SIGN_UP_OR_IN_NAME) + ); + + private HttpServletRequest getHttpServletRequest(String uri) { + Assert.hasText(uri, "uri must contain text."); + + final MockHttpServletRequest request = new MockHttpServletRequest(HttpMethod.GET.toString(), uri); + + request.setServletPath(uri); + + return request; + } + + @Test + public void testAutoConfigurationBean() { + this.contextRunner.run(c -> { + String requestUri = "/fake-url"; + HttpServletRequest request = getHttpServletRequest(requestUri); + final String registrationId = TEST_SIGN_UP_OR_IN_NAME; + final AADB2CAuthorizationRequestResolver resolver = c.getBean(AADB2CAuthorizationRequestResolver.class); + + assertThat(resolver).isNotNull(); + assertThat(resolver.resolve(request)).isNull(); + assertThat(resolver.resolve(request, registrationId)).isNull(); + + requestUri = "/oauth2/authorization/" + TEST_SIGN_UP_OR_IN_NAME; + request = getHttpServletRequest(requestUri); + + assertThat(resolver.resolve(request)).isNotNull(); + assertThat(resolver.resolve(request, registrationId)).isNotNull(); + + assertThat(resolver.resolve(request).getAdditionalParameters().get("p")).isEqualTo(TEST_SIGN_UP_OR_IN_NAME); + assertThat(resolver.resolve(request).getClientId()).isEqualTo(TEST_CLIENT_ID); + assertThat(resolver.resolve(request).getRedirectUri()).isEqualTo(TEST_REPLY_URL); + assertThat(resolver.resolve(request).getGrantType()).isEqualTo(AuthorizationGrantType.AUTHORIZATION_CODE); + assertThat(resolver.resolve(request).getScopes()).contains("openid", TEST_CLIENT_ID); + }); + } +} diff --git a/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/btoc/AADB2CAutoConfigurationTest.java b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/btoc/AADB2CAutoConfigurationTest.java new file mode 100644 index 000000000000..60114aad61bf --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/btoc/AADB2CAutoConfigurationTest.java @@ -0,0 +1,78 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.autoconfigure.btoc; + +import org.junit.Test; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.runner.WebApplicationContextRunner; +import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; + +import static com.microsoft.azure.spring.autoconfigure.btoc.AADB2CConstants.*; +import static org.assertj.core.api.Java6Assertions.assertThat; + +public class AADB2CAutoConfigurationTest { + + private final WebApplicationContextRunner contextRunner = new WebApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(AADB2CAutoConfiguration.class)) + .withPropertyValues( + String.format("%s=%s", TENANT, TEST_TENANT), + String.format("%s=%s", CLIENT_ID, TEST_CLIENT_ID), + String.format("%s=%s", CLIENT_SECRET, TEST_CLIENT_SECRET), + String.format("%s=%s", REPLY_URL, TEST_REPLY_URL), + String.format("%s=%s", LOGOUT_SUCCESS_URL, TEST_LOGOUT_SUCCESS_URL), + String.format("%s=%s", SIGN_UP_OR_SIGN_IN, TEST_SIGN_UP_OR_IN_NAME) + ); + + @Test + public void testAutoConfigurationBean() { + this.contextRunner.run(c -> { + final AADB2CAutoConfiguration config = c.getBean(AADB2CAutoConfiguration.class); + + assertThat(config).isNotNull(); + }); + } + + @Test + public void testPropertiesBean() { + this.contextRunner.run(c -> { + final AADB2CProperties properties = c.getBean(AADB2CProperties.class); + + assertThat(properties).isNotNull(); + assertThat(properties.getTenant()).isEqualTo(TEST_TENANT); + assertThat(properties.getClientId()).isEqualTo(TEST_CLIENT_ID); + assertThat(properties.getClientSecret()).isEqualTo(TEST_CLIENT_SECRET); + assertThat(properties.getReplyUrl()).isEqualTo(TEST_REPLY_URL); + + final String signUpOrSignIn = properties.getUserFlows().getSignUpOrSignIn(); + + assertThat(signUpOrSignIn).isEqualTo(TEST_SIGN_UP_OR_IN_NAME); + }); + } + + @Test + public void testAADB2CAuthorizationRequestResolverBean() { + this.contextRunner.run(c -> { + final AADB2CAuthorizationRequestResolver resolver = c.getBean(AADB2CAuthorizationRequestResolver.class); + + assertThat(resolver).isNotNull(); + }); + } + + @Test + public void testLogoutSuccessHandlerBean() { + this.contextRunner.run(c -> { + final AADB2CLogoutSuccessHandler handler = c.getBean(AADB2CLogoutSuccessHandler.class); + + assertThat(handler).isNotNull(); + }); + } + + @Test + public void testFilterBean() { + this.contextRunner.run(c -> { + final ClientRegistrationRepository repository = c.getBean(ClientRegistrationRepository.class); + + assertThat(repository).isNotNull(); + }); + } +} diff --git a/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/btoc/AADB2CConstants.java b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/btoc/AADB2CConstants.java new file mode 100644 index 000000000000..c68f2449aaa0 --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/btoc/AADB2CConstants.java @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.autoconfigure.btoc; + + +import static com.microsoft.azure.spring.autoconfigure.btoc.AADB2CProperties.PREFIX; +import static com.microsoft.azure.spring.autoconfigure.btoc.AADB2CProperties.USER_FLOW_SIGN_UP_OR_SIGN_IN; + +public class AADB2CConstants { + + public static final String TEST_TENANT = "fake-tenant"; + + public static final String TEST_CLIENT_ID = "fake-client-id"; + + public static final String TEST_CLIENT_SECRET = "fake-client-secret"; + + public static final String TEST_REPLY_URL = "http://localhost:8080/index"; + + public static final String TEST_SIGN_UP_OR_IN_NAME = "fake-sign-in-or-up"; + + public static final String TEST_LOGOUT_SUCCESS_URL = "https://fake-logout-success-url"; + + public static final String TENANT = String.format("%s.%s", PREFIX, "tenant"); + + public static final String CLIENT_ID = String.format("%s.%s", PREFIX, "client-id"); + + public static final String CLIENT_SECRET = String.format("%s.%s", PREFIX, "client-secret"); + + public static final String REPLY_URL = String.format("%s.%s", PREFIX, "reply-url"); + + public static final String LOGOUT_SUCCESS_URL = String.format("%s.%s", PREFIX, "logout-success-url"); + + public static final String SIGN_UP_OR_SIGN_IN = String.format("%s.%s", PREFIX, USER_FLOW_SIGN_UP_OR_SIGN_IN); +} diff --git a/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/btoc/AADB2CLogoutSuccessHandlerTest.java b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/btoc/AADB2CLogoutSuccessHandlerTest.java new file mode 100644 index 000000000000..0262f0ced51e --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/btoc/AADB2CLogoutSuccessHandlerTest.java @@ -0,0 +1,49 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.autoconfigure.btoc; + +import org.junit.Before; +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class AADB2CLogoutSuccessHandlerTest { + + private static final String TEST_TENANT = "test-tenant"; + + private static final String TEST_LOGOUT_SUCCESS_URL = "http://localhost:8080/login"; + + private static final String TEST_USER_FLOW_SIGN_UP_OR_IN = "my-sign-up-or-in"; + + private AADB2CProperties properties; + + @Before + public void setUp() { + properties = new AADB2CProperties(); + + properties.setTenant(TEST_TENANT); + properties.setLogoutSuccessUrl(TEST_LOGOUT_SUCCESS_URL); + properties.getUserFlows().setSignUpOrSignIn(TEST_USER_FLOW_SIGN_UP_OR_IN); + } + + @Test + public void testDefaultTargetUrl() { + final MyLogoutSuccessHandler handler = new MyLogoutSuccessHandler(properties); + final String tenant = properties.getTenant(); + final String url = properties.getLogoutSuccessUrl(); + final String userFlow = properties.getUserFlows().getSignUpOrSignIn(); + + assertThat(handler.getTargetUrl()).isEqualTo(AADB2CURL.getEndSessionUrl(tenant, url, userFlow)); + } + + private static class MyLogoutSuccessHandler extends AADB2CLogoutSuccessHandler { + + MyLogoutSuccessHandler(AADB2CProperties properties) { + super(properties); + } + + public String getTargetUrl() { + return super.getDefaultTargetUrl(); + } + } +} diff --git a/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/btoc/AADB2CURLTest.java b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/btoc/AADB2CURLTest.java new file mode 100644 index 000000000000..7d55ffa859c3 --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/test/java/com/microsoft/azure/spring/autoconfigure/btoc/AADB2CURLTest.java @@ -0,0 +1,72 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.microsoft.azure.spring.autoconfigure.btoc; + +import org.junit.Test; + +import static org.assertj.core.api.Java6Assertions.assertThat; + +public class AADB2CURLTest { + + /** + * Reference pattern see AUTHORIZATION_URL_PATTERN of ${@link AADB2CURL}. + */ + @Test + public void testGetAuthorizationUrl() { + final String expect = "https://fake-tenant.b2clogin.com/fake-tenant.onmicrosoft.com/oauth2/v2.0/authorize"; + + assertThat(AADB2CURL.getAuthorizationUrl("fake-tenant")).isEqualTo(expect); + } + + @Test(expected = IllegalArgumentException.class) + public void testGetAuthorizationUrlException() { + AADB2CURL.getAuthorizationUrl(""); + } + + /** + * Reference pattern see TOKEN_URL_PATTERN of ${@link AADB2CURL}. + */ + @Test + public void testGetTokenUrl() { + final String expect = "https://fake-tenant.b2clogin.com/fake-tenant.onmicrosoft.com/oauth2/v2.0/token?p=fake-p"; + + assertThat(AADB2CURL.getTokenUrl("fake-tenant", "fake-p")).isEqualTo(expect); + } + + @Test(expected = IllegalArgumentException.class) + public void testGetTokenUrlException() { + AADB2CURL.getTokenUrl("", ""); + } + + /** + * Reference pattern see JWKSET_URL_PATTERN of ${@link AADB2CURL}. + */ + @Test + public void testGetJwkSetUrl() { + final String expect = "https://new-tenant.b2clogin.com/new-tenant.onmicrosoft.com/discovery/v2.0/keys?p=new-p"; + + assertThat(AADB2CURL.getJwkSetUrl("new-tenant", "new-p")).isEqualTo(expect); + } + + @Test(expected = IllegalArgumentException.class) + public void testGetJwkSetUrlException() { + AADB2CURL.getJwkSetUrl("", ""); + } + + /** + * Reference pattern see END_SESSION_URL_PATTERN of ${@link AADB2CURL}. + */ + @Test + public void testGetEndSessionUrl() { + final String expect = "https://my-tenant.b2clogin.com/my-tenant.onmicrosoft.com/oauth2/v2.0/logout?" + + "post_logout_redirect_uri=http%3A%2F%2Flocalhost%3A8080%2Fhome&p=my-p"; + + assertThat(AADB2CURL.getEndSessionUrl("my-tenant", "http://localhost:8080/home", + "my-p")).isEqualTo(expect); + } + + @Test(expected = IllegalArgumentException.class) + public void testGetEndSessionUrlException() { + AADB2CURL.getJwkSetUrl("", ""); + } +} diff --git a/sdk/spring/ci.yml b/sdk/spring/ci.yml index 084ee09aed6e..623a16f5da47 100644 --- a/sdk/spring/ci.yml +++ b/sdk/spring/ci.yml @@ -47,6 +47,9 @@ stages: - name: azure-active-directory-spring-boot-starter groupId: com.microsoft.azure safeName: azurespringbootstarteractivedirectory + - name: azure-active-directory-b2c-spring-boot-starter + groupId: com.microsoft.azure + safeName: azurespringbootstarteractivedirectoryb2c - name: azure-keyvault-secrets-spring-boot-starter groupId: com.microsoft.azure safeName: azurespringbootstarterkeyvaultsecrets diff --git a/sdk/spring/pom.xml b/sdk/spring/pom.xml index 2c21d69c6f93..8e1ee88761f3 100644 --- a/sdk/spring/pom.xml +++ b/sdk/spring/pom.xml @@ -12,6 +12,7 @@ azure-spring-boot azure-spring-boot-starter azure-spring-boot-starter-active-directory + azure-spring-boot-starter-active-directory-b2c azure-spring-boot-starter-keyvault-secrets azure-spring-boot-starter-metrics azure-spring-boot-starter-servicebus-jms