Migrate docs to Antora
Issue gh-1295
This commit is contained in:
committed by
Steve Riesenberg
parent
ec3d4fa7e1
commit
0f0424ad2a
228
docs/src/test/java/sample/AuthorizationCodeGrantFlow.java
Normal file
228
docs/src/test/java/sample/AuthorizationCodeGrantFlow.java
Normal file
@@ -0,0 +1,228 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
import java.net.URLDecoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationResponseType;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||
import org.springframework.security.oauth2.core.endpoint.PkceParameterNames;
|
||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.MvcResult;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.util.UriComponents;
|
||||
import org.springframework.web.util.UriComponentsBuilder;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.hamcrest.Matchers.containsString;
|
||||
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
/**
|
||||
* Helper class that performs steps of the {@code authorization_code} flow using
|
||||
* {@link MockMvc} for testing.
|
||||
*
|
||||
* @author Steve Riesenberg
|
||||
*/
|
||||
public class AuthorizationCodeGrantFlow {
|
||||
private static final Pattern HIDDEN_STATE_INPUT_PATTERN = Pattern.compile(".+<input type=\"hidden\" name=\"state\" value=\"([^\"]+)\">.+");
|
||||
private static final TypeReference<Map<String, Object>> TOKEN_RESPONSE_TYPE_REFERENCE = new TypeReference<Map<String, Object>>() {
|
||||
};
|
||||
|
||||
private final MockMvc mockMvc;
|
||||
|
||||
private String username = "user";
|
||||
|
||||
private Set<String> scopes = new HashSet<>();
|
||||
|
||||
public AuthorizationCodeGrantFlow(MockMvc mockMvc) {
|
||||
this.mockMvc = mockMvc;
|
||||
}
|
||||
|
||||
public void setUsername(String username) {
|
||||
this.username = username;
|
||||
}
|
||||
|
||||
public void addScope(String scope) {
|
||||
this.scopes.add(scope);
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform the authorization request and obtain a state parameter.
|
||||
*
|
||||
* @param registeredClient The registered client
|
||||
* @return The state parameter for submitting consent for authorization
|
||||
*/
|
||||
public String authorize(RegisteredClient registeredClient) throws Exception {
|
||||
return authorize(registeredClient, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform the authorization request and obtain a state parameter.
|
||||
*
|
||||
* @param registeredClient The registered client
|
||||
* @param additionalParameters Additional parameters for the request
|
||||
* @return The state parameter for submitting consent for authorization
|
||||
*/
|
||||
public String authorize(RegisteredClient registeredClient, MultiValueMap<String, String> additionalParameters) throws Exception {
|
||||
MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>();
|
||||
parameters.set(OAuth2ParameterNames.RESPONSE_TYPE, OAuth2AuthorizationResponseType.CODE.getValue());
|
||||
parameters.set(OAuth2ParameterNames.CLIENT_ID, registeredClient.getClientId());
|
||||
parameters.set(OAuth2ParameterNames.REDIRECT_URI, registeredClient.getRedirectUris().iterator().next());
|
||||
parameters.set(OAuth2ParameterNames.SCOPE,
|
||||
StringUtils.collectionToDelimitedString(registeredClient.getScopes(), " "));
|
||||
parameters.set(OAuth2ParameterNames.STATE, "state");
|
||||
if (additionalParameters != null) {
|
||||
parameters.addAll(additionalParameters);
|
||||
}
|
||||
|
||||
// @formatter:off
|
||||
MvcResult mvcResult = this.mockMvc.perform(get("/oauth2/authorize")
|
||||
.params(parameters)
|
||||
.with(user(this.username).roles("USER")))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(header().string("content-type", containsString(MediaType.TEXT_HTML_VALUE)))
|
||||
.andReturn();
|
||||
// @formatter:on
|
||||
String responseHtml = mvcResult.getResponse().getContentAsString();
|
||||
Matcher matcher = HIDDEN_STATE_INPUT_PATTERN.matcher(responseHtml);
|
||||
|
||||
return matcher.matches() ? matcher.group(1) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Submit consent for the authorization request and obtain an authorization code.
|
||||
*
|
||||
* @param registeredClient The registered client
|
||||
* @param state The state parameter from the authorization request
|
||||
* @return An authorization code
|
||||
*/
|
||||
public String submitConsent(RegisteredClient registeredClient, String state) throws Exception {
|
||||
MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>();
|
||||
parameters.set(OAuth2ParameterNames.CLIENT_ID, registeredClient.getClientId());
|
||||
parameters.set(OAuth2ParameterNames.STATE, state);
|
||||
for (String scope : scopes) {
|
||||
parameters.add(OAuth2ParameterNames.SCOPE, scope);
|
||||
}
|
||||
|
||||
// @formatter:off
|
||||
MvcResult mvcResult = this.mockMvc.perform(post("/oauth2/authorize")
|
||||
.params(parameters)
|
||||
.with(user(this.username).roles("USER")))
|
||||
.andExpect(status().is3xxRedirection())
|
||||
.andReturn();
|
||||
// @formatter:on
|
||||
String redirectedUrl = mvcResult.getResponse().getRedirectedUrl();
|
||||
assertThat(redirectedUrl).isNotNull();
|
||||
assertThat(redirectedUrl).matches("\\S+\\?code=.{15,}&state=state");
|
||||
|
||||
String locationHeader = URLDecoder.decode(redirectedUrl, StandardCharsets.UTF_8.name());
|
||||
UriComponents uriComponents = UriComponentsBuilder.fromUriString(locationHeader).build();
|
||||
|
||||
return uriComponents.getQueryParams().getFirst("code");
|
||||
}
|
||||
|
||||
/**
|
||||
* Exchange an authorization code for an access token.
|
||||
*
|
||||
* @param registeredClient The registered client
|
||||
* @param authorizationCode The authorization code obtained from the authorization request
|
||||
* @return The token response
|
||||
*/
|
||||
public Map<String, Object> getTokenResponse(RegisteredClient registeredClient, String authorizationCode) throws Exception {
|
||||
return getTokenResponse(registeredClient, authorizationCode, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Exchange an authorization code for an access token.
|
||||
*
|
||||
* @param registeredClient The registered client
|
||||
* @param authorizationCode The authorization code obtained from the authorization request
|
||||
* @param additionalParameters Additional parameters for the request
|
||||
* @return The token response
|
||||
*/
|
||||
public Map<String, Object> getTokenResponse(RegisteredClient registeredClient, String authorizationCode, MultiValueMap<String, String> additionalParameters) throws Exception {
|
||||
MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>();
|
||||
parameters.set(OAuth2ParameterNames.CLIENT_ID, registeredClient.getClientId());
|
||||
parameters.set(OAuth2ParameterNames.GRANT_TYPE, AuthorizationGrantType.AUTHORIZATION_CODE.getValue());
|
||||
parameters.set(OAuth2ParameterNames.CODE, authorizationCode);
|
||||
parameters.set(OAuth2ParameterNames.REDIRECT_URI, registeredClient.getRedirectUris().iterator().next());
|
||||
if (additionalParameters != null) {
|
||||
parameters.addAll(additionalParameters);
|
||||
}
|
||||
|
||||
boolean publicClient = (registeredClient.getClientSecret() == null);
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
if (!publicClient) {
|
||||
headers.setBasicAuth(registeredClient.getClientId(),
|
||||
registeredClient.getClientSecret().replace("{noop}", ""));
|
||||
}
|
||||
|
||||
// @formatter:off
|
||||
MvcResult mvcResult = this.mockMvc.perform(post("/oauth2/token")
|
||||
.params(parameters)
|
||||
.headers(headers))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(header().string(HttpHeaders.CONTENT_TYPE, containsString(MediaType.APPLICATION_JSON_VALUE)))
|
||||
.andExpect(jsonPath("$.access_token").isNotEmpty())
|
||||
.andExpect(jsonPath("$.token_type").isNotEmpty())
|
||||
.andExpect(jsonPath("$.expires_in").isNotEmpty())
|
||||
.andExpect(publicClient
|
||||
? jsonPath("$.refresh_token").doesNotExist()
|
||||
: jsonPath("$.refresh_token").isNotEmpty()
|
||||
)
|
||||
.andExpect(jsonPath("$.scope").isNotEmpty())
|
||||
.andExpect(jsonPath("$.id_token").isNotEmpty())
|
||||
.andReturn();
|
||||
// @formatter:on
|
||||
|
||||
ObjectMapper objectMapper = new ObjectMapper();
|
||||
String responseJson = mvcResult.getResponse().getContentAsString();
|
||||
return objectMapper.readValue(responseJson, TOKEN_RESPONSE_TYPE_REFERENCE);
|
||||
}
|
||||
|
||||
public static MultiValueMap<String, String> withCodeChallenge() {
|
||||
MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>();
|
||||
parameters.set(PkceParameterNames.CODE_CHALLENGE, "BqZZ8pTVLsiA3t3tDOys2flJTSH7LoL3Pp5ZqM_YOnE");
|
||||
parameters.set(PkceParameterNames.CODE_CHALLENGE_METHOD, "S256");
|
||||
return parameters;
|
||||
}
|
||||
|
||||
public static MultiValueMap<String, String> withCodeVerifier() {
|
||||
MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>();
|
||||
parameters.set(PkceParameterNames.CODE_VERIFIER, "yZ6eB-lEB4BBhIzqoDPqXTTATC0Vkgov7qDF8ar2qT4");
|
||||
return parameters;
|
||||
}
|
||||
}
|
||||
188
docs/src/test/java/sample/DeviceAuthorizationGrantFlow.java
Normal file
188
docs/src/test/java/sample/DeviceAuthorizationGrantFlow.java
Normal file
@@ -0,0 +1,188 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.MvcResult;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.hamcrest.Matchers.containsString;
|
||||
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
/**
|
||||
* Helper class that performs steps of the {@code urn:ietf:params:oauth:grant-type:device_code}
|
||||
* flow using {@link MockMvc} for testing.
|
||||
*
|
||||
* @author Steve Riesenberg
|
||||
*/
|
||||
public class DeviceAuthorizationGrantFlow {
|
||||
private static final Pattern HIDDEN_STATE_INPUT_PATTERN = Pattern.compile(".+<input type=\"hidden\" name=\"state\" value=\"([^\"]+)\">.+");
|
||||
private static final TypeReference<Map<String, Object>> JSON_RESPONSE_TYPE_REFERENCE = new TypeReference<>() {
|
||||
};
|
||||
|
||||
private final MockMvc mockMvc;
|
||||
|
||||
private String username = "user";
|
||||
|
||||
private Set<String> scopes = new HashSet<>();
|
||||
|
||||
public DeviceAuthorizationGrantFlow(MockMvc mockMvc) {
|
||||
this.mockMvc = mockMvc;
|
||||
}
|
||||
|
||||
public void setUsername(String username) {
|
||||
this.username = username;
|
||||
}
|
||||
|
||||
public void addScope(String scope) {
|
||||
this.scopes.add(scope);
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform the device authorization request and obtain the response
|
||||
* containing a user code and device code.
|
||||
*
|
||||
* @param registeredClient The registered client
|
||||
* @return The device authorization response
|
||||
*/
|
||||
public Map<String, Object> authorize(RegisteredClient registeredClient) throws Exception {
|
||||
MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>();
|
||||
parameters.set(OAuth2ParameterNames.CLIENT_ID, registeredClient.getClientId());
|
||||
parameters.set(OAuth2ParameterNames.SCOPE,
|
||||
StringUtils.collectionToDelimitedString(registeredClient.getScopes(), " "));
|
||||
|
||||
HttpHeaders basicAuth = new HttpHeaders();
|
||||
basicAuth.setBasicAuth(registeredClient.getClientId(), "secret");
|
||||
|
||||
MvcResult mvcResult = this.mockMvc.perform(post("/oauth2/device_authorization")
|
||||
.params(parameters)
|
||||
.headers(basicAuth))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(header().string(HttpHeaders.CONTENT_TYPE, containsString(MediaType.APPLICATION_JSON_VALUE)))
|
||||
.andExpect(jsonPath("$.user_code").isNotEmpty())
|
||||
.andExpect(jsonPath("$.device_code").isNotEmpty())
|
||||
.andExpect(jsonPath("$.verification_uri").isNotEmpty())
|
||||
.andExpect(jsonPath("$.verification_uri_complete").isNotEmpty())
|
||||
.andExpect(jsonPath("$.expires_in").isNotEmpty())
|
||||
.andReturn();
|
||||
|
||||
ObjectMapper objectMapper = new ObjectMapper();
|
||||
String responseJson = mvcResult.getResponse().getContentAsString();
|
||||
return objectMapper.readValue(responseJson, JSON_RESPONSE_TYPE_REFERENCE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Submit the user code and obtain a state parameter from the consent screen.
|
||||
*
|
||||
* @param userCode The user code from the device authorization request
|
||||
* @return The state parameter for submitting consent for authorization
|
||||
*/
|
||||
public String submitCode(String userCode) throws Exception {
|
||||
MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>();
|
||||
parameters.set(OAuth2ParameterNames.USER_CODE, userCode);
|
||||
|
||||
MvcResult mvcResult = this.mockMvc.perform(get("/oauth2/device_verification")
|
||||
.params(parameters)
|
||||
.with(user(this.username).roles("USER")))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(header().string(HttpHeaders.CONTENT_TYPE, containsString(MediaType.TEXT_HTML_VALUE)))
|
||||
.andReturn();
|
||||
String responseHtml = mvcResult.getResponse().getContentAsString();
|
||||
Matcher matcher = HIDDEN_STATE_INPUT_PATTERN.matcher(responseHtml);
|
||||
|
||||
return matcher.matches() ? matcher.group(1) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Submit consent for the device authorization request.
|
||||
*
|
||||
* @param registeredClient The registered client
|
||||
* @param state The state parameter from the consent screen
|
||||
* @param userCode The user code from the device authorization request
|
||||
*/
|
||||
public void submitConsent(RegisteredClient registeredClient, String state, String userCode) throws Exception {
|
||||
MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>();
|
||||
parameters.set(OAuth2ParameterNames.CLIENT_ID, registeredClient.getClientId());
|
||||
parameters.set(OAuth2ParameterNames.STATE, state);
|
||||
for (String scope : this.scopes) {
|
||||
parameters.add(OAuth2ParameterNames.SCOPE, scope);
|
||||
}
|
||||
parameters.set(OAuth2ParameterNames.USER_CODE, userCode);
|
||||
|
||||
MvcResult mvcResult = this.mockMvc.perform(post("/oauth2/device_verification")
|
||||
.params(parameters)
|
||||
.with(user(this.username).roles("USER")))
|
||||
.andExpect(status().is3xxRedirection())
|
||||
.andReturn();
|
||||
String redirectedUrl = mvcResult.getResponse().getRedirectedUrl();
|
||||
assertThat(redirectedUrl).isNotNull();
|
||||
assertThat(redirectedUrl).isEqualTo("/?success");
|
||||
}
|
||||
|
||||
/**
|
||||
* Exchange a device code for an access token.
|
||||
*
|
||||
* @param registeredClient The registered client
|
||||
* @param deviceCode The device code obtained from the device authorization request
|
||||
* @return The token response
|
||||
*/
|
||||
public Map<String, Object> getTokenResponse(RegisteredClient registeredClient, String deviceCode) throws Exception {
|
||||
MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>();
|
||||
parameters.set(OAuth2ParameterNames.GRANT_TYPE, AuthorizationGrantType.DEVICE_CODE.getValue());
|
||||
parameters.set(OAuth2ParameterNames.DEVICE_CODE, deviceCode);
|
||||
|
||||
HttpHeaders basicAuth = new HttpHeaders();
|
||||
basicAuth.setBasicAuth(registeredClient.getClientId(), "secret");
|
||||
|
||||
MvcResult mvcResult = this.mockMvc.perform(post("/oauth2/token")
|
||||
.params(parameters)
|
||||
.headers(basicAuth))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(header().string(HttpHeaders.CONTENT_TYPE, containsString(MediaType.APPLICATION_JSON_VALUE)))
|
||||
.andExpect(jsonPath("$.access_token").isNotEmpty())
|
||||
.andExpect(jsonPath("$.refresh_token").isNotEmpty())
|
||||
.andExpect(jsonPath("$.token_type").isNotEmpty())
|
||||
.andExpect(jsonPath("$.scope").isNotEmpty())
|
||||
.andExpect(jsonPath("$.expires_in").isNotEmpty())
|
||||
.andReturn();
|
||||
|
||||
ObjectMapper objectMapper = new ObjectMapper();
|
||||
String responseJson = mvcResult.getResponse().getContentAsString();
|
||||
return objectMapper.readValue(responseJson, JSON_RESPONSE_TYPE_REFERENCE);
|
||||
}
|
||||
}
|
||||
74
docs/src/test/java/sample/extgrant/CustomCodeGrantTests.java
Normal file
74
docs/src/test/java/sample/extgrant/CustomCodeGrantTests.java
Normal file
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* 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.extgrant;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import sample.test.SpringTestContext;
|
||||
import sample.test.SpringTestContextExtension;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.context.annotation.ComponentScan;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
|
||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
@ExtendWith(SpringTestContextExtension.class)
|
||||
public class CustomCodeGrantTests {
|
||||
|
||||
public final SpringTestContext spring = new SpringTestContext(this);
|
||||
|
||||
@Autowired
|
||||
private RegisteredClientRepository registeredClientRepository;
|
||||
|
||||
@Autowired
|
||||
private MockMvc mvc;
|
||||
|
||||
@Test
|
||||
public void requestWhenTokenRequestValidThenTokenResponse() throws Exception {
|
||||
this.spring.register(AuthorizationServerConfig.class).autowire();
|
||||
|
||||
RegisteredClient registeredClient = this.registeredClientRepository.findByClientId("messaging-client");
|
||||
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setBasicAuth(registeredClient.getClientId(),
|
||||
registeredClient.getClientSecret().replace("{noop}", ""));
|
||||
|
||||
// @formatter:off
|
||||
this.mvc.perform(post("/oauth2/token")
|
||||
.param(OAuth2ParameterNames.GRANT_TYPE, "urn:ietf:params:oauth:grant-type:custom_code")
|
||||
.param(OAuth2ParameterNames.CODE, "7QR49T1W3")
|
||||
.headers(headers))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.access_token").isNotEmpty());
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@EnableWebSecurity
|
||||
@EnableAutoConfiguration
|
||||
@ComponentScan
|
||||
static class AuthorizationServerConfig {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
/*
|
||||
* 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.gettingstarted;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import org.assertj.core.api.ObjectAssert;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import sample.AuthorizationCodeGrantFlow;
|
||||
import sample.test.SpringTestContext;
|
||||
import sample.test.SpringTestContextExtension;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.ComponentScan;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||
import org.springframework.security.oauth2.core.oidc.OidcScopes;
|
||||
import org.springframework.security.oauth2.core.oidc.endpoint.OidcParameterNames;
|
||||
import org.springframework.security.oauth2.server.authorization.InMemoryOAuth2AuthorizationConsentService;
|
||||
import org.springframework.security.oauth2.server.authorization.InMemoryOAuth2AuthorizationService;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsent;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsentService;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2TokenType;
|
||||
import org.springframework.security.oauth2.server.authorization.client.InMemoryRegisteredClientRepository;
|
||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
|
||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
|
||||
import org.springframework.security.oauth2.server.authorization.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for the Getting Started section of the reference documentation.
|
||||
*
|
||||
* @author Steve Riesenberg
|
||||
*/
|
||||
@ExtendWith(SpringTestContextExtension.class)
|
||||
public class SecurityConfigTests {
|
||||
public final SpringTestContext spring = new SpringTestContext(this);
|
||||
|
||||
@Autowired
|
||||
private MockMvc mockMvc;
|
||||
|
||||
@Autowired
|
||||
private RegisteredClientRepository registeredClientRepository;
|
||||
|
||||
@Autowired
|
||||
private OAuth2AuthorizationService authorizationService;
|
||||
|
||||
@Autowired
|
||||
private OAuth2AuthorizationConsentService authorizationConsentService;
|
||||
|
||||
@Test
|
||||
public void oidcLoginWhenGettingStartedConfigUsedThenSuccess() throws Exception {
|
||||
this.spring.register(AuthorizationServerConfig.class).autowire();
|
||||
assertThat(this.registeredClientRepository).isInstanceOf(InMemoryRegisteredClientRepository.class);
|
||||
assertThat(this.authorizationService).isInstanceOf(InMemoryOAuth2AuthorizationService.class);
|
||||
assertThat(this.authorizationConsentService).isInstanceOf(InMemoryOAuth2AuthorizationConsentService.class);
|
||||
|
||||
RegisteredClient registeredClient = this.registeredClientRepository.findByClientId("oidc-client");
|
||||
assertThat(registeredClient).isNotNull();
|
||||
|
||||
AuthorizationCodeGrantFlow authorizationCodeGrantFlow = new AuthorizationCodeGrantFlow(this.mockMvc);
|
||||
authorizationCodeGrantFlow.setUsername("user");
|
||||
authorizationCodeGrantFlow.addScope(OidcScopes.OPENID);
|
||||
authorizationCodeGrantFlow.addScope(OidcScopes.PROFILE);
|
||||
|
||||
String state = authorizationCodeGrantFlow.authorize(registeredClient);
|
||||
assertThatAuthorization(state, OAuth2ParameterNames.STATE).isNotNull();
|
||||
assertThatAuthorization(state, null).isNotNull();
|
||||
|
||||
String authorizationCode = authorizationCodeGrantFlow.submitConsent(registeredClient, state);
|
||||
assertThatAuthorization(authorizationCode, OAuth2ParameterNames.CODE).isNotNull();
|
||||
assertThatAuthorization(authorizationCode, null).isNotNull();
|
||||
|
||||
Map<String, Object> tokenResponse = authorizationCodeGrantFlow.getTokenResponse(registeredClient, authorizationCode);
|
||||
String accessToken = (String) tokenResponse.get(OAuth2ParameterNames.ACCESS_TOKEN);
|
||||
assertThatAuthorization(accessToken, OAuth2ParameterNames.ACCESS_TOKEN).isNotNull();
|
||||
assertThatAuthorization(accessToken, null).isNotNull();
|
||||
|
||||
String refreshToken = (String) tokenResponse.get(OAuth2ParameterNames.REFRESH_TOKEN);
|
||||
assertThatAuthorization(refreshToken, OAuth2ParameterNames.REFRESH_TOKEN).isNotNull();
|
||||
assertThatAuthorization(refreshToken, null).isNotNull();
|
||||
|
||||
String idToken = (String) tokenResponse.get(OidcParameterNames.ID_TOKEN);
|
||||
assertThatAuthorization(idToken, OidcParameterNames.ID_TOKEN).isNotNull();
|
||||
assertThatAuthorization(idToken, null).isNotNull();
|
||||
|
||||
OAuth2Authorization authorization = findAuthorization(accessToken, OAuth2ParameterNames.ACCESS_TOKEN);
|
||||
assertThat(authorization.getToken(idToken)).isNotNull();
|
||||
|
||||
String scopes = (String) tokenResponse.get(OAuth2ParameterNames.SCOPE);
|
||||
OAuth2AuthorizationConsent authorizationConsent = this.authorizationConsentService.findById(
|
||||
registeredClient.getId(), "user");
|
||||
assertThat(authorizationConsent).isNotNull();
|
||||
assertThat(authorizationConsent.getScopes()).containsExactlyInAnyOrder(
|
||||
StringUtils.delimitedListToStringArray(scopes, " "));
|
||||
}
|
||||
|
||||
private ObjectAssert<OAuth2Authorization> assertThatAuthorization(String token, String tokenType) {
|
||||
return assertThat(findAuthorization(token, tokenType));
|
||||
}
|
||||
|
||||
private OAuth2Authorization findAuthorization(String token, String tokenType) {
|
||||
return this.authorizationService.findByToken(token, tokenType == null ? null : new OAuth2TokenType(tokenType));
|
||||
}
|
||||
|
||||
@EnableWebSecurity
|
||||
@EnableAutoConfiguration
|
||||
@ComponentScan
|
||||
@Import(OAuth2AuthorizationServerConfiguration.class)
|
||||
static class AuthorizationServerConfig extends SecurityConfig {
|
||||
|
||||
@Bean
|
||||
public OAuth2AuthorizationService authorizationService() {
|
||||
return new InMemoryOAuth2AuthorizationService();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public OAuth2AuthorizationConsentService authorizationConsentService() {
|
||||
return new InMemoryOAuth2AuthorizationConsentService();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
112
docs/src/test/java/sample/jose/TestJwks.java
Normal file
112
docs/src/test/java/sample/jose/TestJwks.java
Normal file
@@ -0,0 +1,112 @@
|
||||
/*
|
||||
* Copyright 2020-2021 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.jose;
|
||||
|
||||
import java.security.KeyPair;
|
||||
import java.security.KeyPairGenerator;
|
||||
import java.security.interfaces.ECPrivateKey;
|
||||
import java.security.interfaces.ECPublicKey;
|
||||
import java.security.interfaces.RSAPrivateKey;
|
||||
import java.security.interfaces.RSAPublicKey;
|
||||
import java.util.UUID;
|
||||
|
||||
import javax.crypto.SecretKey;
|
||||
|
||||
import com.nimbusds.jose.jwk.Curve;
|
||||
import com.nimbusds.jose.jwk.ECKey;
|
||||
import com.nimbusds.jose.jwk.KeyUse;
|
||||
import com.nimbusds.jose.jwk.OctetSequenceKey;
|
||||
import com.nimbusds.jose.jwk.RSAKey;
|
||||
|
||||
/**
|
||||
* @author Joe Grandja
|
||||
*/
|
||||
public final class TestJwks {
|
||||
|
||||
private static final KeyPairGenerator rsaKeyPairGenerator;
|
||||
static {
|
||||
try {
|
||||
rsaKeyPairGenerator = KeyPairGenerator.getInstance("RSA");
|
||||
rsaKeyPairGenerator.initialize(2048);
|
||||
} catch (Exception ex) {
|
||||
throw new IllegalStateException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
// @formatter:off
|
||||
public static final RSAKey DEFAULT_RSA_JWK =
|
||||
jwk(
|
||||
TestKeys.DEFAULT_PUBLIC_KEY,
|
||||
TestKeys.DEFAULT_PRIVATE_KEY
|
||||
).build();
|
||||
// @formatter:on
|
||||
|
||||
// @formatter:off
|
||||
public static final ECKey DEFAULT_EC_JWK =
|
||||
jwk(
|
||||
(ECPublicKey) TestKeys.DEFAULT_EC_KEY_PAIR.getPublic(),
|
||||
(ECPrivateKey) TestKeys.DEFAULT_EC_KEY_PAIR.getPrivate()
|
||||
).build();
|
||||
// @formatter:on
|
||||
|
||||
// @formatter:off
|
||||
public static final OctetSequenceKey DEFAULT_SECRET_JWK =
|
||||
jwk(
|
||||
TestKeys.DEFAULT_SECRET_KEY
|
||||
).build();
|
||||
// @formatter:on
|
||||
|
||||
private TestJwks() {
|
||||
}
|
||||
|
||||
public static RSAKey.Builder generateRsaJwk() {
|
||||
KeyPair keyPair = rsaKeyPairGenerator.generateKeyPair();
|
||||
RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
|
||||
RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
|
||||
// @formatter:off
|
||||
return jwk(publicKey, privateKey)
|
||||
.keyID(UUID.randomUUID().toString());
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
public static RSAKey.Builder jwk(RSAPublicKey publicKey, RSAPrivateKey privateKey) {
|
||||
// @formatter:off
|
||||
return new RSAKey.Builder(publicKey)
|
||||
.privateKey(privateKey)
|
||||
.keyUse(KeyUse.SIGNATURE)
|
||||
.keyID("rsa-jwk-kid");
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
public static ECKey.Builder jwk(ECPublicKey publicKey, ECPrivateKey privateKey) {
|
||||
// @formatter:off
|
||||
Curve curve = Curve.forECParameterSpec(publicKey.getParams());
|
||||
return new ECKey.Builder(curve, publicKey)
|
||||
.privateKey(privateKey)
|
||||
.keyUse(KeyUse.SIGNATURE)
|
||||
.keyID("ec-jwk-kid");
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
public static OctetSequenceKey.Builder jwk(SecretKey secretKey) {
|
||||
// @formatter:off
|
||||
return new OctetSequenceKey.Builder(secretKey)
|
||||
.keyUse(KeyUse.SIGNATURE)
|
||||
.keyID("secret-jwk-kid");
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
}
|
||||
148
docs/src/test/java/sample/jose/TestKeys.java
Normal file
148
docs/src/test/java/sample/jose/TestKeys.java
Normal file
@@ -0,0 +1,148 @@
|
||||
/*
|
||||
* Copyright 2020-2022 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.jose;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.security.KeyFactory;
|
||||
import java.security.KeyPair;
|
||||
import java.security.KeyPairGenerator;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.interfaces.RSAPrivateKey;
|
||||
import java.security.interfaces.RSAPublicKey;
|
||||
import java.security.spec.ECFieldFp;
|
||||
import java.security.spec.ECParameterSpec;
|
||||
import java.security.spec.ECPoint;
|
||||
import java.security.spec.EllipticCurve;
|
||||
import java.security.spec.InvalidKeySpecException;
|
||||
import java.security.spec.PKCS8EncodedKeySpec;
|
||||
import java.security.spec.X509EncodedKeySpec;
|
||||
import java.util.Base64;
|
||||
|
||||
import javax.crypto.SecretKey;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
/**
|
||||
* @author Joe Grandja
|
||||
*/
|
||||
public final class TestKeys {
|
||||
|
||||
public static final KeyFactory kf;
|
||||
static {
|
||||
try {
|
||||
kf = KeyFactory.getInstance("RSA");
|
||||
}
|
||||
catch (NoSuchAlgorithmException ex) {
|
||||
throw new IllegalStateException(ex);
|
||||
}
|
||||
}
|
||||
public static final String DEFAULT_ENCODED_SECRET_KEY = "bCzY/M48bbkwBEWjmNSIEPfwApcvXOnkCxORBEbPr+4=";
|
||||
|
||||
public static final SecretKey DEFAULT_SECRET_KEY = new SecretKeySpec(
|
||||
Base64.getDecoder().decode(DEFAULT_ENCODED_SECRET_KEY), "AES");
|
||||
|
||||
// @formatter:off
|
||||
public static final String DEFAULT_RSA_PUBLIC_KEY = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA3FlqJr5TRskIQIgdE3Dd"
|
||||
+ "7D9lboWdcTUT8a+fJR7MAvQm7XXNoYkm3v7MQL1NYtDvL2l8CAnc0WdSTINU6IRv"
|
||||
+ "c5Kqo2Q4csNX9SHOmEfzoROjQqahEcve1jBXluoCXdYuYpx4/1tfRgG6ii4Uhxh6"
|
||||
+ "iI8qNMJQX+fLfqhbfYfxBQVRPywBkAbIP4x1EAsbC6FSNmkhCxiMNqEgxaIpY8C2"
|
||||
+ "kJdJ/ZIV+WW4noDdzpKqHcwmB8FsrumlVY/DNVvUSDIipiq9PbP4H99TXN1o746o"
|
||||
+ "RaNa07rq1hoCgMSSy+85SagCoxlmyE+D+of9SsMY8Ol9t0rdzpobBuhyJ/o5dfvj"
|
||||
+ "KwIDAQAB";
|
||||
// @formatter:on
|
||||
|
||||
public static final RSAPublicKey DEFAULT_PUBLIC_KEY;
|
||||
static {
|
||||
X509EncodedKeySpec spec = new X509EncodedKeySpec(Base64.getDecoder().decode(DEFAULT_RSA_PUBLIC_KEY));
|
||||
try {
|
||||
DEFAULT_PUBLIC_KEY = (RSAPublicKey) kf.generatePublic(spec);
|
||||
}
|
||||
catch (InvalidKeySpecException ex) {
|
||||
throw new IllegalArgumentException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
// @formatter:off
|
||||
public static final String DEFAULT_RSA_PRIVATE_KEY = "MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDcWWomvlNGyQhA"
|
||||
+ "iB0TcN3sP2VuhZ1xNRPxr58lHswC9Cbtdc2hiSbe/sxAvU1i0O8vaXwICdzRZ1JM"
|
||||
+ "g1TohG9zkqqjZDhyw1f1Ic6YR/OhE6NCpqERy97WMFeW6gJd1i5inHj/W19GAbqK"
|
||||
+ "LhSHGHqIjyo0wlBf58t+qFt9h/EFBVE/LAGQBsg/jHUQCxsLoVI2aSELGIw2oSDF"
|
||||
+ "oiljwLaQl0n9khX5ZbiegN3OkqodzCYHwWyu6aVVj8M1W9RIMiKmKr09s/gf31Nc"
|
||||
+ "3WjvjqhFo1rTuurWGgKAxJLL7zlJqAKjGWbIT4P6h/1Kwxjw6X23St3OmhsG6HIn"
|
||||
+ "+jl1++MrAgMBAAECggEBAMf820wop3pyUOwI3aLcaH7YFx5VZMzvqJdNlvpg1jbE"
|
||||
+ "E2Sn66b1zPLNfOIxLcBG8x8r9Ody1Bi2Vsqc0/5o3KKfdgHvnxAB3Z3dPh2WCDek"
|
||||
+ "lCOVClEVoLzziTuuTdGO5/CWJXdWHcVzIjPxmK34eJXioiLaTYqN3XKqKMdpD0ZG"
|
||||
+ "mtNTGvGf+9fQ4i94t0WqIxpMpGt7NM4RHy3+Onggev0zLiDANC23mWrTsUgect/7"
|
||||
+ "62TYg8g1bKwLAb9wCBT+BiOuCc2wrArRLOJgUkj/F4/gtrR9ima34SvWUyoUaKA0"
|
||||
+ "bi4YBX9l8oJwFGHbU9uFGEMnH0T/V0KtIB7qetReywkCgYEA9cFyfBIQrYISV/OA"
|
||||
+ "+Z0bo3vh2aL0QgKrSXZ924cLt7itQAHNZ2ya+e3JRlTczi5mnWfjPWZ6eJB/8MlH"
|
||||
+ "Gpn12o/POEkU+XjZZSPe1RWGt5g0S3lWqyx9toCS9ACXcN9tGbaqcFSVI73zVTRA"
|
||||
+ "8J9grR0fbGn7jaTlTX2tnlOTQ60CgYEA5YjYpEq4L8UUMFkuj+BsS3u0oEBnzuHd"
|
||||
+ "I9LEHmN+CMPosvabQu5wkJXLuqo2TxRnAznsA8R3pCLkdPGoWMCiWRAsCn979TdY"
|
||||
+ "QbqO2qvBAD2Q19GtY7lIu6C35/enQWzJUMQE3WW0OvjLzZ0l/9mA2FBRR+3F9A1d"
|
||||
+ "rBdnmv0c3TcCgYEAi2i+ggVZcqPbtgrLOk5WVGo9F1GqUBvlgNn30WWNTx4zIaEk"
|
||||
+ "HSxtyaOLTxtq2odV7Kr3LGiKxwPpn/T+Ief+oIp92YcTn+VfJVGw4Z3BezqbR8lA"
|
||||
+ "Uf/+HF5ZfpMrVXtZD4Igs3I33Duv4sCuqhEvLWTc44pHifVloozNxYfRfU0CgYBN"
|
||||
+ "HXa7a6cJ1Yp829l62QlJKtx6Ymj95oAnQu5Ez2ROiZMqXRO4nucOjGUP55Orac1a"
|
||||
+ "FiGm+mC/skFS0MWgW8evaHGDbWU180wheQ35hW6oKAb7myRHtr4q20ouEtQMdQIF"
|
||||
+ "snV39G1iyqeeAsf7dxWElydXpRi2b68i3BIgzhzebQKBgQCdUQuTsqV9y/JFpu6H"
|
||||
+ "c5TVvhG/ubfBspI5DhQqIGijnVBzFT//UfIYMSKJo75qqBEyP2EJSmCsunWsAFsM"
|
||||
+ "TszuiGTkrKcZy9G0wJqPztZZl2F2+bJgnA6nBEV7g5PA4Af+QSmaIhRwqGDAuROR"
|
||||
+ "47jndeyIaMTNETEmOnms+as17g==";
|
||||
// @formatter:on
|
||||
|
||||
public static final RSAPrivateKey DEFAULT_PRIVATE_KEY;
|
||||
static {
|
||||
PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(Base64.getDecoder().decode(DEFAULT_RSA_PRIVATE_KEY));
|
||||
try {
|
||||
DEFAULT_PRIVATE_KEY = (RSAPrivateKey) kf.generatePrivate(spec);
|
||||
}
|
||||
catch (InvalidKeySpecException ex) {
|
||||
throw new IllegalArgumentException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
public static final KeyPair DEFAULT_RSA_KEY_PAIR = new KeyPair(DEFAULT_PUBLIC_KEY, DEFAULT_PRIVATE_KEY);
|
||||
|
||||
public static final KeyPair DEFAULT_EC_KEY_PAIR = generateEcKeyPair();
|
||||
|
||||
static KeyPair generateEcKeyPair() {
|
||||
EllipticCurve ellipticCurve = new EllipticCurve(
|
||||
new ECFieldFp(new BigInteger(
|
||||
"115792089210356248762697446949407573530086143415290314195533631308867097853951")),
|
||||
new BigInteger("115792089210356248762697446949407573530086143415290314195533631308867097853948"),
|
||||
new BigInteger("41058363725152142129326129780047268409114441015993725554835256314039467401291"));
|
||||
ECPoint ecPoint = new ECPoint(
|
||||
new BigInteger("48439561293906451759052585252797914202762949526041747995844080717082404635286"),
|
||||
new BigInteger("36134250956749795798585127919587881956611106672985015071877198253568414405109"));
|
||||
ECParameterSpec ecParameterSpec = new ECParameterSpec(ellipticCurve, ecPoint,
|
||||
new BigInteger("115792089210356248762697446949407573529996955224135760342422259061068512044369"), 1);
|
||||
|
||||
KeyPair keyPair;
|
||||
try {
|
||||
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("EC");
|
||||
keyPairGenerator.initialize(ecParameterSpec);
|
||||
keyPair = keyPairGenerator.generateKeyPair();
|
||||
}
|
||||
catch (Exception ex) {
|
||||
throw new IllegalStateException(ex);
|
||||
}
|
||||
return keyPair;
|
||||
}
|
||||
|
||||
private TestKeys() {
|
||||
}
|
||||
|
||||
}
|
||||
236
docs/src/test/java/sample/jpa/JpaTests.java
Normal file
236
docs/src/test/java/sample/jpa/JpaTests.java
Normal file
@@ -0,0 +1,236 @@
|
||||
/*
|
||||
* 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.jpa;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import com.nimbusds.jose.jwk.JWKSet;
|
||||
import com.nimbusds.jose.jwk.source.ImmutableJWKSet;
|
||||
import com.nimbusds.jose.jwk.source.JWKSource;
|
||||
import com.nimbusds.jose.proc.SecurityContext;
|
||||
import org.assertj.core.api.ObjectAssert;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import sample.AuthorizationCodeGrantFlow;
|
||||
import sample.DeviceAuthorizationGrantFlow;
|
||||
import sample.jose.TestJwks;
|
||||
import sample.jpa.service.authorization.JpaOAuth2AuthorizationService;
|
||||
import sample.jpa.service.authorizationConsent.JpaOAuth2AuthorizationConsentService;
|
||||
import sample.jpa.service.client.JpaRegisteredClientRepository;
|
||||
import sample.test.SpringTestContext;
|
||||
import sample.test.SpringTestContextExtension;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.ComponentScan;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.http.MediaType;
|
||||
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.core.endpoint.OAuth2ParameterNames;
|
||||
import org.springframework.security.oauth2.core.oidc.endpoint.OidcParameterNames;
|
||||
import org.springframework.security.oauth2.jwt.JwtDecoder;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsent;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsentService;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
|
||||
import org.springframework.security.oauth2.server.authorization.OAuth2TokenType;
|
||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
|
||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
|
||||
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.web.SecurityFilterChain;
|
||||
import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint;
|
||||
import org.springframework.security.web.util.matcher.MediaTypeRequestMatcher;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static sample.util.RegisteredClients.messagingClient;
|
||||
|
||||
/**
|
||||
* Tests for the guide How-to: Implement core services with JPA.
|
||||
*
|
||||
* @author Steve Riesenberg
|
||||
*/
|
||||
@ExtendWith(SpringTestContextExtension.class)
|
||||
public class JpaTests {
|
||||
public final SpringTestContext spring = new SpringTestContext(this);
|
||||
|
||||
@Autowired
|
||||
private MockMvc mockMvc;
|
||||
|
||||
@Autowired
|
||||
private RegisteredClientRepository registeredClientRepository;
|
||||
|
||||
@Autowired
|
||||
private OAuth2AuthorizationService authorizationService;
|
||||
|
||||
@Autowired
|
||||
private OAuth2AuthorizationConsentService authorizationConsentService;
|
||||
|
||||
@Test
|
||||
public void oidcLoginWhenJpaCoreServicesAutowiredThenUsed() throws Exception {
|
||||
this.spring.register(AuthorizationServerConfig.class).autowire();
|
||||
assertThat(this.registeredClientRepository).isInstanceOf(JpaRegisteredClientRepository.class);
|
||||
assertThat(this.authorizationService).isInstanceOf(JpaOAuth2AuthorizationService.class);
|
||||
assertThat(this.authorizationConsentService).isInstanceOf(JpaOAuth2AuthorizationConsentService.class);
|
||||
|
||||
RegisteredClient registeredClient = messagingClient();
|
||||
this.registeredClientRepository.save(registeredClient);
|
||||
|
||||
AuthorizationCodeGrantFlow authorizationCodeGrantFlow = new AuthorizationCodeGrantFlow(this.mockMvc);
|
||||
authorizationCodeGrantFlow.setUsername("user");
|
||||
authorizationCodeGrantFlow.addScope("message.read");
|
||||
authorizationCodeGrantFlow.addScope("message.write");
|
||||
|
||||
String state = authorizationCodeGrantFlow.authorize(registeredClient);
|
||||
assertThatAuthorization(state, OAuth2ParameterNames.STATE).isNotNull();
|
||||
assertThatAuthorization(state, null).isNotNull();
|
||||
|
||||
String authorizationCode = authorizationCodeGrantFlow.submitConsent(registeredClient, state);
|
||||
assertThatAuthorization(authorizationCode, OAuth2ParameterNames.CODE).isNotNull();
|
||||
assertThatAuthorization(authorizationCode, null).isNotNull();
|
||||
|
||||
Map<String, Object> tokenResponse = authorizationCodeGrantFlow.getTokenResponse(registeredClient, authorizationCode);
|
||||
String accessToken = (String) tokenResponse.get(OAuth2ParameterNames.ACCESS_TOKEN);
|
||||
assertThatAuthorization(accessToken, OAuth2ParameterNames.ACCESS_TOKEN).isNotNull();
|
||||
assertThatAuthorization(accessToken, null).isNotNull();
|
||||
|
||||
String refreshToken = (String) tokenResponse.get(OAuth2ParameterNames.REFRESH_TOKEN);
|
||||
assertThatAuthorization(refreshToken, OAuth2ParameterNames.REFRESH_TOKEN).isNotNull();
|
||||
assertThatAuthorization(refreshToken, null).isNotNull();
|
||||
|
||||
String idToken = (String) tokenResponse.get(OidcParameterNames.ID_TOKEN);
|
||||
assertThatAuthorization(idToken, OidcParameterNames.ID_TOKEN).isNotNull();
|
||||
assertThatAuthorization(idToken, null).isNotNull();
|
||||
|
||||
OAuth2Authorization authorization = findAuthorization(accessToken, OAuth2ParameterNames.ACCESS_TOKEN);
|
||||
assertThat(authorization.getToken(idToken)).isNotNull();
|
||||
|
||||
String scopes = (String) tokenResponse.get(OAuth2ParameterNames.SCOPE);
|
||||
OAuth2AuthorizationConsent authorizationConsent = this.authorizationConsentService.findById(
|
||||
registeredClient.getId(), "user");
|
||||
assertThat(authorizationConsent).isNotNull();
|
||||
assertThat(authorizationConsent.getScopes()).containsExactlyInAnyOrder(
|
||||
StringUtils.delimitedListToStringArray(scopes, " "));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void deviceAuthorizationWhenJpaCoreServicesAutowiredThenSuccess() throws Exception {
|
||||
this.spring.register(AuthorizationServerConfig.class).autowire();
|
||||
assertThat(this.registeredClientRepository).isInstanceOf(JpaRegisteredClientRepository.class);
|
||||
assertThat(this.authorizationService).isInstanceOf(JpaOAuth2AuthorizationService.class);
|
||||
assertThat(this.authorizationConsentService).isInstanceOf(JpaOAuth2AuthorizationConsentService.class);
|
||||
|
||||
RegisteredClient registeredClient = messagingClient();
|
||||
this.registeredClientRepository.save(registeredClient);
|
||||
|
||||
DeviceAuthorizationGrantFlow deviceAuthorizationGrantFlow = new DeviceAuthorizationGrantFlow(this.mockMvc);
|
||||
deviceAuthorizationGrantFlow.setUsername("user");
|
||||
deviceAuthorizationGrantFlow.addScope("message.read");
|
||||
deviceAuthorizationGrantFlow.addScope("message.write");
|
||||
|
||||
Map<String, Object> deviceAuthorizationResponse = deviceAuthorizationGrantFlow.authorize(registeredClient);
|
||||
String userCode = (String) deviceAuthorizationResponse.get(OAuth2ParameterNames.USER_CODE);
|
||||
assertThatAuthorization(userCode, OAuth2ParameterNames.USER_CODE).isNotNull();
|
||||
assertThatAuthorization(userCode, null).isNotNull();
|
||||
|
||||
String deviceCode = (String) deviceAuthorizationResponse.get(OAuth2ParameterNames.DEVICE_CODE);
|
||||
assertThatAuthorization(deviceCode, OAuth2ParameterNames.DEVICE_CODE).isNotNull();
|
||||
assertThatAuthorization(deviceCode, null).isNotNull();
|
||||
|
||||
String state = deviceAuthorizationGrantFlow.submitCode(userCode);
|
||||
assertThatAuthorization(state, OAuth2ParameterNames.STATE).isNotNull();
|
||||
assertThatAuthorization(state, null).isNotNull();
|
||||
|
||||
deviceAuthorizationGrantFlow.submitConsent(registeredClient, state, userCode);
|
||||
|
||||
Map<String, Object> tokenResponse = deviceAuthorizationGrantFlow.getTokenResponse(registeredClient, deviceCode);
|
||||
String accessToken = (String) tokenResponse.get(OAuth2ParameterNames.ACCESS_TOKEN);
|
||||
assertThatAuthorization(accessToken, OAuth2ParameterNames.ACCESS_TOKEN).isNotNull();
|
||||
assertThatAuthorization(accessToken, null).isNotNull();
|
||||
|
||||
String refreshToken = (String) tokenResponse.get(OAuth2ParameterNames.REFRESH_TOKEN);
|
||||
assertThatAuthorization(refreshToken, OAuth2ParameterNames.REFRESH_TOKEN).isNotNull();
|
||||
assertThatAuthorization(refreshToken, null).isNotNull();
|
||||
|
||||
String scopes = (String) tokenResponse.get(OAuth2ParameterNames.SCOPE);
|
||||
OAuth2AuthorizationConsent authorizationConsent = this.authorizationConsentService.findById(
|
||||
registeredClient.getId(), "user");
|
||||
assertThat(authorizationConsent).isNotNull();
|
||||
assertThat(authorizationConsent.getScopes()).containsExactlyInAnyOrder(
|
||||
StringUtils.delimitedListToStringArray(scopes, " "));
|
||||
}
|
||||
|
||||
private ObjectAssert<OAuth2Authorization> assertThatAuthorization(String token, String tokenType) {
|
||||
return assertThat(findAuthorization(token, tokenType));
|
||||
}
|
||||
|
||||
private OAuth2Authorization findAuthorization(String token, String tokenType) {
|
||||
return this.authorizationService.findByToken(token, tokenType == null ? null : new OAuth2TokenType(tokenType));
|
||||
}
|
||||
|
||||
@EnableWebSecurity
|
||||
@EnableAutoConfiguration
|
||||
@ComponentScan
|
||||
static class AuthorizationServerConfig {
|
||||
|
||||
@Bean
|
||||
@Order(Ordered.HIGHEST_PRECEDENCE)
|
||||
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
|
||||
OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
|
||||
http.getConfigurer(OAuth2AuthorizationServerConfigurer.class)
|
||||
.oidc(Customizer.withDefaults()); // Enable OpenID Connect 1.0
|
||||
|
||||
// @formatter:off
|
||||
http
|
||||
.exceptionHandling((exceptions) -> exceptions
|
||||
.defaultAuthenticationEntryPointFor(
|
||||
new LoginUrlAuthenticationEntryPoint("/login"),
|
||||
new MediaTypeRequestMatcher(MediaType.TEXT_HTML)
|
||||
)
|
||||
)
|
||||
.oauth2ResourceServer((resourceServer) -> resourceServer
|
||||
.jwt(Customizer.withDefaults())
|
||||
);
|
||||
// @formatter:on
|
||||
return http.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public JWKSource<SecurityContext> jwkSource() {
|
||||
JWKSet jwkSet = new JWKSet(TestJwks.DEFAULT_RSA_JWK);
|
||||
return new ImmutableJWKSet<>(jwkSet);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public JwtDecoder jwtDecoder(JWKSource<SecurityContext> jwkSource) {
|
||||
return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public AuthorizationServerSettings authorizationServerSettings() {
|
||||
return AuthorizationServerSettings.builder().build();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
87
docs/src/test/java/sample/pkce/PublicClientTests.java
Normal file
87
docs/src/test/java/sample/pkce/PublicClientTests.java
Normal file
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
* 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.pkce;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import sample.AuthorizationCodeGrantFlow;
|
||||
import sample.test.SpringTestContext;
|
||||
import sample.test.SpringTestContextExtension;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.context.annotation.ComponentScan;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||
import org.springframework.security.oauth2.core.oidc.OidcScopes;
|
||||
import org.springframework.security.oauth2.core.oidc.endpoint.OidcParameterNames;
|
||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
|
||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static sample.AuthorizationCodeGrantFlow.withCodeChallenge;
|
||||
import static sample.AuthorizationCodeGrantFlow.withCodeVerifier;
|
||||
|
||||
/**
|
||||
* @author Steve Riesenberg
|
||||
*/
|
||||
@ExtendWith(SpringTestContextExtension.class)
|
||||
public class PublicClientTests {
|
||||
public final SpringTestContext spring = new SpringTestContext(this);
|
||||
|
||||
@Autowired
|
||||
private MockMvc mockMvc;
|
||||
|
||||
@Autowired
|
||||
private RegisteredClientRepository registeredClientRepository;
|
||||
|
||||
@Test
|
||||
public void oidcLoginWhenPublicClientThenSuccess() throws Exception {
|
||||
this.spring.register(AuthorizationServerConfig.class).autowire();
|
||||
|
||||
RegisteredClient registeredClient = this.registeredClientRepository.findByClientId("public-client");
|
||||
assertThat(registeredClient).isNotNull();
|
||||
|
||||
AuthorizationCodeGrantFlow authorizationCodeGrantFlow = new AuthorizationCodeGrantFlow(this.mockMvc);
|
||||
authorizationCodeGrantFlow.setUsername("user");
|
||||
authorizationCodeGrantFlow.addScope(OidcScopes.OPENID);
|
||||
authorizationCodeGrantFlow.addScope(OidcScopes.PROFILE);
|
||||
|
||||
String state = authorizationCodeGrantFlow.authorize(registeredClient, withCodeChallenge());
|
||||
assertThat(state).isNotNull();
|
||||
|
||||
String authorizationCode = authorizationCodeGrantFlow.submitConsent(registeredClient, state);
|
||||
assertThat(authorizationCode).isNotNull();
|
||||
|
||||
Map<String, Object> tokenResponse = authorizationCodeGrantFlow.getTokenResponse(registeredClient,
|
||||
authorizationCode, withCodeVerifier());
|
||||
assertThat(tokenResponse.get(OAuth2ParameterNames.ACCESS_TOKEN)).isNotNull();
|
||||
// Note: Refresh tokens are not issued to public clients
|
||||
assertThat(tokenResponse.get(OAuth2ParameterNames.REFRESH_TOKEN)).isNull();
|
||||
assertThat(tokenResponse.get(OidcParameterNames.ID_TOKEN)).isNotNull();
|
||||
}
|
||||
|
||||
@EnableWebSecurity
|
||||
@EnableAutoConfiguration
|
||||
@ComponentScan
|
||||
static class AuthorizationServerConfig {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
151
docs/src/test/java/sample/test/SpringTestContext.java
Normal file
151
docs/src/test/java/sample/test/SpringTestContext.java
Normal file
@@ -0,0 +1,151 @@
|
||||
/*
|
||||
* Copyright 2020-2022 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.test;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import jakarta.servlet.Filter;
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor;
|
||||
import org.springframework.mock.web.MockServletConfig;
|
||||
import org.springframework.mock.web.MockServletContext;
|
||||
import org.springframework.security.config.BeanIds;
|
||||
import org.springframework.test.context.web.GenericXmlWebContextLoader;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.test.web.servlet.request.RequestPostProcessor;
|
||||
import org.springframework.test.web.servlet.setup.ConfigurableMockMvcBuilder;
|
||||
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
|
||||
import org.springframework.test.web.servlet.setup.MockMvcConfigurer;
|
||||
import org.springframework.web.context.ConfigurableWebApplicationContext;
|
||||
import org.springframework.web.context.WebApplicationContext;
|
||||
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
|
||||
import org.springframework.web.context.support.XmlWebApplicationContext;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity;
|
||||
|
||||
/**
|
||||
* @author Rob Winch
|
||||
*/
|
||||
public class SpringTestContext implements Closeable {
|
||||
|
||||
private Object test;
|
||||
|
||||
private ConfigurableWebApplicationContext context;
|
||||
|
||||
private List<Filter> filters = new ArrayList<>();
|
||||
|
||||
public SpringTestContext(Object test) {
|
||||
setTest(test);
|
||||
}
|
||||
|
||||
public void setTest(Object test) {
|
||||
this.test = test;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
try {
|
||||
this.context.close();
|
||||
}
|
||||
catch (Exception ex) {
|
||||
}
|
||||
}
|
||||
|
||||
public SpringTestContext context(ConfigurableWebApplicationContext context) {
|
||||
this.context = context;
|
||||
return this;
|
||||
}
|
||||
|
||||
public SpringTestContext register(Class<?>... classes) {
|
||||
AnnotationConfigWebApplicationContext applicationContext = new AnnotationConfigWebApplicationContext();
|
||||
applicationContext.register(classes);
|
||||
this.context = applicationContext;
|
||||
return this;
|
||||
}
|
||||
|
||||
public SpringTestContext testConfigLocations(String... configLocations) {
|
||||
GenericXmlWebContextLoader loader = new GenericXmlWebContextLoader();
|
||||
String[] locations = loader.processLocations(this.test.getClass(), configLocations);
|
||||
return configLocations(locations);
|
||||
}
|
||||
|
||||
public SpringTestContext configLocations(String... configLocations) {
|
||||
XmlWebApplicationContext context = new XmlWebApplicationContext();
|
||||
context.setConfigLocations(configLocations);
|
||||
this.context = context;
|
||||
return this;
|
||||
}
|
||||
|
||||
public SpringTestContext mockMvcAfterSpringSecurityOk() {
|
||||
return addFilter(new OncePerRequestFilter() {
|
||||
@Override
|
||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
|
||||
FilterChain filterChain) {
|
||||
response.setStatus(HttpServletResponse.SC_OK);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private SpringTestContext addFilter(Filter filter) {
|
||||
this.filters.add(filter);
|
||||
return this;
|
||||
}
|
||||
|
||||
public ConfigurableWebApplicationContext getContext() {
|
||||
if (!this.context.isRunning()) {
|
||||
this.context.setServletContext(new MockServletContext());
|
||||
this.context.setServletConfig(new MockServletConfig());
|
||||
this.context.refresh();
|
||||
}
|
||||
return this.context;
|
||||
}
|
||||
|
||||
public void autowire() {
|
||||
this.context.setServletContext(new MockServletContext());
|
||||
this.context.setServletConfig(new MockServletConfig());
|
||||
this.context.refresh();
|
||||
if (this.context.containsBean(BeanIds.SPRING_SECURITY_FILTER_CHAIN)) {
|
||||
// @formatter:off
|
||||
MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(this.context).
|
||||
apply(springSecurity())
|
||||
.apply(new AddFilter())
|
||||
.build();
|
||||
// @formatter:on
|
||||
this.context.getBeanFactory().registerResolvableDependency(MockMvc.class, mockMvc);
|
||||
}
|
||||
AutowiredAnnotationBeanPostProcessor bpp = new AutowiredAnnotationBeanPostProcessor();
|
||||
bpp.setBeanFactory(this.context.getBeanFactory());
|
||||
bpp.processInjection(this.test);
|
||||
}
|
||||
|
||||
private class AddFilter implements MockMvcConfigurer {
|
||||
|
||||
@Override
|
||||
public RequestPostProcessor beforeMockMvcCreated(ConfigurableMockMvcBuilder<?> builder,
|
||||
WebApplicationContext context) {
|
||||
builder.addFilters(SpringTestContext.this.filters.toArray(new Filter[0]));
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* Copyright 2020-2022 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.test;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.jupiter.api.extension.AfterEachCallback;
|
||||
import org.junit.jupiter.api.extension.BeforeEachCallback;
|
||||
import org.junit.jupiter.api.extension.ExtensionContext;
|
||||
|
||||
import org.springframework.security.test.context.TestSecurityContextHolder;
|
||||
|
||||
/**
|
||||
* @author Rob Winch
|
||||
*/
|
||||
public class SpringTestContextExtension implements BeforeEachCallback, AfterEachCallback {
|
||||
|
||||
@Override
|
||||
public void afterEach(ExtensionContext context) throws Exception {
|
||||
TestSecurityContextHolder.clearContext();
|
||||
getContexts(context.getRequiredTestInstance()).forEach((springTestContext) -> springTestContext.close());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void beforeEach(ExtensionContext context) throws Exception {
|
||||
Object testInstance = context.getRequiredTestInstance();
|
||||
getContexts(testInstance).forEach((springTestContext) -> springTestContext.setTest(testInstance));
|
||||
}
|
||||
|
||||
private static List<SpringTestContext> getContexts(Object test) throws IllegalAccessException {
|
||||
Field[] declaredFields = test.getClass().getDeclaredFields();
|
||||
List<SpringTestContext> result = new ArrayList<>();
|
||||
for (Field field : declaredFields) {
|
||||
if (SpringTestContext.class.isAssignableFrom(field.getType())) {
|
||||
result.add((SpringTestContext) field.get(test));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
/*
|
||||
* Copyright 2020-2022 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.userinfo;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import sample.AuthorizationCodeGrantFlow;
|
||||
import sample.test.SpringTestContext;
|
||||
import sample.test.SpringTestContextExtension;
|
||||
import sample.userinfo.idtoken.IdTokenCustomizerConfig;
|
||||
import sample.userinfo.idtoken.OidcUserInfoService;
|
||||
import sample.userinfo.jwt.JwtTokenCustomizerConfig;
|
||||
import sample.userinfo.jwt.JwtUserInfoMapperSecurityConfig;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
|
||||
import org.springframework.security.oauth2.core.oidc.OidcScopes;
|
||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
|
||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.hamcrest.Matchers.equalTo;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
/**
|
||||
* Tests for the guide How-to: Customize the OpenID Connect 1.0 UserInfo response.
|
||||
*
|
||||
* @author Steve Riesenberg
|
||||
*/
|
||||
@ExtendWith(SpringTestContextExtension.class)
|
||||
public class EnableUserInfoSecurityConfigTests {
|
||||
public final SpringTestContext spring = new SpringTestContext(this);
|
||||
|
||||
@Autowired
|
||||
private MockMvc mockMvc;
|
||||
|
||||
@Autowired
|
||||
private RegisteredClientRepository registeredClientRepository;
|
||||
|
||||
@Test
|
||||
public void userInfoWhenEnabledThenSuccess() throws Exception {
|
||||
this.spring.register(AuthorizationServerConfig.class).autowire();
|
||||
|
||||
RegisteredClient registeredClient = this.registeredClientRepository.findByClientId("messaging-client");
|
||||
assertThat(registeredClient).isNotNull();
|
||||
|
||||
AuthorizationCodeGrantFlow authorizationCodeGrantFlow = new AuthorizationCodeGrantFlow(this.mockMvc);
|
||||
authorizationCodeGrantFlow.setUsername("user1");
|
||||
authorizationCodeGrantFlow.addScope("message.read");
|
||||
authorizationCodeGrantFlow.addScope("message.write");
|
||||
|
||||
String state = authorizationCodeGrantFlow.authorize(registeredClient);
|
||||
String authorizationCode = authorizationCodeGrantFlow.submitConsent(registeredClient, state);
|
||||
Map<String, Object> tokenResponse = authorizationCodeGrantFlow.getTokenResponse(registeredClient, authorizationCode);
|
||||
String accessToken = (String) tokenResponse.get(OAuth2ParameterNames.ACCESS_TOKEN);
|
||||
|
||||
this.mockMvc.perform(get("/userinfo")
|
||||
.header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(header().string(HttpHeaders.CONTENT_TYPE, equalTo(MediaType.APPLICATION_JSON_VALUE)))
|
||||
.andExpect(jsonPath("sub").value("user1"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void userInfoWhenIdTokenCustomizerThenIdTokenClaimsMappedToResponse() throws Exception {
|
||||
this.spring.register(AuthorizationServerConfigWithIdTokenCustomizer.class).autowire();
|
||||
|
||||
RegisteredClient registeredClient = this.registeredClientRepository.findByClientId("messaging-client");
|
||||
assertThat(registeredClient).isNotNull();
|
||||
|
||||
AuthorizationCodeGrantFlow authorizationCodeGrantFlow = new AuthorizationCodeGrantFlow(this.mockMvc);
|
||||
authorizationCodeGrantFlow.setUsername("user1");
|
||||
authorizationCodeGrantFlow.addScope(OidcScopes.ADDRESS);
|
||||
authorizationCodeGrantFlow.addScope(OidcScopes.EMAIL);
|
||||
authorizationCodeGrantFlow.addScope(OidcScopes.PHONE);
|
||||
authorizationCodeGrantFlow.addScope(OidcScopes.PROFILE);
|
||||
|
||||
String state = authorizationCodeGrantFlow.authorize(registeredClient);
|
||||
String authorizationCode = authorizationCodeGrantFlow.submitConsent(registeredClient, state);
|
||||
Map<String, Object> tokenResponse = authorizationCodeGrantFlow.getTokenResponse(registeredClient, authorizationCode);
|
||||
String accessToken = (String) tokenResponse.get(OAuth2ParameterNames.ACCESS_TOKEN);
|
||||
|
||||
this.mockMvc.perform(get("/userinfo")
|
||||
.header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(header().string(HttpHeaders.CONTENT_TYPE, equalTo(MediaType.APPLICATION_JSON_VALUE)))
|
||||
.andExpectAll(
|
||||
jsonPath("sub").value("user1"),
|
||||
jsonPath("name").value("First Last"),
|
||||
jsonPath("given_name").value("First"),
|
||||
jsonPath("family_name").value("Last"),
|
||||
jsonPath("middle_name").value("Middle"),
|
||||
jsonPath("nickname").value("User"),
|
||||
jsonPath("preferred_username").value("user1"),
|
||||
jsonPath("profile").value("https://example.com/user1"),
|
||||
jsonPath("picture").value("https://example.com/user1.jpg"),
|
||||
jsonPath("website").value("https://example.com"),
|
||||
jsonPath("email").value("user1@example.com"),
|
||||
jsonPath("email_verified").value("true"),
|
||||
jsonPath("gender").value("female"),
|
||||
jsonPath("birthdate").value("1970-01-01"),
|
||||
jsonPath("zoneinfo").value("Europe/Paris"),
|
||||
jsonPath("locale").value("en-US"),
|
||||
jsonPath("phone_number").value("+1 (604) 555-1234;ext=5678"),
|
||||
jsonPath("phone_number_verified").value("false"),
|
||||
jsonPath("address.formatted").value("Champ de Mars\n5 Av. Anatole France\n75007 Paris\nFrance"),
|
||||
jsonPath("updated_at").value("1970-01-01T00:00:00Z")
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void userInfoWhenUserInfoMapperThenClaimsMappedToResponse() throws Exception {
|
||||
this.spring.register(AuthorizationServerConfigWithJwtTokenCustomizer.class).autowire();
|
||||
|
||||
RegisteredClient registeredClient = this.registeredClientRepository.findByClientId("messaging-client");
|
||||
assertThat(registeredClient).isNotNull();
|
||||
|
||||
AuthorizationCodeGrantFlow authorizationCodeGrantFlow = new AuthorizationCodeGrantFlow(this.mockMvc);
|
||||
authorizationCodeGrantFlow.setUsername("user1");
|
||||
authorizationCodeGrantFlow.addScope("message.read");
|
||||
authorizationCodeGrantFlow.addScope("message.write");
|
||||
|
||||
String state = authorizationCodeGrantFlow.authorize(registeredClient);
|
||||
String authorizationCode = authorizationCodeGrantFlow.submitConsent(registeredClient, state);
|
||||
Map<String, Object> tokenResponse = authorizationCodeGrantFlow.getTokenResponse(registeredClient, authorizationCode);
|
||||
String accessToken = (String) tokenResponse.get(OAuth2ParameterNames.ACCESS_TOKEN);
|
||||
|
||||
this.mockMvc.perform(get("/userinfo")
|
||||
.header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(header().string(HttpHeaders.CONTENT_TYPE, equalTo(MediaType.APPLICATION_JSON_VALUE)))
|
||||
.andExpectAll(
|
||||
jsonPath("sub").value("user1"),
|
||||
jsonPath("claim-1").value("value-1"),
|
||||
jsonPath("claim-2").value("value-2")
|
||||
);
|
||||
}
|
||||
|
||||
@EnableWebSecurity
|
||||
@EnableAutoConfiguration
|
||||
@Import(EnableUserInfoSecurityConfig.class)
|
||||
static class AuthorizationServerConfig {
|
||||
|
||||
}
|
||||
|
||||
@EnableWebSecurity
|
||||
@Import({EnableUserInfoSecurityConfig.class, IdTokenCustomizerConfig.class})
|
||||
static class AuthorizationServerConfigWithIdTokenCustomizer {
|
||||
|
||||
@Bean
|
||||
public OidcUserInfoService userInfoService() {
|
||||
return new OidcUserInfoService();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@EnableWebSecurity
|
||||
@EnableAutoConfiguration
|
||||
@Import({JwtUserInfoMapperSecurityConfig.class, JwtTokenCustomizerConfig.class})
|
||||
static class AuthorizationServerConfigWithJwtTokenCustomizer {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
49
docs/src/test/java/sample/util/RegisteredClients.java
Normal file
49
docs/src/test/java/sample/util/RegisteredClients.java
Normal file
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* 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.util;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
import org.springframework.security.oauth2.core.AuthorizationGrantType;
|
||||
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
|
||||
import org.springframework.security.oauth2.core.oidc.OidcScopes;
|
||||
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
|
||||
import org.springframework.security.oauth2.server.authorization.settings.ClientSettings;
|
||||
|
||||
/**
|
||||
* @author Steve Riesenberg
|
||||
*/
|
||||
public class RegisteredClients {
|
||||
// @formatter:off
|
||||
public static RegisteredClient messagingClient() {
|
||||
return RegisteredClient.withId(UUID.randomUUID().toString())
|
||||
.clientId("messaging-client")
|
||||
.clientSecret("{noop}secret")
|
||||
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
|
||||
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
|
||||
.authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
|
||||
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
|
||||
.authorizationGrantType(AuthorizationGrantType.DEVICE_CODE)
|
||||
.redirectUri("http://127.0.0.1:8080/authorized")
|
||||
.postLogoutRedirectUri("http://127.0.0.1:8080/index")
|
||||
.scope(OidcScopes.OPENID)
|
||||
.scope("message.read")
|
||||
.scope("message.write")
|
||||
.clientSettings(ClientSettings.builder().requireAuthorizationConsent(true).build())
|
||||
.build();
|
||||
}
|
||||
// @formatter:on
|
||||
}
|
||||
Reference in New Issue
Block a user