From edf54605d3bcaa7c1d83cc49e1e94043d5f7c6c3 Mon Sep 17 00:00:00 2001 From: Rujun Chen Date: Thu, 26 Nov 2020 15:24:22 +0800 Subject: [PATCH] In Oauth2UserService, append authorities instead of override authorities (#17838) * In XxxOAuth2UserService, append authorities instead of override authorities. --- ...AzureActiveDirectoryOAuth2UserService.java | 122 ++++-------------- .../aad/implementation/GraphClient.java | 97 ++++++++++++++ .../aad/AADOAuth2UserService.java | 23 +++- .../autoconfigure/aad/AzureADGraphClient.java | 9 -- .../aad/UserPrincipalAzureADGraphTest.java | 35 +---- .../aad/UserPrincipalMicrosoftGraphTest.java | 38 +----- 6 files changed, 147 insertions(+), 177 deletions(-) create mode 100644 sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/implementation/GraphClient.java diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/implementation/AzureActiveDirectoryOAuth2UserService.java b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/implementation/AzureActiveDirectoryOAuth2UserService.java index ba689c29d070..826400c46695 100644 --- a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/implementation/AzureActiveDirectoryOAuth2UserService.java +++ b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/implementation/AzureActiveDirectoryOAuth2UserService.java @@ -5,16 +5,6 @@ import com.azure.spring.autoconfigure.aad.AADAuthenticationProperties; import com.azure.spring.autoconfigure.aad.AADTokenClaim; -import com.azure.spring.autoconfigure.aad.JacksonObjectMapperFactory; -import com.azure.spring.autoconfigure.aad.Membership; -import com.azure.spring.autoconfigure.aad.Memberships; -import com.fasterxml.jackson.databind.ObjectMapper; -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 org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest; @@ -26,19 +16,13 @@ import org.springframework.security.oauth2.core.OAuth2AuthenticationException; import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser; import org.springframework.security.oauth2.core.oidc.user.OidcUser; +import org.springframework.util.StringUtils; -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStreamReader; -import java.net.HttpURLConnection; -import java.net.URL; -import java.nio.charset.StandardCharsets; -import java.util.LinkedHashSet; +import java.util.Collections; import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; -import static com.azure.spring.autoconfigure.aad.Constants.DEFAULT_AUTHORITY_SET; import static com.azure.spring.autoconfigure.aad.Constants.ROLE_PREFIX; /** @@ -46,111 +30,49 @@ * GrantedAuthority}. */ public class AzureActiveDirectoryOAuth2UserService implements OAuth2UserService { - private static final Logger LOGGER = LoggerFactory.getLogger(AzureActiveDirectoryOAuth2UserService.class); private final OidcUserService oidcUserService; private final AADAuthenticationProperties properties; + private final GraphClient graphClient; public AzureActiveDirectoryOAuth2UserService( AADAuthenticationProperties properties ) { this.properties = properties; this.oidcUserService = new OidcUserService(); + this.graphClient = new GraphClient(properties); } @Override public OidcUser loadUser(OidcUserRequest userRequest) throws OAuth2AuthenticationException { // Delegate to the default implementation for loading a user OidcUser oidcUser = oidcUserService.loadUser(userRequest); - Set authorities = - Optional.of(userRequest) - .map(OAuth2UserRequest::getAccessToken) - .map(AbstractOAuth2Token::getTokenValue) - .map(this::getGroups) - .map(this::toGrantedAuthoritySet) - .filter(g -> !g.isEmpty()) - .orElse(DEFAULT_AUTHORITY_SET); + Set groups = Optional.of(userRequest) + .map(OAuth2UserRequest::getAccessToken) + .map(AbstractOAuth2Token::getTokenValue) + .map(graphClient::getGroupsFromGraph) + .orElseGet(Collections::emptySet); + Set groupRoles = groups.stream() + .filter(properties::isAllowedGroup) + .map(group -> ROLE_PREFIX + group) + .collect(Collectors.toSet()); + Set allRoles = oidcUser.getAuthorities() + .stream() + .map(GrantedAuthority::getAuthority) + .collect(Collectors.toSet()); + allRoles.addAll(groupRoles); + Set authorities = allRoles.stream() + .map(SimpleGrantedAuthority::new) + .collect(Collectors.toSet()); String nameAttributeKey = Optional.of(userRequest) .map(OAuth2UserRequest::getClientRegistration) .map(ClientRegistration::getProviderDetails) .map(ClientRegistration.ProviderDetails::getUserInfoEndpoint) .map(ClientRegistration.ProviderDetails.UserInfoEndpoint::getUserNameAttributeName) - .filter(s -> !s.isEmpty()) + .filter(StringUtils::hasText) .orElse(AADTokenClaim.NAME); // Create a copy of oidcUser but use the mappedAuthorities instead return new DefaultOidcUser(authorities, oidcUser.getIdToken(), nameAttributeKey); } - - public Set toGrantedAuthoritySet(final Set groups) { - Set grantedAuthoritySet = - groups.stream() - .filter(properties::isAllowedGroup) - .map(group -> new SimpleGrantedAuthority(ROLE_PREFIX + group)) - .collect(Collectors.toSet()); - return Optional.of(grantedAuthoritySet) - .filter(g -> !g.isEmpty()) - .orElse(DEFAULT_AUTHORITY_SET); - } - - public Set getGroups(String accessToken) { - final Set groups = new LinkedHashSet<>(); - final ObjectMapper objectMapper = JacksonObjectMapperFactory.getInstance(); - String aadMembershipRestUri = properties.getGraphMembershipUri(); - while (aadMembershipRestUri != null) { - Memberships memberships; - try { - String membershipsJson = getUserMemberships(accessToken, aadMembershipRestUri); - memberships = objectMapper.readValue(membershipsJson, Memberships.class); - } catch (IOException ioException) { - LOGGER.error("Can not get group information from graph server.", ioException); - break; - } - memberships.getValue() - .stream() - .filter(this::isGroupObject) - .map(Membership::getDisplayName) - .forEach(groups::add); - aadMembershipRestUri = Optional.of(memberships) - .map(Memberships::getOdataNextLink) - .orElse(null); - } - return groups; - } - - private String getUserMemberships(String accessToken, String urlString) throws IOException { - URL url = new URL(urlString); - final HttpURLConnection connection = (HttpURLConnection) url.openConnection(); - connection.setRequestMethod(HttpMethod.GET.toString()); - connection.setRequestProperty(HttpHeaders.AUTHORIZATION, String.format("Bearer %s", accessToken)); - connection.setRequestProperty(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE); - connection.setRequestProperty(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE); - final String responseInJson = getResponseString(connection); - final int responseCode = connection.getResponseCode(); - if (responseCode == HTTPResponse.SC_OK) { - return responseInJson; - } else { - throw new IllegalStateException( - "Response is not " + HTTPResponse.SC_OK + ", response json: " + responseInJson); - } - } - - private String getResponseString(HttpURLConnection connection) throws IOException { - try (BufferedReader reader = - new BufferedReader( - new InputStreamReader(connection.getInputStream(), - StandardCharsets.UTF_8)) - ) { - final StringBuilder stringBuffer = new StringBuilder(); - String line; - while ((line = reader.readLine()) != null) { - stringBuffer.append(line); - } - return stringBuffer.toString(); - } - } - - private boolean isGroupObject(final Membership membership) { - return membership.getObjectType().equals(properties.getUserGroup().getValue()); - } } diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/implementation/GraphClient.java b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/implementation/GraphClient.java new file mode 100644 index 000000000000..8aa3f52f00da --- /dev/null +++ b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/aad/implementation/GraphClient.java @@ -0,0 +1,97 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.aad.implementation; + +import com.azure.spring.autoconfigure.aad.AADAuthenticationProperties; +import com.azure.spring.autoconfigure.aad.JacksonObjectMapperFactory; +import com.azure.spring.autoconfigure.aad.Membership; +import com.azure.spring.autoconfigure.aad.Memberships; +import com.fasterxml.jackson.databind.ObjectMapper; +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 java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.LinkedHashSet; +import java.util.Optional; +import java.util.Set; + +public class GraphClient { + private static final Logger LOGGER = LoggerFactory.getLogger(GraphClient.class); + + private final AADAuthenticationProperties properties; + + public GraphClient(AADAuthenticationProperties properties) { + this.properties = properties; + } + + public Set getGroupsFromGraph(String accessToken) { + final Set groups = new LinkedHashSet<>(); + final ObjectMapper objectMapper = JacksonObjectMapperFactory.getInstance(); + String aadMembershipRestUri = properties.getGraphMembershipUri(); + while (aadMembershipRestUri != null) { + Memberships memberships; + try { + String membershipsJson = getUserMemberships(accessToken, aadMembershipRestUri); + memberships = objectMapper.readValue(membershipsJson, Memberships.class); + } catch (IOException ioException) { + LOGGER.error("Can not get group information from graph server.", ioException); + break; + } + memberships.getValue() + .stream() + .filter(this::isGroupObject) + .map(Membership::getDisplayName) + .forEach(groups::add); + aadMembershipRestUri = Optional.of(memberships) + .map(Memberships::getOdataNextLink) + .orElse(null); + } + return groups; + } + + private String getUserMemberships(String accessToken, String urlString) throws IOException { + URL url = new URL(urlString); + final HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + connection.setRequestMethod(HttpMethod.GET.toString()); + connection.setRequestProperty(HttpHeaders.AUTHORIZATION, String.format("Bearer %s", accessToken)); + connection.setRequestProperty(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE); + connection.setRequestProperty(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE); + final String responseInJson = getResponseString(connection); + final int responseCode = connection.getResponseCode(); + if (responseCode == HTTPResponse.SC_OK) { + return responseInJson; + } else { + throw new IllegalStateException( + "Response is not " + HTTPResponse.SC_OK + ", response json: " + responseInJson); + } + } + + private String getResponseString(HttpURLConnection connection) throws IOException { + try (BufferedReader reader = + new BufferedReader( + new InputStreamReader(connection.getInputStream(), StandardCharsets.UTF_8) + ) + ) { + final StringBuilder stringBuffer = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null) { + stringBuffer.append(line); + } + return stringBuffer.toString(); + } + } + + private boolean isGroupObject(final Membership membership) { + return membership.getObjectType().equals(properties.getUserGroup().getValue()); + } +} diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/autoconfigure/aad/AADOAuth2UserService.java b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/autoconfigure/aad/AADOAuth2UserService.java index b8026c125316..d5548a5645b8 100644 --- a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/autoconfigure/aad/AADOAuth2UserService.java +++ b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/autoconfigure/aad/AADOAuth2UserService.java @@ -15,16 +15,19 @@ import org.springframework.security.oauth2.core.OAuth2Error; 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.Optional; import java.util.Set; +import java.util.stream.Collectors; import static com.azure.spring.autoconfigure.aad.AADOAuth2ErrorCode.CONDITIONAL_ACCESS_POLICY; import static com.azure.spring.autoconfigure.aad.AADOAuth2ErrorCode.INVALID_REQUEST; import static com.azure.spring.autoconfigure.aad.AADOAuth2ErrorCode.SERVER_SERVER; +import static com.azure.spring.autoconfigure.aad.Constants.ROLE_PREFIX; /** * This implementation will retrieve group info of user from Microsoft Graph and map groups to {@link @@ -46,7 +49,7 @@ public AADOAuth2UserService(AADAuthenticationProperties aadAuthenticationPropert public OidcUser loadUser(OidcUserRequest userRequest) throws OAuth2AuthenticationException { // Delegate to the default implementation for loading a user OidcUser oidcUser = oidcUserService.loadUser(userRequest); - final Set mappedAuthorities; + final Set authorities; try { // https://github.com/MicrosoftDocs/azure-docs/issues/8121#issuecomment-387090099 // In AAD App Registration configure oauth2AllowImplicitFlow to true @@ -63,7 +66,19 @@ public OidcUser loadUser(OidcUserRequest userRequest) throws OAuth2Authenticatio aadAuthenticationProperties.getTenantId() ) .accessToken(); - mappedAuthorities = azureADGraphClient.getGrantedAuthorities(graphApiToken); + Set groups = azureADGraphClient.getGroups(graphApiToken); + Set groupRoles = groups.stream() + .filter(aadAuthenticationProperties::isAllowedGroup) + .map(group -> ROLE_PREFIX + group) + .collect(Collectors.toSet()); + Set allRoles = oidcUser.getAuthorities() + .stream() + .map(GrantedAuthority::getAuthority) + .collect(Collectors.toSet()); + allRoles.addAll(groupRoles); + authorities = allRoles.stream() + .map(SimpleGrantedAuthority::new) + .collect(Collectors.toSet()); } catch (MalformedURLException e) { throw toOAuth2AuthenticationException(INVALID_REQUEST, "Failed to acquire token for Graph API.", e); } catch (ServiceUnavailableException e) { @@ -85,10 +100,10 @@ public OidcUser loadUser(OidcUserRequest userRequest) throws OAuth2Authenticatio .map(ClientRegistration::getProviderDetails) .map(ClientRegistration.ProviderDetails::getUserInfoEndpoint) .map(ClientRegistration.ProviderDetails.UserInfoEndpoint::getUserNameAttributeName) - .filter(s -> !s.isEmpty()) + .filter(StringUtils::hasText) .orElse(AADTokenClaim.NAME); // Create a copy of oidcUser but use the mappedAuthorities instead - return new DefaultOidcUser(mappedAuthorities, oidcUser.getIdToken(), nameAttributeKey); + return new DefaultOidcUser(authorities, oidcUser.getIdToken(), nameAttributeKey); } private OAuth2AuthenticationException toOAuth2AuthenticationException(String errorCode, diff --git a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/autoconfigure/aad/AzureADGraphClient.java b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/autoconfigure/aad/AzureADGraphClient.java index 53873d2589f4..359ae5bb9858 100644 --- a/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/autoconfigure/aad/AzureADGraphClient.java +++ b/sdk/spring/azure-spring-boot/src/main/java/com/azure/spring/autoconfigure/aad/AzureADGraphClient.java @@ -146,15 +146,6 @@ private boolean isGroupObject(final Membership membership) { return membership.getObjectType().equals(aadAuthenticationProperties.getUserGroup().getValue()); } - /** - * @param graphApiToken token of graph api. - * @return set of SimpleGrantedAuthority - * @throws IOException throw exception if get groups failed by IOException. - */ - public Set getGrantedAuthorities(String graphApiToken) throws IOException { - return toGrantedAuthoritySet(getGroups(graphApiToken)); - } - public Set toGrantedAuthoritySet(final Set groups) { Set grantedAuthoritySet = groups.stream() diff --git a/sdk/spring/azure-spring-boot/src/test/java/com/azure/spring/autoconfigure/aad/UserPrincipalAzureADGraphTest.java b/sdk/spring/azure-spring-boot/src/test/java/com/azure/spring/autoconfigure/aad/UserPrincipalAzureADGraphTest.java index 5b5aa984546c..d31ec8579fbc 100644 --- a/sdk/spring/azure-spring-boot/src/test/java/com/azure/spring/autoconfigure/aad/UserPrincipalAzureADGraphTest.java +++ b/sdk/spring/azure-spring-boot/src/test/java/com/azure/spring/autoconfigure/aad/UserPrincipalAzureADGraphTest.java @@ -13,7 +13,6 @@ 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; @@ -25,10 +24,9 @@ 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 java.util.Set; import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; import static com.github.tomakehurst.wiremock.client.WireMock.equalTo; @@ -83,31 +81,9 @@ public void setup() { clientSecret = "pass"; } - @Test - public void getAuthoritiesByUserGroups() throws Exception { - aadAuthenticationProperties.getUserGroup().setAllowedGroups(Collections.singletonList("group1")); - this.graphClientMock = new AzureADGraphClient(clientId, clientSecret, aadAuthenticationProperties, - serviceEndpointsProperties); - - 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(TestConstants.ACCESS_TOKEN)).isNotEmpty() - .extracting(GrantedAuthority::getAuthority).containsExactly("ROLE_group1"); - - verify(getRequestedFor(urlMatching("/memberOf")) - .withHeader(HttpHeaders.AUTHORIZATION, equalTo(String.format("Bearer %s", accessToken))) - .withHeader(ACCEPT, equalTo("application/json;odata=minimalmetadata")) - .withHeader("api-version", equalTo("1.6"))); - } - @Test public void getGroups() throws Exception { - aadAuthenticationProperties.setActiveDirectoryGroups(Arrays.asList("group1", "group2", "group3")); + aadAuthenticationProperties.getUserGroup().setAllowedGroups(Arrays.asList("group1", "group2", "group3")); this.graphClientMock = new AzureADGraphClient(clientId, clientSecret, aadAuthenticationProperties, serviceEndpointsProperties); @@ -118,11 +94,8 @@ public void getGroups() throws Exception { .withHeader(CONTENT_TYPE, APPLICATION_JSON_VALUE) .withBody(userGroupsJson))); - final Collection authorities = graphClientMock - .getGrantedAuthorities(TestConstants.ACCESS_TOKEN); - - assertThat(authorities).isNotEmpty().extracting(GrantedAuthority::getAuthority) - .containsExactlyInAnyOrder("ROLE_group1", "ROLE_group2", "ROLE_group3"); + Set groups = graphClientMock.getGroups(TestConstants.ACCESS_TOKEN); + assertThat(groups).isNotEmpty().containsExactlyInAnyOrder("group1", "group2", "group3"); verify(getRequestedFor(urlMatching("/memberOf")) .withHeader(HttpHeaders.AUTHORIZATION, equalTo(String.format("Bearer %s", accessToken))) diff --git a/sdk/spring/azure-spring-boot/src/test/java/com/azure/spring/autoconfigure/aad/UserPrincipalMicrosoftGraphTest.java b/sdk/spring/azure-spring-boot/src/test/java/com/azure/spring/autoconfigure/aad/UserPrincipalMicrosoftGraphTest.java index de1b910a4594..08e393a2053a 100644 --- a/sdk/spring/azure-spring-boot/src/test/java/com/azure/spring/autoconfigure/aad/UserPrincipalMicrosoftGraphTest.java +++ b/sdk/spring/azure-spring-boot/src/test/java/com/azure/spring/autoconfigure/aad/UserPrincipalMicrosoftGraphTest.java @@ -12,7 +12,6 @@ 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; @@ -24,10 +23,9 @@ 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 java.util.Set; import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; import static com.github.tomakehurst.wiremock.client.WireMock.equalTo; @@ -87,32 +85,9 @@ public void setup() { clientSecret = "pass"; } - @Test - public void getAuthoritiesByUserGroups() throws Exception { - aadAuthenticationProperties.getUserGroup().setAllowedGroups(Collections.singletonList("group1")); - this.graphClientMock = new AzureADGraphClient(clientId, clientSecret, aadAuthenticationProperties, - serviceEndpointsProperties); - - 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 { - aadAuthenticationProperties.setActiveDirectoryGroups(Arrays.asList("group1", "group2", "group3")); + aadAuthenticationProperties.getUserGroup().setAllowedGroups(Arrays.asList("group1", "group2", "group3")); this.graphClientMock = new AzureADGraphClient(clientId, clientSecret, aadAuthenticationProperties, serviceEndpointsProperties); @@ -123,13 +98,10 @@ public void getGroups() throws Exception { .withHeader(CONTENT_TYPE, APPLICATION_JSON_VALUE) .withBody(userGroupsJson))); - final Collection authorities = graphClientMock - .getGrantedAuthorities(MicrosoftGraphConstants.BEARER_TOKEN); - - assertThat(authorities) + Set groups = graphClientMock.getGroups(MicrosoftGraphConstants.BEARER_TOKEN); + assertThat(groups) .isNotEmpty() - .extracting(GrantedAuthority::getAuthority) - .containsExactlyInAnyOrder("ROLE_group1", "ROLE_group2", "ROLE_group3"); + .containsExactlyInAnyOrder("group1", "group2", "group3"); verify(getRequestedFor(urlMatching("/memberOf")) .withHeader(AUTHORIZATION, equalTo(String.format("Bearer %s", accessToken)))