Polish gh-1320

This commit is contained in:
Joe Grandja
2023-09-13 19:38:41 -04:00
parent 3386b1e8a2
commit 3eb951f59d
6 changed files with 149 additions and 208 deletions

View File

@@ -1,96 +0,0 @@
/*
* Copyright 2020-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package sample.dcr;
import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.RSAKey;
import com.nimbusds.jose.jwk.source.ImmutableJWKSet;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.proc.SecurityContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.server.authorization.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration;
import org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2AuthorizationServerConfigurer;
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.util.Collections;
import java.util.UUID;
@Configuration
@EnableWebSecurity
public class DcrConfiguration {
@Bean // <1>
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
http.getConfigurer(OAuth2AuthorizationServerConfigurer.class)
.oidc(oidc -> oidc.clientRegistrationEndpoint(Customizer.withDefaults())); // <2>
http.oauth2ResourceServer(oauth2ResourceServer ->
oauth2ResourceServer.jwt(Customizer.withDefaults()));
return http.build();
}
// @fold:on
@Bean
public UserDetailsService userDetailsService() {
// This example uses client credentials grant type - no need for any users.
return new InMemoryUserDetailsManager(Collections.emptyList());
}
@Bean
public JWKSource<SecurityContext> jwkSource() {
// @formatter:off
KeyPair keyPair;
try {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
keyPairGenerator.initialize(2048);
keyPair = keyPairGenerator.generateKeyPair();
} catch (Exception ex) {
throw new IllegalStateException(ex);
}
RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
RSAKey rsaKey = new RSAKey.Builder(publicKey)
.privateKey(privateKey)
.keyID(UUID.randomUUID().toString())
.build();
// @formatter:on
JWKSet jwkSet = new JWKSet(rsaKey);
return new ImmutableJWKSet<>(jwkSet);
}
@Bean
public JwtDecoder jwtDecoder(JWKSource<SecurityContext> jwkSource) {
return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource);
}
@Bean
public AuthorizationServerSettings authorizationServerSettings() {
return AuthorizationServerSettings.builder().build();
}
// @fold:off
}

View File

@@ -13,7 +13,9 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package sample.dcr;
package sample.registration;
import java.util.UUID;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@@ -23,21 +25,21 @@ import org.springframework.security.oauth2.server.authorization.client.InMemoryR
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
import java.util.UUID;
@Configuration
public class RegisteredClientConfiguration {
@Bean // <1>
public class ClientConfig {
@Bean
public RegisteredClientRepository registeredClientRepository() {
RegisteredClient initialClient = RegisteredClient.withId(UUID.randomUUID().toString())
.clientId("dcr-client") // <2>
RegisteredClient registrarClient = RegisteredClient.withId(UUID.randomUUID().toString())
.clientId("registrar-client")
.clientSecret("{noop}secret")
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS) // <3>
.scope("client.create") // <4>
.scope("client.read") // <5>
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS) // <1>
.scope("client.create") // <2>
.scope("client.read") // <3>
.build();
return new InMemoryRegisteredClientRepository(initialClient); // <6>
return new InMemoryRegisteredClientRepository(registrarClient);
}
}

View File

@@ -13,56 +13,58 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package sample.dcr;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;
package sample.registration;
import java.util.List;
import java.util.Objects;
public class DcrClient {
import com.fasterxml.jackson.annotation.JsonProperty;
import reactor.core.publisher.Mono;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.web.reactive.function.client.WebClient;
public class ClientRegistrar {
// @fold:on
private final WebClient webClient;
public DcrClient(final WebClient webClient) {
public ClientRegistrar(WebClient webClient) {
this.webClient = webClient;
}
// @fold:off
public record DcrRequest( // <1>
public record ClientRegistrationRequest( // <1>
@JsonProperty("client_name") String clientName,
@JsonProperty("grant_types") List<String> grantTypes,
@JsonProperty("redirect_uris") List<String> redirectUris,
String scope) {
}
public record DcrResponse( // <2>
public record ClientRegistrationResponse( // <2>
@JsonProperty("registration_access_token") String registrationAccessToken,
@JsonProperty("registration_client_uri") String registrationClientUri,
@JsonProperty("client_name") String clientName,
@JsonProperty("client_id") String clientId,
@JsonProperty("client_secret") String clientSecret,
@JsonProperty("grant_types") List<String> grantTypes,
@JsonProperty("redirect_uris") List<String> redirectUris,
String scope) {
}
public static final DcrRequest SAMPLE_CLIENT_REGISTRATION_REQUEST = new DcrRequest( // <3>
"client-1",
List.of(AuthorizationGrantType.AUTHORIZATION_CODE.getValue()),
List.of("https://client.example.org/callback", "https://client.example.org/callback2"),
"openid email profile"
);
public void exampleRegistration(String initialAccessToken) { // <3>
ClientRegistrationRequest clientRegistrationRequest = new ClientRegistrationRequest( // <4>
"client-1",
List.of(AuthorizationGrantType.AUTHORIZATION_CODE.getValue()),
List.of("https://client.example.org/callback", "https://client.example.org/callback2"),
"openid email profile"
);
public void exampleRegistration(String initialAccessToken) { // <4>
DcrResponse clientRegistrationResponse =
this.registerClient(initialAccessToken, SAMPLE_CLIENT_REGISTRATION_REQUEST); // <5>
ClientRegistrationResponse clientRegistrationResponse =
registerClient(initialAccessToken, clientRegistrationRequest); // <5>
assert (clientRegistrationResponse.clientName().contentEquals("client-1")); // <6>
assert (clientRegistrationResponse.clientName().contentEquals("client-1")); // <6>
assert (!Objects.isNull(clientRegistrationResponse.clientSecret()));
assert (clientRegistrationResponse.scope().contentEquals("openid profile email"));
assert (clientRegistrationResponse.grantTypes().contains(AuthorizationGrantType.AUTHORIZATION_CODE.getValue()));
@@ -71,12 +73,13 @@ public class DcrClient {
assert (!clientRegistrationResponse.registrationAccessToken().isEmpty());
assert (!clientRegistrationResponse.registrationClientUri().isEmpty());
String registrationAccessToken = clientRegistrationResponse.registrationAccessToken(); // <7>
String registrationAccessToken = clientRegistrationResponse.registrationAccessToken(); // <7>
String registrationClientUri = clientRegistrationResponse.registrationClientUri();
DcrResponse retrievedClient = this.retrieveClient(registrationAccessToken, registrationClientUri); // <8>
ClientRegistrationResponse retrievedClient = retrieveClient(registrationAccessToken, registrationClientUri); // <8>
assert (retrievedClient.clientName().contentEquals("client-1")); // <9>
assert (retrievedClient.clientName().contentEquals("client-1")); // <9>
assert (!Objects.isNull(retrievedClient.clientId()));
assert (!Objects.isNull(retrievedClient.clientSecret()));
assert (retrievedClient.scope().contentEquals("openid profile email"));
assert (retrievedClient.grantTypes().contains(AuthorizationGrantType.AUTHORIZATION_CODE.getValue()));
@@ -86,26 +89,27 @@ public class DcrClient {
assert (!retrievedClient.registrationClientUri().isEmpty());
}
public DcrResponse registerClient(String initialAccessToken, DcrRequest request) { // <10>
public ClientRegistrationResponse registerClient(String initialAccessToken, ClientRegistrationRequest request) { // <10>
return this.webClient
.post()
.uri("/connect/register")
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaType.APPLICATION_JSON)
.header(HttpHeaders.AUTHORIZATION, "Bearer %s".formatted(initialAccessToken))
.body(Mono.just(request), DcrRequest.class)
.body(Mono.just(request), ClientRegistrationRequest.class)
.retrieve()
.bodyToMono(DcrResponse.class)
.bodyToMono(ClientRegistrationResponse.class)
.block();
}
public DcrResponse retrieveClient(String registrationAccessToken, String registrationClientUri) { // <11>
public ClientRegistrationResponse retrieveClient(String registrationAccessToken, String registrationClientUri) { // <11>
return this.webClient
.get()
.uri(registrationClientUri)
.header(HttpHeaders.AUTHORIZATION, "Bearer %s".formatted(registrationAccessToken))
.retrieve()
.bodyToMono(DcrResponse.class)
.bodyToMono(ClientRegistrationResponse.class)
.block();
}
}

View File

@@ -0,0 +1,42 @@
/*
* Copyright 2020-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package sample.registration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.oauth2.server.authorization.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration;
import org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2AuthorizationServerConfigurer;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
http.getConfigurer(OAuth2AuthorizationServerConfigurer.class)
.oidc(oidc -> oidc.clientRegistrationEndpoint(Customizer.withDefaults())); // <1>
http.oauth2ResourceServer(oauth2ResourceServer ->
oauth2ResourceServer.jwt(Customizer.withDefaults()));
return http.build();
}
}

View File

@@ -13,19 +13,22 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package sample.dcr;
package sample.registration;
import com.jayway.jsonpath.JsonPath;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.server.LocalServerPort;
import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.http.MediaType;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.web.reactive.function.client.WebClient;
@@ -34,9 +37,8 @@ import static org.springframework.test.web.servlet.request.MockMvcRequestBuilder
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
/**
* Tests for Dynamic Client Registration how-to guide
* Tests for Dynamic Client Registration how-to guide.
*
* @author Dmitriy Dubson
*/
@@ -54,12 +56,12 @@ public class DynamicClientRegistrationTests {
private String port;
@Test
public void dynamicallyRegisterAClient() throws Exception {
String tokenRequestBody = "scope=client.create&grant_type=client_credentials" ;
public void dynamicallyRegisterClient() throws Exception {
MockHttpServletResponse tokenResponse = this.mvc.perform(post("/oauth2/token")
.with(httpBasic("dcr-client", "secret"))
.contentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE)
.content(tokenRequestBody))
.with(httpBasic("registrar-client", "secret"))
.param(OAuth2ParameterNames.GRANT_TYPE, AuthorizationGrantType.CLIENT_CREDENTIALS.getValue())
.param(OAuth2ParameterNames.SCOPE, "client.create")
.contentType(MediaType.APPLICATION_FORM_URLENCODED_VALUE))
.andExpect(status().isOk())
.andExpect(jsonPath("$.access_token").isNotEmpty())
.andReturn()
@@ -67,15 +69,16 @@ public class DynamicClientRegistrationTests {
String initialAccessToken = JsonPath.parse(tokenResponse.getContentAsString()).read("$.access_token");
WebClient webClient = WebClient.builder().baseUrl("http://127.0.0.1:%s".formatted(port)).build();
DcrClient dcrClient = new DcrClient(webClient);
WebClient webClient = WebClient.builder().baseUrl("http://127.0.0.1:%s".formatted(this.port)).build();
ClientRegistrar clientRegistrar = new ClientRegistrar(webClient);
dcrClient.exampleRegistration(initialAccessToken);
clientRegistrar.exampleRegistration(initialAccessToken);
}
@EnableAutoConfiguration
@EnableWebSecurity
@Import({DcrConfiguration.class, RegisteredClientConfiguration.class})
@ComponentScan
static class AuthorizationServerConfig {
}
}