Skip to content

Commit 85c42f4

Browse files
author
Steve Riesenberg
committed
Add overview, getting help, and getting started
Closes spring-projectsgh-667 Closes spring-projectsgh-668 Closes spring-projectsgh-669
1 parent 51de75b commit 85c42f4

File tree

5 files changed

+486
-15
lines changed

5 files changed

+486
-15
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
/*
2+
* Copyright 2020-2022 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package sample.gettingStarted;
17+
18+
import java.security.KeyPair;
19+
import java.security.KeyPairGenerator;
20+
import java.security.interfaces.RSAPrivateKey;
21+
import java.security.interfaces.RSAPublicKey;
22+
import java.util.UUID;
23+
24+
import com.nimbusds.jose.jwk.JWKSet;
25+
import com.nimbusds.jose.jwk.RSAKey;
26+
import com.nimbusds.jose.jwk.source.ImmutableJWKSet;
27+
import com.nimbusds.jose.jwk.source.JWKSource;
28+
import com.nimbusds.jose.proc.SecurityContext;
29+
30+
import org.springframework.context.annotation.Bean;
31+
import org.springframework.context.annotation.Configuration;
32+
import org.springframework.core.annotation.Order;
33+
import org.springframework.security.config.Customizer;
34+
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
35+
import org.springframework.security.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration;
36+
import org.springframework.security.core.userdetails.User;
37+
import org.springframework.security.core.userdetails.UserDetails;
38+
import org.springframework.security.core.userdetails.UserDetailsService;
39+
import org.springframework.security.oauth2.core.AuthorizationGrantType;
40+
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
41+
import org.springframework.security.oauth2.core.oidc.OidcScopes;
42+
import org.springframework.security.oauth2.server.authorization.client.InMemoryRegisteredClientRepository;
43+
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
44+
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
45+
import org.springframework.security.oauth2.server.authorization.config.ClientSettings;
46+
import org.springframework.security.oauth2.server.authorization.config.ProviderSettings;
47+
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
48+
import org.springframework.security.web.SecurityFilterChain;
49+
import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint;
50+
51+
@Configuration
52+
public class SecurityConfig {
53+
54+
@Bean // <1>
55+
@Order(1)
56+
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
57+
OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
58+
// @formatter:off
59+
http
60+
.exceptionHandling((exceptions) -> exceptions
61+
.authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("/login"))
62+
);
63+
// @formatter:on
64+
65+
return http.build();
66+
}
67+
68+
@Bean // <2>
69+
@Order(2)
70+
public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
71+
// @formatter:off
72+
http
73+
.authorizeHttpRequests((authorize) -> authorize
74+
.anyRequest().authenticated()
75+
)
76+
.formLogin(Customizer.withDefaults());
77+
// @formatter:on
78+
79+
return http.build();
80+
}
81+
82+
@Bean // <3>
83+
public UserDetailsService userDetailsService() {
84+
// @formatter:off
85+
UserDetails userDetails = User.withDefaultPasswordEncoder()
86+
.username("user")
87+
.password("password")
88+
.roles("USER")
89+
.build();
90+
// @formatter:on
91+
92+
return new InMemoryUserDetailsManager(userDetails);
93+
}
94+
95+
@Bean // <4>
96+
public RegisteredClientRepository registeredClientRepository() {
97+
// @formatter:off
98+
RegisteredClient registeredClient = RegisteredClient.withId(UUID.randomUUID().toString())
99+
.clientId("messaging-client")
100+
.clientSecret("{noop}secret")
101+
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
102+
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
103+
.authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
104+
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
105+
.redirectUri("http://127.0.0.1:8080/login/oauth2/code/messaging-client-oidc")
106+
.redirectUri("http://127.0.0.1:8080/authorized")
107+
.scope(OidcScopes.OPENID)
108+
.scope("message.read")
109+
.scope("message.write")
110+
.clientSettings(ClientSettings.builder().requireAuthorizationConsent(true).build())
111+
.build();
112+
// @formatter:on
113+
114+
return new InMemoryRegisteredClientRepository(registeredClient);
115+
}
116+
117+
@Bean // <5>
118+
public JWKSource<SecurityContext> jwkSource() {
119+
KeyPair keyPair = generateRsaKey();
120+
RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
121+
RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
122+
// @formatter:off
123+
RSAKey rsaKey = new RSAKey.Builder(publicKey)
124+
.privateKey(privateKey)
125+
.keyID(UUID.randomUUID().toString())
126+
.build();
127+
// @formatter:on
128+
JWKSet jwkSet = new JWKSet(rsaKey);
129+
return new ImmutableJWKSet<>(jwkSet);
130+
}
131+
132+
private static KeyPair generateRsaKey() { // <6>
133+
KeyPair keyPair;
134+
try {
135+
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
136+
keyPairGenerator.initialize(2048);
137+
keyPair = keyPairGenerator.generateKeyPair();
138+
}
139+
catch (Exception ex) {
140+
throw new IllegalStateException(ex);
141+
}
142+
return keyPair;
143+
}
144+
145+
@Bean // <7>
146+
public ProviderSettings providerSettings() {
147+
return ProviderSettings.builder().build();
148+
}
149+
150+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
/*
2+
* Copyright 2020-2022 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package sample.gettingStarted;
17+
18+
import java.net.URLDecoder;
19+
import java.nio.charset.StandardCharsets;
20+
import java.util.Map;
21+
import java.util.regex.Matcher;
22+
import java.util.regex.Pattern;
23+
24+
import com.fasterxml.jackson.core.type.TypeReference;
25+
import com.fasterxml.jackson.databind.ObjectMapper;
26+
import org.assertj.core.api.ObjectAssert;
27+
import org.junit.jupiter.api.Test;
28+
import org.junit.jupiter.api.extension.ExtendWith;
29+
import sample.test.SpringTestContext;
30+
import sample.test.SpringTestContextExtension;
31+
32+
import org.springframework.beans.factory.annotation.Autowired;
33+
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
34+
import org.springframework.context.annotation.Bean;
35+
import org.springframework.context.annotation.ComponentScan;
36+
import org.springframework.context.annotation.Import;
37+
import org.springframework.http.HttpHeaders;
38+
import org.springframework.http.MediaType;
39+
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
40+
import org.springframework.security.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration;
41+
import org.springframework.security.oauth2.core.AuthorizationGrantType;
42+
import org.springframework.security.oauth2.core.OAuth2TokenType;
43+
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponseType;
44+
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
45+
import org.springframework.security.oauth2.core.oidc.endpoint.OidcParameterNames;
46+
import org.springframework.security.oauth2.server.authorization.InMemoryOAuth2AuthorizationConsentService;
47+
import org.springframework.security.oauth2.server.authorization.InMemoryOAuth2AuthorizationService;
48+
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
49+
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsent;
50+
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsentService;
51+
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
52+
import org.springframework.security.oauth2.server.authorization.client.InMemoryRegisteredClientRepository;
53+
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
54+
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
55+
import org.springframework.test.web.servlet.MockMvc;
56+
import org.springframework.test.web.servlet.MvcResult;
57+
import org.springframework.util.LinkedMultiValueMap;
58+
import org.springframework.util.MultiValueMap;
59+
import org.springframework.util.StringUtils;
60+
import org.springframework.web.util.UriComponents;
61+
import org.springframework.web.util.UriComponentsBuilder;
62+
63+
import static org.assertj.core.api.Assertions.assertThat;
64+
import static org.hamcrest.Matchers.containsString;
65+
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user;
66+
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
67+
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
68+
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
69+
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
70+
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
71+
72+
/**
73+
* Tests for the Getting Started section of the reference documentation.
74+
*
75+
* @author Steve Riesenberg
76+
*/
77+
@ExtendWith(SpringTestContextExtension.class)
78+
public class SecurityConfigTests {
79+
private static final Pattern HIDDEN_STATE_INPUT_PATTERN = Pattern.compile(".+<input type=\"hidden\" name=\"state\" value=\"([^\"]+)\">.+");
80+
private static final TypeReference<Map<String, Object>> TOKEN_RESPONSE_TYPE_REFERENCE = new TypeReference<Map<String, Object>>() {
81+
};
82+
83+
public final SpringTestContext spring = new SpringTestContext(this);
84+
85+
@Autowired
86+
private MockMvc mockMvc;
87+
88+
@Autowired
89+
private RegisteredClientRepository registeredClientRepository;
90+
91+
@Autowired
92+
private OAuth2AuthorizationService authorizationService;
93+
94+
@Autowired
95+
private OAuth2AuthorizationConsentService authorizationConsentService;
96+
97+
@Test
98+
public void oidcLoginWhenGettingStartedConfigUsedThenSuccess() throws Exception {
99+
this.spring.register(AuthorizationServerConfig.class).autowire();
100+
assertThat(this.registeredClientRepository).isInstanceOf(InMemoryRegisteredClientRepository.class);
101+
assertThat(this.authorizationService).isInstanceOf(InMemoryOAuth2AuthorizationService.class);
102+
assertThat(this.authorizationConsentService).isInstanceOf(InMemoryOAuth2AuthorizationConsentService.class);
103+
104+
RegisteredClient registeredClient = this.registeredClientRepository.findByClientId("messaging-client");
105+
assertThat(registeredClient).isNotNull();
106+
107+
String state = performAuthorizationCodeRequest(registeredClient);
108+
assertThatAuthorization(state, OAuth2ParameterNames.STATE).isNotNull();
109+
assertThatAuthorization(state, null).isNotNull();
110+
111+
String authorizationCode = performAuthorizationConsentRequest(registeredClient, state);
112+
assertThatAuthorization(authorizationCode, OAuth2ParameterNames.CODE).isNotNull();
113+
assertThatAuthorization(authorizationCode, null).isNotNull();
114+
115+
Map<String, Object> tokenResponse = performTokenRequest(registeredClient, authorizationCode);
116+
String accessToken = (String) tokenResponse.get(OAuth2ParameterNames.ACCESS_TOKEN);
117+
assertThatAuthorization(accessToken, OAuth2ParameterNames.ACCESS_TOKEN).isNotNull();
118+
assertThatAuthorization(accessToken, null).isNotNull();
119+
120+
String refreshToken = (String) tokenResponse.get(OAuth2ParameterNames.REFRESH_TOKEN);
121+
assertThatAuthorization(refreshToken, OAuth2ParameterNames.REFRESH_TOKEN).isNotNull();
122+
assertThatAuthorization(refreshToken, null).isNotNull();
123+
124+
String idToken = (String) tokenResponse.get(OidcParameterNames.ID_TOKEN);
125+
assertThatAuthorization(idToken, OidcParameterNames.ID_TOKEN).isNull(); // id_token is not searchable
126+
127+
OAuth2Authorization authorization = findAuthorization(accessToken, OAuth2ParameterNames.ACCESS_TOKEN);
128+
assertThat(authorization.getToken(idToken)).isNotNull();
129+
130+
String scopes = (String) tokenResponse.get(OAuth2ParameterNames.SCOPE);
131+
OAuth2AuthorizationConsent authorizationConsent = this.authorizationConsentService.findById(
132+
registeredClient.getId(), "user");
133+
assertThat(authorizationConsent).isNotNull();
134+
assertThat(authorizationConsent.getScopes()).containsExactlyInAnyOrder(
135+
StringUtils.delimitedListToStringArray(scopes, " "));
136+
}
137+
138+
private ObjectAssert<OAuth2Authorization> assertThatAuthorization(String token, String tokenType) {
139+
return assertThat(findAuthorization(token, tokenType));
140+
}
141+
142+
private OAuth2Authorization findAuthorization(String token, String tokenType) {
143+
return this.authorizationService.findByToken(token, tokenType == null ? null : new OAuth2TokenType(tokenType));
144+
}
145+
146+
private String performAuthorizationCodeRequest(RegisteredClient registeredClient) throws Exception {
147+
MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>();
148+
parameters.set(OAuth2ParameterNames.RESPONSE_TYPE, OAuth2AuthorizationResponseType.CODE.getValue());
149+
parameters.set(OAuth2ParameterNames.CLIENT_ID, registeredClient.getClientId());
150+
parameters.set(OAuth2ParameterNames.REDIRECT_URI, registeredClient.getRedirectUris().iterator().next());
151+
parameters.set(OAuth2ParameterNames.SCOPE,
152+
StringUtils.collectionToDelimitedString(registeredClient.getScopes(), " "));
153+
parameters.set(OAuth2ParameterNames.STATE, "state");
154+
155+
MvcResult mvcResult = this.mockMvc.perform(get("/oauth2/authorize")
156+
.params(parameters)
157+
.with(user("user").roles("USER")))
158+
.andExpect(status().isOk())
159+
.andExpect(header().string("content-type", containsString(MediaType.TEXT_HTML_VALUE)))
160+
.andReturn();
161+
String responseHtml = mvcResult.getResponse().getContentAsString();
162+
Matcher matcher = HIDDEN_STATE_INPUT_PATTERN.matcher(responseHtml);
163+
164+
return matcher.matches() ? matcher.group(1) : null;
165+
}
166+
167+
private String performAuthorizationConsentRequest(RegisteredClient registeredClient, String state) throws Exception {
168+
MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>();
169+
parameters.set(OAuth2ParameterNames.CLIENT_ID, registeredClient.getClientId());
170+
parameters.set(OAuth2ParameterNames.STATE, state);
171+
parameters.add(OAuth2ParameterNames.SCOPE, "message.read");
172+
parameters.add(OAuth2ParameterNames.SCOPE, "message.write");
173+
174+
MvcResult mvcResult = this.mockMvc.perform(post("/oauth2/authorize")
175+
.params(parameters)
176+
.with(user("user").roles("USER")))
177+
.andExpect(status().is3xxRedirection())
178+
.andReturn();
179+
String redirectedUrl = mvcResult.getResponse().getRedirectedUrl();
180+
assertThat(redirectedUrl).isNotNull();
181+
assertThat(redirectedUrl).matches("http://127.0.0.1:8080/authorized\\?code=.{15,}&state=state");
182+
183+
String locationHeader = URLDecoder.decode(redirectedUrl, StandardCharsets.UTF_8.name());
184+
UriComponents uriComponents = UriComponentsBuilder.fromUriString(locationHeader).build();
185+
186+
return uriComponents.getQueryParams().getFirst("code");
187+
}
188+
189+
private Map<String, Object> performTokenRequest(RegisteredClient registeredClient, String authorizationCode) throws Exception {
190+
MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>();
191+
parameters.set(OAuth2ParameterNames.GRANT_TYPE, AuthorizationGrantType.AUTHORIZATION_CODE.getValue());
192+
parameters.set(OAuth2ParameterNames.CODE, authorizationCode);
193+
parameters.set(OAuth2ParameterNames.REDIRECT_URI, registeredClient.getRedirectUris().iterator().next());
194+
195+
HttpHeaders basicAuth = new HttpHeaders();
196+
basicAuth.setBasicAuth(registeredClient.getClientId(), "secret");
197+
198+
MvcResult mvcResult = this.mockMvc.perform(post("/oauth2/token")
199+
.params(parameters)
200+
.headers(basicAuth))
201+
.andExpect(status().isOk())
202+
.andExpect(header().string(HttpHeaders.CONTENT_TYPE, containsString(MediaType.APPLICATION_JSON_VALUE)))
203+
.andExpect(jsonPath("$.access_token").isNotEmpty())
204+
.andExpect(jsonPath("$.token_type").isNotEmpty())
205+
.andExpect(jsonPath("$.expires_in").isNotEmpty())
206+
.andExpect(jsonPath("$.refresh_token").isNotEmpty())
207+
.andExpect(jsonPath("$.scope").isNotEmpty())
208+
.andExpect(jsonPath("$.id_token").isNotEmpty())
209+
.andReturn();
210+
211+
ObjectMapper objectMapper = new ObjectMapper();
212+
String responseJson = mvcResult.getResponse().getContentAsString();
213+
return objectMapper.readValue(responseJson, TOKEN_RESPONSE_TYPE_REFERENCE);
214+
}
215+
216+
@EnableWebSecurity
217+
@EnableAutoConfiguration
218+
@ComponentScan
219+
@Import(OAuth2AuthorizationServerConfiguration.class)
220+
static class AuthorizationServerConfig extends SecurityConfig {
221+
222+
@Bean
223+
public OAuth2AuthorizationService authorizationService() {
224+
return new InMemoryOAuth2AuthorizationService();
225+
}
226+
227+
@Bean
228+
public OAuth2AuthorizationConsentService authorizationConsentService() {
229+
return new InMemoryOAuth2AuthorizationConsentService();
230+
}
231+
232+
}
233+
234+
}

0 commit comments

Comments
 (0)