* [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:
psevestre 2023-02-25 16:46:17 -03:00 committed by GitHub
parent 8fd4ad46ea
commit a713d96d4d
15 changed files with 532 additions and 3 deletions

View File

@ -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) {

View File

@ -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

View File

@ -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>

View File

@ -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>

View File

@ -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);
}
}

View File

@ -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());
// }
}

View File

@ -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;
}
}

View File

@ -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";
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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

View File

@ -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>

View File

@ -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");
}
}

View File

@ -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();
}
}