From a713d96d4da5056c2ebf2881941b26d10c5254ac Mon Sep 17 00:00:00 2001 From: psevestre Date: Sat, 25 Feb 2023 16:46:17 -0300 Subject: [PATCH] BAEL 6160 (#13519) * [BAEL-4849] Article code * [BAEL-4968] Article code * [BAEL-4968] Article code * [BAEL-4968] Article code * [BAEL-4968] Remove extra comments * [BAEL-5258] Article Code * [BAEL-2765] PKCE Support for Secret Clients * [BAEL-5698] Article code * [BAEL-5698] Article code * [BAEL-5900] Initial commit * [BAEL-5974] Article Code * [BAEL-5974] Article Code * [BAEL-6160] Article code * [BAEL-6160] Tests --------- Co-authored-by: Philippe Sevestre --- .../baeldung/sentry/support/SentryFilter.java | 2 +- .../src/main/resources/sentry.properties | 4 +- spring-security-modules/pom.xml | 1 + .../spring-security-azuread/pom.xml | 76 +++++++++++++++++++ .../security/azuread/Application.java | 14 ++++ .../config/JwtAuthorizationConfiguration.java | 72 ++++++++++++++++++ .../config/JwtAuthorizationProperties.java | 68 +++++++++++++++++ .../azuread/controllers/IndexController.java | 22 ++++++ .../azuread/support/GroupsClaimMapper.java | 61 +++++++++++++++ .../azuread/support/NamedOidcUser.java | 24 ++++++ .../main/resources/application-azuread.yml | 27 +++++++ .../src/main/resources/application.properties | 1 + .../src/main/resources/templates/index.html | 53 +++++++++++++ .../security/azuread/ApplicationLiveTest.java | 62 +++++++++++++++ .../support/GroupsClaimMapperUnitTest.java | 48 ++++++++++++ 15 files changed, 532 insertions(+), 3 deletions(-) create mode 100644 spring-security-modules/spring-security-azuread/pom.xml create mode 100644 spring-security-modules/spring-security-azuread/src/main/java/com/baeldung/security/azuread/Application.java create mode 100644 spring-security-modules/spring-security-azuread/src/main/java/com/baeldung/security/azuread/config/JwtAuthorizationConfiguration.java create mode 100644 spring-security-modules/spring-security-azuread/src/main/java/com/baeldung/security/azuread/config/JwtAuthorizationProperties.java create mode 100644 spring-security-modules/spring-security-azuread/src/main/java/com/baeldung/security/azuread/controllers/IndexController.java create mode 100644 spring-security-modules/spring-security-azuread/src/main/java/com/baeldung/security/azuread/support/GroupsClaimMapper.java create mode 100644 spring-security-modules/spring-security-azuread/src/main/java/com/baeldung/security/azuread/support/NamedOidcUser.java create mode 100644 spring-security-modules/spring-security-azuread/src/main/resources/application-azuread.yml create mode 100644 spring-security-modules/spring-security-azuread/src/main/resources/application.properties create mode 100644 spring-security-modules/spring-security-azuread/src/main/resources/templates/index.html create mode 100644 spring-security-modules/spring-security-azuread/src/test/java/com/baeldung/security/azuread/ApplicationLiveTest.java create mode 100644 spring-security-modules/spring-security-azuread/src/test/java/com/baeldung/security/azuread/support/GroupsClaimMapperUnitTest.java diff --git a/saas-modules/sentry-servlet/src/main/java/com/baeldung/sentry/support/SentryFilter.java b/saas-modules/sentry-servlet/src/main/java/com/baeldung/sentry/support/SentryFilter.java index e853368ad9..51055f321d 100644 --- a/saas-modules/sentry-servlet/src/main/java/com/baeldung/sentry/support/SentryFilter.java +++ b/saas-modules/sentry-servlet/src/main/java/com/baeldung/sentry/support/SentryFilter.java @@ -21,7 +21,7 @@ public class SentryFilter implements Filter { try { chain.doFilter(request, response); int rc = ((HttpServletResponse) response).getStatus(); - if (rc/100 == 5) { + if (rc / 100 == 5) { Sentry.captureMessage("Application error: code=" + rc, SentryLevel.ERROR); } } catch (Throwable t) { diff --git a/saas-modules/sentry-servlet/src/main/resources/sentry.properties b/saas-modules/sentry-servlet/src/main/resources/sentry.properties index c937874420..cd8b3005dd 100644 --- a/saas-modules/sentry-servlet/src/main/resources/sentry.properties +++ b/saas-modules/sentry-servlet/src/main/resources/sentry.properties @@ -1,3 +1,3 @@ # Sentry configuration file -# put your DSN here -dsn=https://xxxxxxxxxxxxxxxx@zzzzzzz.ingest.sentry.io/wwww \ No newline at end of file +# put your own DSN here. This one is NOT valid !!! +dsn=https://c295098aadd04f719f1c9f50d801f93e@o75061.ingest.sentry.io/4504455033978880 \ No newline at end of file diff --git a/spring-security-modules/pom.xml b/spring-security-modules/pom.xml index d6e30e8ab8..223f0894d5 100644 --- a/spring-security-modules/pom.xml +++ b/spring-security-modules/pom.xml @@ -49,6 +49,7 @@ spring-security-web-x509 spring-security-opa spring-security-pkce + spring-security-azuread \ No newline at end of file diff --git a/spring-security-modules/spring-security-azuread/pom.xml b/spring-security-modules/spring-security-azuread/pom.xml new file mode 100644 index 0000000000..c4dbbd14b9 --- /dev/null +++ b/spring-security-modules/spring-security-azuread/pom.xml @@ -0,0 +1,76 @@ + + + + 4.0.0 + + com.baeldung + parent-boot-2 + 0.0.1-SNAPSHOT + ../../parent-boot-2 + + spring-security-azuread + + + 1.8 + + + + + org.springframework.boot + spring-boot-starter-oauth2-client + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.boot + spring-boot-starter + + + org.springframework.boot + spring-boot-devtools + runtime + true + + + org.springframework.boot + spring-boot-configuration-processor + true + + + org.projectlombok + lombok + true + + + org.springframework.boot + spring-boot-starter-thymeleaf + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.projectlombok + lombok + + + + + + + \ No newline at end of file diff --git a/spring-security-modules/spring-security-azuread/src/main/java/com/baeldung/security/azuread/Application.java b/spring-security-modules/spring-security-azuread/src/main/java/com/baeldung/security/azuread/Application.java new file mode 100644 index 0000000000..ac36bc1328 --- /dev/null +++ b/spring-security-modules/spring-security-azuread/src/main/java/com/baeldung/security/azuread/Application.java @@ -0,0 +1,14 @@ +package com.baeldung.security.azuread; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + + +@SpringBootApplication +public class Application { + + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } + +} diff --git a/spring-security-modules/spring-security-azuread/src/main/java/com/baeldung/security/azuread/config/JwtAuthorizationConfiguration.java b/spring-security-modules/spring-security-azuread/src/main/java/com/baeldung/security/azuread/config/JwtAuthorizationConfiguration.java new file mode 100644 index 0000000000..4d82e930ae --- /dev/null +++ b/spring-security-modules/spring-security-azuread/src/main/java/com/baeldung/security/azuread/config/JwtAuthorizationConfiguration.java @@ -0,0 +1,72 @@ +package com.baeldung.security.azuread.config; + +import java.util.HashSet; +import java.util.Set; + +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest; +import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserService; +import org.springframework.security.oauth2.client.registration.ClientRegistration.ProviderDetails; +import org.springframework.security.oauth2.client.userinfo.OAuth2UserService; +import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser; +import org.springframework.security.oauth2.core.oidc.user.OidcUser; +import org.springframework.security.web.SecurityFilterChain; + +import com.baeldung.security.azuread.support.GroupsClaimMapper; +import com.baeldung.security.azuread.support.NamedOidcUser; + +@Configuration +@EnableConfigurationProperties(JwtAuthorizationProperties.class) +public class JwtAuthorizationConfiguration { + + + + @Bean + SecurityFilterChain customJwtSecurityChain(HttpSecurity http, JwtAuthorizationProperties props) throws Exception { + // @formatter:off + return http + .authorizeRequests( r -> r.anyRequest().authenticated()) + .oauth2Login(oauth2 -> { + oauth2.userInfoEndpoint(ep -> + ep.oidcUserService(customOidcUserService(props))); + }) + .build(); + // @formatter:on + } + + private OAuth2UserService customOidcUserService(JwtAuthorizationProperties props) { + final OidcUserService delegate = new OidcUserService(); + final GroupsClaimMapper mapper = new GroupsClaimMapper( + props.getAuthoritiesPrefix(), + props.getGroupsClaim(), + props.getGroupToAuthorities()); + + return (userRequest) -> { + OidcUser oidcUser = delegate.loadUser(userRequest); + // Enrich standard authorities with groups + Set mappedAuthorities = new HashSet<>(); + mappedAuthorities.addAll(oidcUser.getAuthorities()); + mappedAuthorities.addAll(mapper.mapAuthorities(oidcUser)); + + oidcUser = new NamedOidcUser(mappedAuthorities, oidcUser.getIdToken(), oidcUser.getUserInfo(),oidcUser.getName()); + + return oidcUser; + }; + } + + + +// @Bean +// GrantedAuthoritiesMapper jwtAuthoritiesMapper(JwtAuthorizationProperties props) { +// return new MappingJwtGrantedAuthoritiesMapper( +// props.getAuthoritiesPrefix(), +// props.getGroupsClaim(), +// props.getGroupToAuthorities()); +// } + + +} diff --git a/spring-security-modules/spring-security-azuread/src/main/java/com/baeldung/security/azuread/config/JwtAuthorizationProperties.java b/spring-security-modules/spring-security-azuread/src/main/java/com/baeldung/security/azuread/config/JwtAuthorizationProperties.java new file mode 100644 index 0000000000..981be317a3 --- /dev/null +++ b/spring-security-modules/spring-security-azuread/src/main/java/com/baeldung/security/azuread/config/JwtAuthorizationProperties.java @@ -0,0 +1,68 @@ +package com.baeldung.security.azuread.config; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * @author Baeldung + * + */ +@ConfigurationProperties(prefix="baeldung.jwt.authorization") +public class JwtAuthorizationProperties { + + // Claim that has the group list + private String groupsClaim = "groups"; + + private String authoritiesPrefix = "ROLE_"; + + // map groupIds to a list of authorities. + private Map> groupToAuthorities = new HashMap<>(); + + /** + * @return the groupsClaim + */ + public String getGroupsClaim() { + return groupsClaim; + } + + /** + * @param groupsClaim the groupsClaim to set + */ + public void setGroupsClaim(String groupsClaim) { + this.groupsClaim = groupsClaim; + } + + /** + * @return the groupToAuthorities + */ + public Map> getGroupToAuthorities() { + return groupToAuthorities; + } + + /** + * @param groupToAuthorities the groupToAuthorities to set + */ + public void setGroupToAuthorities(Map> groupToAuthorities) { + this.groupToAuthorities = groupToAuthorities; + } + + /** + * @return the authoritiesPrefix + */ + public String getAuthoritiesPrefix() { + return authoritiesPrefix; + } + + /** + * @param authoritiesPrefix the authoritiesPrefix to set + */ + public void setAuthoritiesPrefix(String authoritiesPrefix) { + this.authoritiesPrefix = authoritiesPrefix; + } + + + +} diff --git a/spring-security-modules/spring-security-azuread/src/main/java/com/baeldung/security/azuread/controllers/IndexController.java b/spring-security-modules/spring-security-azuread/src/main/java/com/baeldung/security/azuread/controllers/IndexController.java new file mode 100644 index 0000000000..d2cebd0231 --- /dev/null +++ b/spring-security-modules/spring-security-azuread/src/main/java/com/baeldung/security/azuread/controllers/IndexController.java @@ -0,0 +1,22 @@ +package com.baeldung.security.azuread.controllers; + +import org.springframework.security.core.Authentication; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; + +import lombok.extern.slf4j.Slf4j; + +@Controller +@RequestMapping("/") +@Slf4j +public class IndexController { + + @GetMapping + public String index(Model model, Authentication user) { + log.info("GET /: user={}", user); + model.addAttribute("user", user); + return "index"; + } +} diff --git a/spring-security-modules/spring-security-azuread/src/main/java/com/baeldung/security/azuread/support/GroupsClaimMapper.java b/spring-security-modules/spring-security-azuread/src/main/java/com/baeldung/security/azuread/support/GroupsClaimMapper.java new file mode 100644 index 0000000000..2487cd9db3 --- /dev/null +++ b/spring-security-modules/spring-security-azuread/src/main/java/com/baeldung/security/azuread/support/GroupsClaimMapper.java @@ -0,0 +1,61 @@ +/** + * + */ +package com.baeldung.security.azuread.support; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import org.springframework.core.convert.converter.Converter; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper; +import org.springframework.security.oauth2.core.ClaimAccessor; +import org.springframework.security.oauth2.jwt.Jwt; + +/** + * @author Baeldung + * + */ +public class GroupsClaimMapper { + + private final String authoritiesPrefix; + private final String groupsClaim; + private final Map> groupToAuthorities; + + public GroupsClaimMapper(String authoritiesPrefix, String groupsClaim, Map> groupToAuthorities) { + this.authoritiesPrefix = authoritiesPrefix; + this.groupsClaim = groupsClaim; + this.groupToAuthorities = Collections.unmodifiableMap(groupToAuthorities); + } + + public Collection mapAuthorities(ClaimAccessor source) { + + List groups = source.getClaimAsStringList(groupsClaim); + if ( groups == null || groups.isEmpty()) { + return Collections.emptyList(); + } + + List result = new ArrayList<>(); + for( String g : groups) { + List authNames = groupToAuthorities.get(g); + if ( authNames == null ) { + continue; + } + + List mapped = authNames.stream() + .map( s -> authoritiesPrefix + s) + .map( SimpleGrantedAuthority::new) + .collect(Collectors.toList()); + + result.addAll(mapped); + } + + return result; + } + +} diff --git a/spring-security-modules/spring-security-azuread/src/main/java/com/baeldung/security/azuread/support/NamedOidcUser.java b/spring-security-modules/spring-security-azuread/src/main/java/com/baeldung/security/azuread/support/NamedOidcUser.java new file mode 100644 index 0000000000..b29b04fe7b --- /dev/null +++ b/spring-security-modules/spring-security-azuread/src/main/java/com/baeldung/security/azuread/support/NamedOidcUser.java @@ -0,0 +1,24 @@ +package com.baeldung.security.azuread.support; + +import java.util.Collection; + +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.oauth2.core.oidc.OidcIdToken; +import org.springframework.security.oauth2.core.oidc.OidcUserInfo; +import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser; + +public class NamedOidcUser extends DefaultOidcUser { + private static final long serialVersionUID = 1L; + private final String userName; + + public NamedOidcUser(Collection authorities, OidcIdToken idToken, + OidcUserInfo userInfo, String userName) { + super(authorities,idToken,userInfo); + this.userName= userName; + } + + @Override + public String getName() { + return this.userName; + } +} diff --git a/spring-security-modules/spring-security-azuread/src/main/resources/application-azuread.yml b/spring-security-modules/spring-security-azuread/src/main/resources/application-azuread.yml new file mode 100644 index 0000000000..5e65c381c8 --- /dev/null +++ b/spring-security-modules/spring-security-azuread/src/main/resources/application-azuread.yml @@ -0,0 +1,27 @@ +spring: + security: + oauth2: + client: + provider: + azure: + issuer-uri: https://login.microsoftonline.com/2e9fde3a-38ec-44f9-8bcd-c184dc1e8033/v2.0 + user-name-attribute: name + registration: + azure-dev: + provider: azure + #client-id: "6035bfd4-22f0-437c-b76f-da729a916cbf" + #client-secret: "fo28Q~-aLbmQvonnZtzbgtSiqYstmBWEmGPAodmx" + client-id: your-client-id + client-secret: your-secret-id + scope: + - openid + - email + - profile + +# Group mapping +baeldung: + jwt: + authorization: + group-to-authorities: + "ceef656a-fca9-49b6-821b-f7543b7065cb": BAELDUNG_RW + "eaaecb69-ccbc-4143-b111-7dd1ce1d99f1": BAELDUNG_RO,BAELDUNG_ADMIN \ No newline at end of file diff --git a/spring-security-modules/spring-security-azuread/src/main/resources/application.properties b/spring-security-modules/spring-security-azuread/src/main/resources/application.properties new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/spring-security-modules/spring-security-azuread/src/main/resources/application.properties @@ -0,0 +1 @@ + diff --git a/spring-security-modules/spring-security-azuread/src/main/resources/templates/index.html b/spring-security-modules/spring-security-azuread/src/main/resources/templates/index.html new file mode 100644 index 0000000000..ca9a9bdbe8 --- /dev/null +++ b/spring-security-modules/spring-security-azuread/src/main/resources/templates/index.html @@ -0,0 +1,53 @@ + + + + +Insert title here + + + +

Hello, NOMO_NOMO

+ +

User info:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
AttributeValue
Nameuser.name
Classuser.class
Principal Classuser.principal.class
isAuthenticated?user.authenticated
clientRegistrationIduser.authorizedClientregistrationId
Claim: keyvalue
Granted Authority: authority
+ + \ No newline at end of file diff --git a/spring-security-modules/spring-security-azuread/src/test/java/com/baeldung/security/azuread/ApplicationLiveTest.java b/spring-security-modules/spring-security-azuread/src/test/java/com/baeldung/security/azuread/ApplicationLiveTest.java new file mode 100644 index 0000000000..8c941aa787 --- /dev/null +++ b/spring-security-modules/spring-security-azuread/src/test/java/com/baeldung/security/azuread/ApplicationLiveTest.java @@ -0,0 +1,62 @@ +package com.baeldung.security.azuread; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.*; + +import java.net.URI; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.test.context.ActiveProfiles; + +@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) +@ActiveProfiles("azuread") +class ApplicationLiveTest { + + @Autowired + TestRestTemplate rest; + + @LocalServerPort + int port; + + @Test + void testWhenAccessRootPath_thenRedirectToAzureAD() { + + ResponseEntity response = rest.getForEntity("http://localhost:" + port , String.class); + HttpStatus st = response.getStatusCode(); + assertThat(st) + .isEqualTo(HttpStatus.FOUND); + + URI location1 = response.getHeaders().getLocation(); + assertThat(location1) + .isNotNull(); + assertThat(location1.getPath()) + .isEqualTo("/oauth2/authorization/azure-dev"); + + assertThat(location1.getPort()) + .isEqualTo(port); + + assertThat(location1.getHost()) + .isEqualTo("localhost"); + + // Now let't follow this redirect + response = rest.getForEntity(location1, String.class); + assertThat(st) + .isEqualTo(HttpStatus.FOUND); + URI location2 = response.getHeaders().getLocation(); + assertThat(location2) + .isNotNull(); + + assertThat(location2.getHost()) + .describedAs("Should redirect to AzureAD") + .isEqualTo("login.microsoftonline.com"); + + } + +} diff --git a/spring-security-modules/spring-security-azuread/src/test/java/com/baeldung/security/azuread/support/GroupsClaimMapperUnitTest.java b/spring-security-modules/spring-security-azuread/src/test/java/com/baeldung/security/azuread/support/GroupsClaimMapperUnitTest.java new file mode 100644 index 0000000000..0a8f85d0d1 --- /dev/null +++ b/spring-security-modules/spring-security-azuread/src/test/java/com/baeldung/security/azuread/support/GroupsClaimMapperUnitTest.java @@ -0,0 +1,48 @@ +package com.baeldung.security.azuread.support; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.Test; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.oauth2.core.ClaimAccessor; + +class GroupsClaimMapperUnitTest { + + private Map> g2a = new HashMap<>(); + + @Test + void testWhenNoGroupClaimsPresent_thenNoAuthoritiesAdded() { + + ClaimAccessor source = mock(ClaimAccessor.class); + GroupsClaimMapper mapper = new GroupsClaimMapper("ROLE", "group", g2a); + + Collection authorities = mapper.mapAuthorities(source); + assertThat(authorities) + .isNotNull() + .isEmpty(); + } + + @Test + void testWhenEmptyGroupClaimsPresent_thenNoAuthoritiesAdded() { + + ClaimAccessor source = mock(ClaimAccessor.class); + when(source.getClaimAsStringList("group")) + .thenReturn(Collections.emptyList()); + + GroupsClaimMapper mapper = new GroupsClaimMapper("ROLE", "group", g2a); + + Collection authorities = mapper.mapAuthorities(source); + assertThat(authorities) + .isNotNull() + .isEmpty(); + } + +}