Skip to content

Commit b4d7fd2

Browse files
psevestrePhilippe Sevestre
andauthored
[BAEL-8063] Dynamic Client Registration in Spring Authorization Server (#17286)
* [BAEL-7978] WIP: Sanity check * [BAEL-7978] Article code * [BAEL-8063] WIP * [BAEL-8063] Article code * [BAEL-8063] Article code * [BAEL-8063] Article code * [BAEL-8063] Code cleanup * [BAEL-8063] UnitTests --------- Co-authored-by: Philippe Sevestre <[email protected]>
1 parent 3dc5634 commit b4d7fd2

File tree

16 files changed

+881
-0
lines changed

16 files changed

+881
-0
lines changed

spring-security-modules/pom.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@
5959
<module>spring-security-pkce-spa</module>
6060
<module>spring-security-compromised-password</module>
6161
<module>spring-security-authorization</module>
62+
<module>spring-security-dynamic-registration</module>
6263
</modules>
6364

6465
</project>
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<project xmlns="http://maven.apache.org/POM/4.0.0"
3+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
5+
<modelVersion>4.0.0</modelVersion>
6+
7+
8+
<parent>
9+
<groupId>com.baeldung</groupId>
10+
<artifactId>spring-security-dynamic-registration</artifactId>
11+
<version>0.0.1-SNAPSHOT</version>
12+
</parent>
13+
14+
<artifactId>spring-security-dynamic-registration-client</artifactId>
15+
<packaging>jar</packaging>
16+
<name>${project.artifactId}</name>
17+
<description>Demo project for Dynamic Client: Client module</description>
18+
19+
<dependencies>
20+
<dependency>
21+
<groupId>org.springframework.boot</groupId>
22+
<artifactId>spring-boot-starter-web</artifactId>
23+
</dependency>
24+
<dependency>
25+
<groupId>org.springframework.boot</groupId>
26+
<artifactId>spring-boot-starter-thymeleaf</artifactId>
27+
</dependency>
28+
<dependency>
29+
<groupId>org.springframework.boot</groupId>
30+
<artifactId>spring-boot-starter-oauth2-client</artifactId>
31+
</dependency>
32+
<dependency>
33+
<groupId>org.springframework.boot</groupId>
34+
<artifactId>spring-boot-configuration-processor</artifactId>
35+
<optional>true</optional>
36+
</dependency>
37+
<dependency>
38+
<groupId>org.projectlombok</groupId>
39+
<artifactId>lombok</artifactId>
40+
<optional>true</optional>
41+
</dependency>
42+
<dependency>
43+
<groupId>org.springframework.boot</groupId>
44+
<artifactId>spring-boot-starter-test</artifactId>
45+
<scope>test</scope>
46+
</dependency>
47+
<dependency>
48+
<groupId>io.projectreactor</groupId>
49+
<artifactId>reactor-test</artifactId>
50+
<scope>test</scope>
51+
</dependency>
52+
<dependency>
53+
<groupId>org.springframework.security</groupId>
54+
<artifactId>spring-security-test</artifactId>
55+
<scope>test</scope>
56+
</dependency>
57+
<dependency>
58+
<groupId>org.springframework.boot</groupId>
59+
<artifactId>spring-boot-devtools</artifactId>
60+
<scope>runtime</scope>
61+
</dependency>
62+
</dependencies>
63+
64+
<build>
65+
<plugins>
66+
<plugin>
67+
<groupId>org.springframework.boot</groupId>
68+
<artifactId>spring-boot-maven-plugin</artifactId>
69+
<configuration>
70+
<excludes>
71+
<exclude>
72+
<groupId>org.projectlombok</groupId>
73+
<artifactId>lombok</artifactId>
74+
</exclude>
75+
</excludes>
76+
</configuration>
77+
</plugin>
78+
<plugin>
79+
<groupId>org.apache.maven.plugins</groupId>
80+
<artifactId>maven-compiler-plugin</artifactId>
81+
<configuration>
82+
<source>16</source>
83+
<target>16</target>
84+
</configuration>
85+
</plugin>
86+
</plugins>
87+
</build>
88+
89+
</project>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package com.baeldung.spring.security.dynreg.client;
2+
3+
import org.springframework.boot.SpringApplication;
4+
import org.springframework.boot.autoconfigure.SpringBootApplication;
5+
6+
@SpringBootApplication
7+
public class DynamicRegistrationClientApplication {
8+
9+
public static void main(String[] args) {
10+
SpringApplication.run(DynamicRegistrationClientApplication.class,args);
11+
}
12+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
package com.baeldung.spring.security.dynreg.client.config;
2+
3+
import com.baeldung.spring.security.dynreg.client.service.DynamicClientRegistrationRepository;
4+
import lombok.Getter;
5+
import lombok.RequiredArgsConstructor;
6+
import lombok.Setter;
7+
import lombok.extern.slf4j.Slf4j;
8+
import org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2ClientProperties;
9+
import org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2ClientPropertiesMapper;
10+
import org.springframework.boot.context.properties.ConfigurationProperties;
11+
import org.springframework.boot.context.properties.EnableConfigurationProperties;
12+
import org.springframework.boot.web.client.RestTemplateBuilder;
13+
import org.springframework.context.annotation.Bean;
14+
import org.springframework.context.annotation.Configuration;
15+
import org.springframework.security.config.Customizer;
16+
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
17+
import org.springframework.security.config.annotation.web.configurers.AuthorizeHttpRequestsConfigurer;
18+
import org.springframework.security.oauth2.client.registration.ClientRegistration;
19+
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
20+
import org.springframework.security.oauth2.client.web.DefaultOAuth2AuthorizationRequestResolver;
21+
import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestCustomizers;
22+
import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestResolver;
23+
import org.springframework.security.web.SecurityFilterChain;
24+
25+
import java.net.URI;
26+
import java.util.List;
27+
import java.util.Map;
28+
29+
@Configuration
30+
@EnableConfigurationProperties({ OAuth2DynamicClientConfiguration.RegistrationProperties.class, OAuth2ClientProperties.class })
31+
@Slf4j
32+
@RequiredArgsConstructor
33+
public class OAuth2DynamicClientConfiguration {
34+
35+
private final OAuth2ClientProperties clientProperties;
36+
private final RegistrationProperties registrationProperties;
37+
38+
@Bean
39+
ClientRegistrationRepository dynamicClientRegistrationRepository( DynamicClientRegistrationRepository.RegistrationRestTemplate restTemplate) {
40+
41+
log.info("Creating a dynamic client registration repository");
42+
43+
var registrationDetails = new DynamicClientRegistrationRepository.RegistrationDetails(
44+
registrationProperties.getRegistrationEndpoint(),
45+
registrationProperties.getRegistrationUsername(),
46+
registrationProperties.getRegistrationPassword(),
47+
registrationProperties.getRegistrationScopes(),
48+
registrationProperties.getGrantTypes(),
49+
registrationProperties.getRedirectUris(),
50+
registrationProperties.getTokenEndpoint());
51+
52+
// Use standard client registrations as
53+
Map<String,ClientRegistration> staticClients = (new OAuth2ClientPropertiesMapper(clientProperties)).asClientRegistrations();
54+
var repo = new DynamicClientRegistrationRepository(registrationDetails, staticClients, restTemplate);
55+
repo.doRegistrations();
56+
return repo;
57+
}
58+
59+
60+
@Bean
61+
DynamicClientRegistrationRepository.RegistrationRestTemplate registrationRestTemplate(RestTemplateBuilder restTemplateBuilder) {
62+
return restTemplateBuilder.build(DynamicClientRegistrationRepository.RegistrationRestTemplate.class);
63+
}
64+
65+
66+
// As of Spring Boot 3.2, we could use a record instead of a class.
67+
@ConfigurationProperties(prefix = "baeldung.security.client.registration")
68+
@Getter
69+
@Setter
70+
public static final class RegistrationProperties {
71+
URI registrationEndpoint;
72+
String registrationUsername;
73+
String registrationPassword;
74+
List<String> registrationScopes;
75+
List<String> grantTypes;
76+
List<String> redirectUris;
77+
URI tokenEndpoint;
78+
}
79+
80+
@Bean
81+
public OAuth2AuthorizationRequestResolver pkceResolver(ClientRegistrationRepository repo) {
82+
var resolver = new DefaultOAuth2AuthorizationRequestResolver(repo, "/oauth2/authorization");
83+
resolver.setAuthorizationRequestCustomizer(OAuth2AuthorizationRequestCustomizers.withPkce());
84+
return resolver;
85+
}
86+
87+
@Bean
88+
SecurityFilterChain oauth2SecurityFilterChain(HttpSecurity http, OAuth2AuthorizationRequestResolver resolver) throws Exception {
89+
http.authorizeHttpRequests((requests) -> {
90+
requests.anyRequest().authenticated();
91+
});
92+
http.oauth2Login(a -> a.authorizationEndpoint(c -> c.authorizationRequestResolver(resolver))) ;
93+
http.oauth2Client(Customizer.withDefaults());
94+
return http.build();
95+
}
96+
97+
98+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package com.baeldung.spring.security.dynreg.client.controller;
2+
3+
import org.springframework.security.core.Authentication;
4+
import org.springframework.security.core.annotation.AuthenticationPrincipal;
5+
import org.springframework.security.oauth2.core.user.OAuth2User;
6+
import org.springframework.stereotype.Controller;
7+
import org.springframework.ui.Model;
8+
import org.springframework.web.bind.annotation.GetMapping;
9+
10+
import java.security.Principal;
11+
12+
@Controller
13+
public class HomeController {
14+
15+
@GetMapping(path = {"/", "/index"} )
16+
String index( Authentication user, Model model) {
17+
model.addAttribute("user", user);
18+
return "index";
19+
}
20+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
package com.baeldung.spring.security.dynreg.client.service;
2+
3+
import com.fasterxml.jackson.databind.node.ObjectNode;
4+
import lombok.RequiredArgsConstructor;
5+
import lombok.extern.slf4j.Slf4j;
6+
import org.springframework.http.HttpHeaders;
7+
import org.springframework.http.HttpMethod;
8+
import org.springframework.http.MediaType;
9+
import org.springframework.http.RequestEntity;
10+
import org.springframework.security.oauth2.client.registration.ClientRegistration;
11+
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
12+
import org.springframework.util.Assert;
13+
import org.springframework.util.LinkedMultiValueMap;
14+
import org.springframework.web.client.RestTemplate;
15+
import org.springframework.web.util.UriComponentsBuilder;
16+
17+
import java.net.URI;
18+
import java.util.HashMap;
19+
import java.util.Iterator;
20+
import java.util.List;
21+
import java.util.Map;
22+
23+
@RequiredArgsConstructor
24+
@Slf4j
25+
public class DynamicClientRegistrationRepository implements ClientRegistrationRepository, Iterable<ClientRegistration> {
26+
27+
private final RegistrationDetails registrationDetails;
28+
private final Map<String, ClientRegistration> staticClients;
29+
private final RegistrationRestTemplate registrationClient;
30+
private final Map<String, ClientRegistration> registrations = new HashMap<>();
31+
32+
@Override
33+
public ClientRegistration findByRegistrationId(String registrationId) {
34+
log.info("findByRegistrationId: {}", registrationId);
35+
return registrations.computeIfAbsent(registrationId, this::doRegistration);
36+
}
37+
38+
private ClientRegistration doRegistration(String registrationId) {
39+
40+
log.info("doRegistration: registrationId={}", registrationId);
41+
String token = createRegistrationToken();
42+
43+
var staticRegistration = staticClients.get(registrationId);
44+
Assert.notNull(staticRegistration,"Invalid registrationId: " + registrationId);
45+
46+
var body = Map.of(
47+
"client_name", staticRegistration.getClientName(),
48+
"grant_types", List.of(staticRegistration.getAuthorizationGrantType()),
49+
"scope", String.join(" ", staticRegistration.getScopes()),
50+
"redirect_uris", List.of(resolveCallbackUri(staticRegistration)));
51+
52+
var headers = new HttpHeaders();
53+
headers.setBearerAuth(token);
54+
headers.setContentType(MediaType.APPLICATION_JSON);
55+
56+
var request = new RequestEntity<>(
57+
body,
58+
headers,
59+
HttpMethod.POST,
60+
registrationDetails.registrationEndpoint());
61+
62+
var response = registrationClient.exchange(request, ObjectNode.class);
63+
64+
if ( response.getBody() == null ) {
65+
throw new RuntimeException("Invalid registration response");
66+
}
67+
68+
return createClientRegistration(staticRegistration, response.getBody());
69+
70+
}
71+
72+
private String resolveCallbackUri(ClientRegistration registration) {
73+
74+
var path = UriComponentsBuilder.fromUriString(registration.getRedirectUri())
75+
.build(Map.of(
76+
"baseUrl", "",
77+
"action", "login",
78+
"registrationId", registration.getRegistrationId()))
79+
.toString();
80+
81+
return "http://localhost:8090" + path;
82+
}
83+
84+
private ClientRegistration createClientRegistration(ClientRegistration staticRegistration, ObjectNode body) {
85+
86+
var clientId = body.get("client_id").asText();
87+
var clientSecret = body.get("client_secret").asText();
88+
89+
log.info("creating ClientRegistration: registrationId={}, client_id={}", staticRegistration.getRegistrationId(),clientId);
90+
91+
return ClientRegistration.withClientRegistration(staticRegistration)
92+
.clientId(body.get("client_id").asText())
93+
.clientSecret(body.get("client_secret").asText())
94+
.build();
95+
96+
}
97+
98+
private String createRegistrationToken() {
99+
100+
var body = new LinkedMultiValueMap<String,String>();
101+
body.put( "grant_type", List.of("client_credentials"));
102+
body.put( "scope", registrationDetails.registrationScopes());
103+
104+
var headers = new HttpHeaders();
105+
headers.setBasicAuth(registrationDetails.registrationUsername(), registrationDetails.registrationPassword());
106+
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
107+
108+
var request = new RequestEntity<>(
109+
body,
110+
headers,
111+
HttpMethod.POST,
112+
registrationDetails.tokenEndpoint());
113+
114+
var result = registrationClient.exchange(request, ObjectNode.class);
115+
if ( !result.getStatusCode().is2xxSuccessful()) {
116+
throw new RuntimeException("Failed to create registration token: code=" + result.getStatusCode());
117+
}
118+
119+
return result.getBody().get("access_token").asText();
120+
}
121+
122+
/**
123+
* Returns an iterator over elements of type {@code T}.
124+
*
125+
* @return an Iterator.
126+
*/
127+
@Override
128+
public Iterator<ClientRegistration> iterator() {
129+
return registrations
130+
.values()
131+
.iterator();
132+
}
133+
134+
public void doRegistrations() {
135+
staticClients.forEach((key, value) -> findByRegistrationId(key));
136+
}
137+
138+
public record RegistrationDetails(
139+
URI registrationEndpoint,
140+
String registrationUsername,
141+
String registrationPassword,
142+
List<String> registrationScopes,
143+
List<String> grantTypes,
144+
List<String> redirectUris,
145+
URI tokenEndpoint
146+
) {
147+
}
148+
149+
// Type-safe RestTemplate
150+
public static class RegistrationRestTemplate extends RestTemplate {
151+
}
152+
}

0 commit comments

Comments
 (0)