Skip to content

Commit

Permalink
aad b2c optimization (Azure#18489)
Browse files Browse the repository at this point in the history
- b2c optimization
- update readme file
- code refactor
  • Loading branch information
backwind1233 authored Jan 12, 2021
1 parent 81ba0b9 commit c92f807
Show file tree
Hide file tree
Showing 9 changed files with 137 additions and 59 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ Follow the guide of [AAD B2C tenant creation](https://docs.microsoft.com/azure/a
### Register your Azure Active Directory B2C application

Follow the guide of [AAD B2C application registry](https://docs.microsoft.com/azure/active-directory-b2c/tutorial-register-applications).
Please make sure that your b2c application `reply URL` contains `http://localhost:8080/home`.
Please make sure that your b2c application `reply URL` contains `http://localhost:8080/login/oauth2/code`.

### Create user flows

Expand All @@ -26,7 +26,7 @@ Follow the guide of [AAD B2C user flows creation](https://docs.microsoft.com/azu

#### Application.yml

1. Fill in `${your-tenant-name}` from Azure AD B2C portal `Overviews` domain name (format may looks like
1. Fill in `${your-tenant-name}` from **Azure AD B2C** portal `Overviews` domain name (format may looks like
`${your-tenant-name}.onmicrosoft.com`).
2. Select one registered instance under `Applications` from portal, and then:
1. Fill in `${your-client-id}` from `Application ID`.
Expand All @@ -35,14 +35,14 @@ Follow the guide of [AAD B2C user flows creation](https://docs.microsoft.com/azu
1. Fill in the `${your-sign-up-or-in-user-flow}` with the name of `sign-in-or-up` user flow.
2. Fill in the `${your-profile-edit-user-flow}` with the name of `profile-edit` user flow.
3. Fill in the `${your-password-reset-user-flow}` with the name of `password-reset` user flow.
4. Replace `${your-reply-url}` to `http://localhost:8080/home`.
4. Replace `${your-reply-url}` to `http://localhost:8080/login/oauth2/code`.
5. Replace `${your-logout-success-url}` to `http://localhost:8080/login`.

```yaml
azure:
activedirectory:
b2c:
tenant: ${your-tenant-name}
tenant: ${your-tenant-name} # ❗not tenant id
client-id: ${your-client-id}
client-secret: ${your-client-secret}
reply-url: ${your-reply-url} # should be absolute url.
Expand All @@ -64,7 +64,7 @@ mvn spring-boot:run
```
### Validation
1. Access `http://localhost:8080/` as index page.
2. Sign up/in.
3. Access greeting button.
Expand All @@ -76,7 +76,22 @@ mvn spring-boot:run
9. Sign in.
## Troubleshooting
- `Missing attribute 'name' in attributes `
```
java.lang.IllegalArgumentException: Missing attribute 'name' in attributes
at org.springframework.security.oauth2.core.user.DefaultOAuth2User.<init>(DefaultOAuth2User.java:67) ~[spring-security-oauth2-core-5.3.6.RELEASE.jar:5.3.6.RELEASE]
at org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser.<init>(DefaultOidcUser.java:89) ~[spring-security-oauth2-core-5.3.6.RELEASE.jar:5.3.6.RELEASE]
at org.springframework.security.oauth2.client.oidc.userinfo.OidcUserService.loadUser(OidcUserService.java:144) ~[spring-security-oauth2-client-5.3.6.RELEASE.jar:5.3.6.RELEASE]
at org.springframework.security.oauth2.client.oidc.userinfo.OidcUserService.loadUser(OidcUserService.java:63) ~[spring-security-oauth2-client-5.3.6.RELEASE.jar:5.3.6.RELEASE]
```
While running sample, if error occurs with logs above:
- make sure that while creating user workflow by following this [guide](https://docs.microsoft.com/azure/active-directory-b2c/tutorial-create-user-flows), for **User attributes and claims** , attributes and claims for **Display Name** should be chosen.
### FAQ
#### Sign in with loops to B2C endpoint ?
This issue almost due to polluted cookies of `localhost`. Clean up cookies of `localhost` and try it again.
Expand All @@ -87,4 +102,5 @@ And also available for Amazon, Azure AD, FaceBook, Github, Linkedin and Twitter.
## Next steps
## Contributing
<!-- LINKS -->
[ready-to-run-checklist]: https://github.com/Azure/azure-sdk-for-java/blob/master/sdk/spring/azure-spring-boot-samples/README.md#ready-to-run-checklist
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@ public WebSecurityConfiguration(AADB2COidcLoginConfigurer configurer) {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest()
.authenticated()
.and()
.antMatchers("/login").permitAll()
.anyRequest().authenticated()
.and()
.apply(configurer);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import com.azure.spring.autoconfigure.b2c.AADB2COidcLoginConfigurer;
import com.azure.test.aad.b2c.utils.AADB2CTestUtils;
import java.util.Collections;
import org.junit.After;
import org.junit.Assert;
import org.junit.Test;
import org.springframework.boot.autoconfigure.SpringBootApplication;
Expand All @@ -23,13 +24,14 @@ public class AADB2CIT {

private final String JOB_TITLE_A_WORKER = "a worker";
private final String JOB_TITLE_WORKER = "worker";
private AADB2CSeleniumITHelper aadB2CSeleniumITHelper;

@Test
public void testSignIn() throws InterruptedException {
AADB2CSeleniumITHelper AADB2CSeleniumITHelper = new AADB2CSeleniumITHelper(DumbApp.class,
Collections.emptyMap());
String name = AADB2CSeleniumITHelper.getName();
String userFlowName = AADB2CSeleniumITHelper.getUserFlowName();
aadB2CSeleniumITHelper = new AADB2CSeleniumITHelper(DumbApp.class, Collections.emptyMap());
aadB2CSeleniumITHelper.signIn(AADB2CTestUtils.AAD_B2C_SIGN_UP_OR_SIGN_IN);
String name = aadB2CSeleniumITHelper.getName();
String userFlowName = aadB2CSeleniumITHelper.getUserFlowName();

Assert.assertNotNull(name);
Assert.assertNotNull(userFlowName);
Expand All @@ -38,14 +40,14 @@ public void testSignIn() throws InterruptedException {

@Test
public void testProfileEdit() throws InterruptedException {
AADB2CSeleniumITHelper AADB2CSeleniumITHelper = new AADB2CSeleniumITHelper(DumbApp.class,
Collections.emptyMap());
String currentJobTitle = AADB2CSeleniumITHelper.getJobTitle();
aadB2CSeleniumITHelper = new AADB2CSeleniumITHelper(DumbApp.class, Collections.emptyMap());
aadB2CSeleniumITHelper.signIn(AADB2CTestUtils.AAD_B2C_SIGN_UP_OR_SIGN_IN);
String currentJobTitle = aadB2CSeleniumITHelper.getJobTitle();
String newJobTitle = JOB_TITLE_A_WORKER.equals(currentJobTitle) ? JOB_TITLE_WORKER : JOB_TITLE_A_WORKER;
AADB2CSeleniumITHelper.profileEditJobTitle(newJobTitle);
String name = AADB2CSeleniumITHelper.getName();
String jobTitle = AADB2CSeleniumITHelper.getJobTitle();
String userFlowName = AADB2CSeleniumITHelper.getUserFlowName();
aadB2CSeleniumITHelper.profileEditJobTitle(newJobTitle);
String name = aadB2CSeleniumITHelper.getName();
String jobTitle = aadB2CSeleniumITHelper.getJobTitle();
String userFlowName = aadB2CSeleniumITHelper.getUserFlowName();

Assert.assertNotNull(name);
Assert.assertNotNull(jobTitle);
Expand All @@ -55,12 +57,17 @@ public void testProfileEdit() throws InterruptedException {

@Test
public void testLogOut() throws InterruptedException {
AADB2CSeleniumITHelper AADB2CSeleniumITHelper = new AADB2CSeleniumITHelper(DumbApp.class,
Collections.emptyMap());
String signInButtonText = AADB2CSeleniumITHelper.logoutAndGetSignInButtonText();
aadB2CSeleniumITHelper = new AADB2CSeleniumITHelper(DumbApp.class, Collections.emptyMap());
aadB2CSeleniumITHelper.signIn(AADB2CTestUtils.AAD_B2C_SIGN_UP_OR_SIGN_IN);
String signInButtonText = aadB2CSeleniumITHelper.logoutAndGetSignInButtonText();
Assert.assertEquals("Sign in", signInButtonText);
}

@After
public void quitDriver() {
aadB2CSeleniumITHelper.quitDriver();
}

@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
@SpringBootApplication
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,16 @@
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.chrome.ChromeDriverService;
import org.openqa.selenium.chrome.ChromeOptions;
import org.openqa.selenium.support.ui.ExpectedConditions;
import org.openqa.selenium.support.ui.WebDriverWait;

public class AADB2CSeleniumITHelper {

private final String userEmail;
private final String userPassword;
private final AppRunner app;
private final WebDriver driver;
private WebDriver driver;
private WebDriverWait wait;
private static final Map<String, String> DEFAULT_PROPERTIES = new HashMap<>();

static {
Expand Down Expand Up @@ -70,62 +73,70 @@ public AADB2CSeleniumITHelper(Class<?> appClass, Map<String, String> properties)
app = new AppRunner(appClass);
DEFAULT_PROPERTIES.forEach(app::property);
properties.forEach(app::property);
this.app.start();
setDriver();
}

ChromeOptions options = new ChromeOptions();
options.addArguments("--headless");
options.addArguments("--incognito", "--no-sandbox", "--disable-dev-shm-usage");
this.driver = new ChromeDriver(options);
public void quitDriver() {
try {
this.driver.quit();
} catch (Exception e) {
this.driver = null;
}
}

this.app.start();
Thread.sleep(5000);
signIn(AADB2CTestUtils.AAD_B2C_SIGN_UP_OR_SIGN_IN);
private void setDriver() {
if (driver == null) {
ChromeOptions options = new ChromeOptions();
options.addArguments("--headless");
options.addArguments("--incognito", "--no-sandbox", "--disable-dev-shm-usage");
this.driver = new ChromeDriver(options);
wait = new WebDriverWait(driver, 5);
}
}

public void signIn(String userFlowName) throws InterruptedException {
driver.get(app.root());
Thread.sleep(5000);
driver.findElement(By.cssSelector("a[href='/oauth2/authorization/" + userFlowName + "']")).click();
Thread.sleep(5000);
wait.until(ExpectedConditions.elementToBeClickable(By.cssSelector("button[type='submit']")));
driver.findElement(By.id("email")).sendKeys(userEmail);
driver.findElement(By.id("password")).sendKeys(userPassword);
driver.findElement(By.cssSelector("button[type='submit']")).click();
Thread.sleep(7000);
manualRedirection();
}

public void profileEditJobTitle(String newJobTitle) throws InterruptedException {
Thread.sleep(5000);
driver.findElement(By.id("profileEdit")).click();
Thread.sleep(5000);
changeJobTile(newJobTitle);
driver.findElement(By.cssSelector("button[type='submit']")).click();
Thread.sleep(5000);
manualRedirection();
}

public String logoutAndGetSignInButtonText() throws InterruptedException {
Thread.sleep(5000);
wait.until(ExpectedConditions.elementToBeClickable(By.id("logout")));
driver.findElement(By.id("logout")).click();
Thread.sleep(5000);
driver.findElement(By.cssSelector("button[type='submit']")).click();
Thread.sleep(5000);

wait.until(ExpectedConditions.elementToBeClickable(By.cssSelector("button[type='submit']")));
driver.findElement(By.cssSelector("button[type='submit']")).submit();
manualRedirection();
wait.until(ExpectedConditions.elementToBeClickable(By.cssSelector(
"a[href='/oauth2/authorization/" + AADB2CTestUtils.AAD_B2C_SIGN_UP_OR_SIGN_IN + "']")));
driver.findElement(
By.cssSelector(
"a[href='/oauth2/authorization/" + AADB2CTestUtils.AAD_B2C_SIGN_UP_OR_SIGN_IN + "']")).click();
Thread.sleep(5000);
wait.until(ExpectedConditions.elementToBeClickable(By.id("next")));
return driver.findElement(By.cssSelector("button[type='submit']")).getText();
}

private void manualRedirection() throws InterruptedException {
wait.until(ExpectedConditions.urlMatches("^http://localhost"));
String currentUrl = driver.getCurrentUrl();
String newCurrentUrl = currentUrl.replaceFirst("http://localhost:8080/", app.root());
driver.get(newCurrentUrl);
Thread.sleep(5000);
}

public void changeJobTile(String newValue) {
String elementId = "jobTitle";
wait.until(ExpectedConditions.elementToBeClickable(By.id(elementId)));
driver.findElement(By.id(elementId)).clear();
driver.findElement(By.id(elementId)).sendKeys(newValue);
}
Expand All @@ -138,6 +149,7 @@ public String getJobTitle() {
}

public String getName() {
wait.until(ExpectedConditions.elementToBeClickable(By.cssSelector("tbody")));
return driver.findElement(By.cssSelector("tbody"))
.findElement(By.xpath("tr[2]"))
.findElement(By.xpath("th[2]"))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ public abstract class AADWebSecurityConfigurerAdapter extends WebSecurityConfigu
protected void configure(HttpSecurity http) throws Exception {
// @formatter:off
http.authorizeRequests()
.antMatchers("/login").permitAll()
.anyRequest().authenticated()
.and()
.oauth2Login()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

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;
Expand Down Expand Up @@ -74,10 +73,6 @@ public OAuth2AuthorizationRequest resolve(@NonNull HttpServletRequest request, S
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.");
Expand All @@ -86,8 +81,6 @@ private OAuth2AuthorizationRequest getB2CAuthorizationRequest(@Nullable OAuth2Au
return null;
}

cleanupSecurityContextAuthentication();

final Map<String, Object> additionalParameters = new HashMap<>();
Optional.ofNullable(this.properties)
.map(AADB2CProperties::getAuthenticateAdditionalParameters)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -124,13 +124,15 @@ private void addB2CClientRegistration(@NonNull List<ClientRegistration> registra
@Bean
@ConditionalOnMissingBean
public ClientRegistrationRepository clientRegistrationRepository() {
final List<ClientRegistration> registrations = new ArrayList<>();
final List<ClientRegistration> signUpOrSignInRegistrations = new ArrayList<>(1);
final List<ClientRegistration> otherRegistrations = new ArrayList<>();

addB2CClientRegistration(registrations, properties.getUserFlows().getSignUpOrSignIn());
addB2CClientRegistration(registrations, properties.getUserFlows().getProfileEdit());
addB2CClientRegistration(registrations, properties.getUserFlows().getPasswordReset());

return new InMemoryClientRegistrationRepository(registrations);
addB2CClientRegistration(signUpOrSignInRegistrations, properties.getUserFlows().getSignUpOrSignIn());
addB2CClientRegistration(otherRegistrations, properties.getUserFlows().getProfileEdit());
addB2CClientRegistration(otherRegistrations, properties.getUserFlows().getPasswordReset());

return new AADB2CClientRegistrationRepository(signUpOrSignInRegistrations, otherRegistrations);
}

private ClientRegistration b2cClientRegistration(String userFlow) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
package com.azure.spring.autoconfigure.b2c;

import org.jetbrains.annotations.NotNull;
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 java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
* <p>
* ClientRegistrationRepository for aad b2c
* </p>
*/
public class AADB2CClientRegistrationRepository implements ClientRegistrationRepository, Iterable<ClientRegistration> {

private final InMemoryClientRegistrationRepository clientRegistrations;
private final List<ClientRegistration> signUpOrSignInRegistrations;


AADB2CClientRegistrationRepository(List<ClientRegistration> signUpOrSignInRegistrations,
List<ClientRegistration> otherRegistrations) {
this.signUpOrSignInRegistrations = signUpOrSignInRegistrations;
List<ClientRegistration> allRegistrations = Stream.of(signUpOrSignInRegistrations, otherRegistrations)
.flatMap(Collection::stream)
.collect(Collectors.toList());
this.clientRegistrations = new InMemoryClientRegistrationRepository(allRegistrations);
}

@Override
public ClientRegistration findByRegistrationId(String registrationId) {
return this.clientRegistrations.findByRegistrationId(registrationId);
}

@NotNull
@Override
public Iterator<ClientRegistration> iterator() {
return this.signUpOrSignInRegistrations.iterator();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,11 @@ public AADB2COidcLoginConfigurer(AADB2CProperties properties,
@Override
public void init(HttpSecurity http) throws Exception {
http.logout()
.logoutSuccessHandler(handler)
.and()
.logoutSuccessHandler(handler)
.and()
.oauth2Login()
.loginProcessingUrl(properties.getLoginProcessingUrl())
.authorizationEndpoint().authorizationRequestResolver(resolver);
.loginProcessingUrl(properties.getLoginProcessingUrl())
.authorizationEndpoint()
.authorizationRequestResolver(resolver);
}
}

0 comments on commit c92f807

Please sign in to comment.