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 <psevestre@gmail.com>
This commit is contained in:
parent
8fd4ad46ea
commit
a713d96d4d
|
@ -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) {
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
# Sentry configuration file
|
||||
# put your DSN here
|
||||
dsn=https://xxxxxxxxxxxxxxxx@zzzzzzz.ingest.sentry.io/wwww
|
||||
# put your own DSN here. This one is NOT valid !!!
|
||||
dsn=https://c295098aadd04f719f1c9f50d801f93e@o75061.ingest.sentry.io/4504455033978880
|
|
@ -49,6 +49,7 @@
|
|||
<module>spring-security-web-x509</module>
|
||||
<module>spring-security-opa</module>
|
||||
<module>spring-security-pkce</module>
|
||||
<module>spring-security-azuread</module>
|
||||
</modules>
|
||||
|
||||
</project>
|
|
@ -0,0 +1,76 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project
|
||||
xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>com.baeldung</groupId>
|
||||
<artifactId>parent-boot-2</artifactId>
|
||||
<version>0.0.1-SNAPSHOT</version>
|
||||
<relativePath>../../parent-boot-2</relativePath>
|
||||
</parent>
|
||||
<artifactId>spring-security-azuread</artifactId>
|
||||
|
||||
<properties>
|
||||
<java.version>1.8</java.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-oauth2-client</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-devtools</artifactId>
|
||||
<scope>runtime</scope>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-configuration-processor</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-thymeleaf</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
<configuration>
|
||||
<excludes>
|
||||
<exclude>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
</exclude>
|
||||
</excludes>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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<OidcUserRequest, OidcUser> 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<GrantedAuthority> 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());
|
||||
// }
|
||||
|
||||
|
||||
}
|
|
@ -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<String,List<String>> 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<String, List<String>> getGroupToAuthorities() {
|
||||
return groupToAuthorities;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param groupToAuthorities the groupToAuthorities to set
|
||||
*/
|
||||
public void setGroupToAuthorities(Map<String, List<String>> 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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
|
@ -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";
|
||||
}
|
||||
}
|
|
@ -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<String, List<String>> groupToAuthorities;
|
||||
|
||||
public GroupsClaimMapper(String authoritiesPrefix, String groupsClaim, Map<String, List<String>> groupToAuthorities) {
|
||||
this.authoritiesPrefix = authoritiesPrefix;
|
||||
this.groupsClaim = groupsClaim;
|
||||
this.groupToAuthorities = Collections.unmodifiableMap(groupToAuthorities);
|
||||
}
|
||||
|
||||
public Collection<? extends GrantedAuthority> mapAuthorities(ClaimAccessor source) {
|
||||
|
||||
List<String> groups = source.getClaimAsStringList(groupsClaim);
|
||||
if ( groups == null || groups.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
List<GrantedAuthority> result = new ArrayList<>();
|
||||
for( String g : groups) {
|
||||
List<String> authNames = groupToAuthorities.get(g);
|
||||
if ( authNames == null ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
List<SimpleGrantedAuthority> mapped = authNames.stream()
|
||||
.map( s -> authoritiesPrefix + s)
|
||||
.map( SimpleGrantedAuthority::new)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
result.addAll(mapped);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
|
@ -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<? extends GrantedAuthority> authorities, OidcIdToken idToken,
|
||||
OidcUserInfo userInfo, String userName) {
|
||||
super(authorities,idToken,userInfo);
|
||||
this.userName= userName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return this.userName;
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -0,0 +1 @@
|
|||
|
|
@ -0,0 +1,53 @@
|
|||
<!DOCTYPE html>
|
||||
<html data-theme="dark" >
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Insert title here</title>
|
||||
<link rel="stylesheet" href="https://unpkg.com/@picocss/pico@latest/css/pico.min.css">
|
||||
</head>
|
||||
<body class="container">
|
||||
<h1>Hello, <span data-th-text="${user.name}">NOMO_NOMO</span></h1>
|
||||
|
||||
<h2>User info:</h2>
|
||||
|
||||
<table role="grid">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Attribute</th>
|
||||
<th>Value</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Name</td>
|
||||
<td data-th-text="${user.name}">user.name</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Class</td>
|
||||
<td data-th-text="${user.class}">user.class</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Principal Class</td>
|
||||
<td data-th-text="${user.principal.class}">user.principal.class</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>isAuthenticated?</td>
|
||||
<td data-th-text="${user.authenticated}">user.authenticated</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>clientRegistrationId</td>
|
||||
<td data-th-text="${user.authorizedClientRegistrationId}">user.authorizedClientregistrationId</td>
|
||||
</tr>
|
||||
<tr data-th-each="claim : ${user.principal.claims}">
|
||||
<td >Claim: <span data-th-text="${claim.key}">key</span></td>
|
||||
<td data-th-text="${claim.value}">value</td>
|
||||
</tr>
|
||||
<tr data-th-each="ga : ${user.authorities}">
|
||||
<td >Granted Authority: <span data-th-text="${ga.class.simpleName}"></span></td>
|
||||
<td data-th-text="${ga.authority}">authority</td>
|
||||
</tr>
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
|
@ -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<String> 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");
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -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<String,List<String>> g2a = new HashMap<>();
|
||||
|
||||
@Test
|
||||
void testWhenNoGroupClaimsPresent_thenNoAuthoritiesAdded() {
|
||||
|
||||
ClaimAccessor source = mock(ClaimAccessor.class);
|
||||
GroupsClaimMapper mapper = new GroupsClaimMapper("ROLE", "group", g2a);
|
||||
|
||||
Collection<? extends GrantedAuthority> 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<? extends GrantedAuthority> authorities = mapper.mapAuthorities(source);
|
||||
assertThat(authorities)
|
||||
.isNotNull()
|
||||
.isEmpty();
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue