Migrate docs to Antora

Issue gh-1295
This commit is contained in:
Rob Winch
2023-07-03 13:36:26 -05:00
committed by Steve Riesenberg
parent ec3d4fa7e1
commit 0f0424ad2a
62 changed files with 327 additions and 198 deletions

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

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

View 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 {
}
}

View File

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

View 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
}
}

View 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() {
}
}

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

View 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 {
}
}

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

View File

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

View File

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

View 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
}