Skip to content

Commit 4831a55

Browse files
committed
Fix #34941: Support MS Graph API to fetch groups
1 parent 459710f commit 4831a55

3 files changed

Lines changed: 148 additions & 0 deletions

File tree

pom.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,10 @@
117117
<groupId>org.springframework.integration</groupId>
118118
<artifactId>spring-integration-redis</artifactId>
119119
</dependency>
120+
<dependency>
121+
<groupId>org.springframework.boot</groupId>
122+
<artifactId>spring-boot-starter-webflux</artifactId>
123+
</dependency>
120124

121125
<dependency>
122126
<groupId>org.springframework.boot</groupId>

src/main/java/eu/openanalytics/containerproxy/auth/impl/OpenIDAuthenticationBackend.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
package eu.openanalytics.containerproxy.auth.impl;
2222

2323
import eu.openanalytics.containerproxy.auth.IAuthenticationBackend;
24+
import eu.openanalytics.containerproxy.auth.impl.msgraph.MicrosoftGraphGroupFetcher;
2425
import eu.openanalytics.containerproxy.auth.impl.oidc.AccessTokenDecoder;
2526
import eu.openanalytics.containerproxy.auth.impl.oidc.OpenIdReAuthorizeFilter;
2627
import eu.openanalytics.containerproxy.spec.expression.SpecExpressionContext;
@@ -72,6 +73,7 @@
7273
import java.util.HashSet;
7374
import java.util.List;
7475
import java.util.Map;
76+
import java.util.Optional;
7577
import java.util.Set;
7678
import java.util.stream.Collectors;
7779

@@ -98,6 +100,8 @@ public class OpenIDAuthenticationBackend implements IAuthenticationBackend {
98100
private SpecExpressionResolver specExpressionResolver;
99101
@Inject
100102
private ContextPathHelper contextPathHelper;
103+
@Autowired(required = false)
104+
private MicrosoftGraphGroupFetcher microsoftGraphGroupFetcher;
101105

102106
/**
103107
* Parses the claim containing the roles to a List of Strings.
@@ -287,6 +291,18 @@ public LogoutSuccessHandler getLogoutSuccessHandler() {
287291
}
288292

289293
protected GrantedAuthoritiesMapper createAuthoritiesMapper() {
294+
if (microsoftGraphGroupFetcher != null) {
295+
log.info("Using MS graph");
296+
return authorities -> {
297+
for (GrantedAuthority auth : authorities) {
298+
if (auth instanceof OidcUserAuthority) {
299+
OidcIdToken idToken = ((OidcUserAuthority) auth).getIdToken();
300+
return microsoftGraphGroupFetcher.fetchGroups(idToken.getSubject());
301+
}
302+
}
303+
return Set.of();
304+
};
305+
}
290306
String rolesClaimName = environment.getProperty("proxy.openid.roles-claim");
291307
return authorities -> {
292308
Set<GrantedAuthority> mappedAuthorities = new HashSet<>();
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
/*
2+
* ContainerProxy
3+
*
4+
* Copyright (C) 2016-2025 Open Analytics
5+
*
6+
* ===========================================================================
7+
*
8+
* This program is free software: you can redistribute it and/or modify
9+
* it under the terms of the Apache License as published by
10+
* The Apache Software Foundation, either version 2 of the License, or
11+
* (at your option) any later version.
12+
*
13+
* This program is distributed in the hope that it will be useful,
14+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
15+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16+
* Apache License for more details.
17+
*
18+
* You should have received a copy of the Apache License
19+
* along with this program. If not, see <http://www.apache.org/licenses/>
20+
*/
21+
package eu.openanalytics.containerproxy.auth.impl.msgraph;
22+
23+
import eu.openanalytics.containerproxy.util.EnvironmentUtils;
24+
import org.slf4j.Logger;
25+
import org.slf4j.LoggerFactory;
26+
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
27+
import org.springframework.core.env.Environment;
28+
import org.springframework.http.HttpHeaders;
29+
import org.springframework.http.HttpStatusCode;
30+
import org.springframework.http.MediaType;
31+
import org.springframework.security.core.GrantedAuthority;
32+
import org.springframework.security.core.authority.SimpleGrantedAuthority;
33+
import org.springframework.security.oauth2.client.AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager;
34+
import org.springframework.security.oauth2.client.InMemoryReactiveOAuth2AuthorizedClientService;
35+
import org.springframework.security.oauth2.client.registration.ClientRegistration;
36+
import org.springframework.security.oauth2.client.registration.InMemoryReactiveClientRegistrationRepository;
37+
import org.springframework.security.oauth2.client.web.reactive.function.client.ServerOAuth2AuthorizedClientExchangeFilterFunction;
38+
import org.springframework.security.oauth2.core.AuthorizationGrantType;
39+
import org.springframework.stereotype.Component;
40+
import org.springframework.web.reactive.function.client.WebClient;
41+
import reactor.core.publisher.Mono;
42+
43+
import java.util.HashSet;
44+
import java.util.List;
45+
import java.util.Set;
46+
47+
@ConditionalOnProperty("proxy.ms-graph.client-id")
48+
@Component
49+
public class MicrosoftGraphGroupFetcher {
50+
51+
private static final String REGISTRATION_ID = "shinyproxy-ms-graph";
52+
private final String tenantId;
53+
private final Logger logger = LoggerFactory.getLogger(getClass());
54+
private final WebClient webClient;
55+
56+
public MicrosoftGraphGroupFetcher(Environment environment) {
57+
String clientId = environment.getProperty("proxy.ms-graph.client-id");
58+
String clientSecret = environment.getProperty("proxy.ms-graph.client-secret");
59+
String graphApiUrl = environment.getProperty("proxy.ms-graph.api-url", "https://graph.microsoft.com");
60+
String tokenUrl = environment.getProperty("proxy.ms-graph.token-url");
61+
List<String> scopes = EnvironmentUtils.readList(environment, "proxy.ms-graph.scopes");
62+
if (scopes == null || scopes.isEmpty()) {
63+
scopes = List.of("https://graph.microsoft.com/.default");
64+
}
65+
tenantId = environment.getProperty("proxy.ms-graph.tenant-id");
66+
67+
ClientRegistration clientRegistration = ClientRegistration.withRegistrationId(REGISTRATION_ID)
68+
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
69+
.clientId(clientId)
70+
.clientSecret(clientSecret)
71+
.tokenUri(tokenUrl)
72+
.scope(scopes)
73+
.build();
74+
ServerOAuth2AuthorizedClientExchangeFilterFunction oauth = getServerOAuth2AuthorizedClientExchangeFilterFunction(clientRegistration);
75+
76+
webClient = WebClient.builder()
77+
.baseUrl(graphApiUrl)
78+
.filter(oauth)
79+
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
80+
.build();
81+
}
82+
83+
private ServerOAuth2AuthorizedClientExchangeFilterFunction getServerOAuth2AuthorizedClientExchangeFilterFunction(ClientRegistration clientRegistration) {
84+
InMemoryReactiveClientRegistrationRepository clientRegistrations = new InMemoryReactiveClientRegistrationRepository(clientRegistration);
85+
InMemoryReactiveOAuth2AuthorizedClientService clientService = new InMemoryReactiveOAuth2AuthorizedClientService(clientRegistrations);
86+
AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager authorizedClientManager = new AuthorizedClientServiceReactiveOAuth2AuthorizedClientManager(clientRegistrations, clientService);
87+
ServerOAuth2AuthorizedClientExchangeFilterFunction oauth = new ServerOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager);
88+
oauth.setDefaultClientRegistrationId(REGISTRATION_ID);
89+
return oauth;
90+
}
91+
92+
public Set<GrantedAuthority> fetchGroups(String userId) {
93+
try {
94+
MemberOfResponse memberships = webClient.get()
95+
.uri(String.format("/v1.0/%s/users/%s/memberOf", tenantId, userId))
96+
.retrieve()
97+
.onStatus(HttpStatusCode::isError,
98+
response -> response.bodyToMono(String.class)
99+
.flatMap(body -> Mono.error(new IllegalStateException(String.format("Error from Microsoft Graph API, status: %s, response: %s", response.statusCode(), body))))
100+
)
101+
.bodyToFlux(MemberOfResponse.class).blockLast();
102+
103+
if (memberships == null) {
104+
logger.warn("No group memberships found for {}", userId);
105+
return Set.of();
106+
}
107+
108+
Set<GrantedAuthority> result = new HashSet<>(memberships.value.stream().map(m -> {
109+
String mappedRole = m.displayName
110+
.toUpperCase()
111+
.startsWith("ROLE_") ? m.displayName : "ROLE_" + m.displayName;
112+
return new SimpleGrantedAuthority(mappedRole.toUpperCase());
113+
}).toList());
114+
logger.debug("Received groups from Microsoft Graph api for user: {}, groups: {}", userId, result);
115+
return result;
116+
} catch (Exception e) {
117+
logger.warn("Error while fetching groups from Microsoft Graph API - continuing without groups", e);
118+
return Set.of();
119+
}
120+
}
121+
122+
private record MemberOfResponse(List<GroupMembership> value) {
123+
}
124+
125+
private record GroupMembership(String id, String displayName) {
126+
}
127+
128+
}

0 commit comments

Comments
 (0)