Enable public clients to exchange codes for access tokens #14
@ -21,6 +21,7 @@ import org.springframework.security.crypto.password.NoOpPasswordEncoder;
|
|||||||
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeRequestAuthenticationProvider;
|
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeRequestAuthenticationProvider;
|
||||||
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationConsentAuthenticationProvider;
|
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationConsentAuthenticationProvider;
|
||||||
import org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2AuthorizationServerConfigurer;
|
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.oauth2.server.authorization.token.JwtEncodingContext;
|
import org.springframework.security.oauth2.server.authorization.token.JwtEncodingContext;
|
||||||
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenCustomizer;
|
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenCustomizer;
|
||||||
import org.springframework.security.web.AuthenticationEntryPoint;
|
import org.springframework.security.web.AuthenticationEntryPoint;
|
||||||
@ -28,7 +29,13 @@ import org.springframework.security.web.SecurityFilterChain;
|
|||||||
import org.springframework.security.web.access.intercept.AuthorizationFilter;
|
import org.springframework.security.web.access.intercept.AuthorizationFilter;
|
||||||
import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint;
|
import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint;
|
||||||
import org.springframework.security.web.authentication.preauth.j2ee.J2eePreAuthenticatedProcessingFilter;
|
import org.springframework.security.web.authentication.preauth.j2ee.J2eePreAuthenticatedProcessingFilter;
|
||||||
|
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
||||||
import org.springframework.security.web.util.matcher.MediaTypeRequestMatcher;
|
import org.springframework.security.web.util.matcher.MediaTypeRequestMatcher;
|
||||||
|
import org.springframework.security.web.util.matcher.OrRequestMatcher;
|
||||||
|
import org.springframework.security.web.util.matcher.RequestMatcher;
|
||||||
|
import org.springframework.web.cors.CorsConfiguration;
|
||||||
|
import org.springframework.web.cors.CorsConfigurationSource;
|
||||||
|
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
|
||||||
import se.su.dsv.oauth2.shibboleth.Entitlement;
|
import se.su.dsv.oauth2.shibboleth.Entitlement;
|
||||||
import se.su.dsv.oauth2.shibboleth.ShibbolethConfigurer;
|
import se.su.dsv.oauth2.shibboleth.ShibbolethConfigurer;
|
||||||
import se.su.dsv.oauth2.shibboleth.ShibbolethTokenPopulator;
|
import se.su.dsv.oauth2.shibboleth.ShibbolethTokenPopulator;
|
||||||
@ -75,18 +82,27 @@ public class AuthorizationServer extends SpringBootServletInitializer {
|
|||||||
/// in the session and the user is redirected back to the OAuth 2.0
|
/// in the session and the user is redirected back to the OAuth 2.0
|
||||||
/// authorization endpoint which will then retrieve the authentication from
|
/// authorization endpoint which will then retrieve the authentication from
|
||||||
/// the session and allow the flow to start.
|
/// the session and allow the flow to start.
|
||||||
|
///
|
||||||
|
/// CORS is enabled on the token endpoint so that public clients can exchange
|
||||||
|
/// authorization codes for tokens from the browser using `POST`.
|
||||||
@Bean
|
@Bean
|
||||||
@Order(1)
|
@Order(1)
|
||||||
public SecurityFilterChain oauth2Endpoints(
|
public SecurityFilterChain oauth2Endpoints(
|
||||||
HttpSecurity http,
|
HttpSecurity http,
|
||||||
List<HttpSecurityCustomizer> customizer)
|
List<HttpSecurityCustomizer> customizer,
|
||||||
|
AuthorizationServerSettings authorizationServerSettings)
|
||||||
throws Exception
|
throws Exception
|
||||||
{
|
{
|
||||||
OAuth2AuthorizationServerConfigurer authorizationServerConfigurer =
|
OAuth2AuthorizationServerConfigurer authorizationServerConfigurer =
|
||||||
OAuth2AuthorizationServerConfigurer.authorizationServer();
|
OAuth2AuthorizationServerConfigurer.authorizationServer();
|
||||||
|
|
||||||
|
String tokenEndpoint = authorizationServerSettings.getTokenEndpoint();
|
||||||
|
RequestMatcher corsEnabledMatcher = new OrRequestMatcher(
|
||||||
|
authorizationServerConfigurer.getEndpointsMatcher(),
|
||||||
|
new AntPathRequestMatcher(tokenEndpoint, "OPTIONS")
|
||||||
|
);
|
||||||
http
|
http
|
||||||
.securityMatcher(authorizationServerConfigurer.getEndpointsMatcher())
|
.securityMatcher(corsEnabledMatcher)
|
||||||
.with(authorizationServerConfigurer, authorizationServer -> authorizationServer
|
.with(authorizationServerConfigurer, authorizationServer -> authorizationServer
|
||||||
.authorizationEndpoint(authorizeEndpoint -> authorizeEndpoint
|
.authorizationEndpoint(authorizeEndpoint -> authorizeEndpoint
|
||||||
.consentPage("/oauth2/consent"))
|
.consentPage("/oauth2/consent"))
|
||||||
@ -100,6 +116,7 @@ public class AuthorizationServer extends SpringBootServletInitializer {
|
|||||||
.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
||||||
.authorizeHttpRequests(authorize -> authorize
|
.authorizeHttpRequests(authorize -> authorize
|
||||||
.anyRequest().authenticated())
|
.anyRequest().authenticated())
|
||||||
|
.cors(cors -> cors.configurationSource(corsConfigurationSource(tokenEndpoint)))
|
||||||
.exceptionHandling(exceptions -> exceptions
|
.exceptionHandling(exceptions -> exceptions
|
||||||
.defaultAuthenticationEntryPointFor(
|
.defaultAuthenticationEntryPointFor(
|
||||||
new LoginUrlAuthenticationEntryPoint("/login"),
|
new LoginUrlAuthenticationEntryPoint("/login"),
|
||||||
@ -117,6 +134,16 @@ public class AuthorizationServer extends SpringBootServletInitializer {
|
|||||||
return http.build();
|
return http.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private CorsConfigurationSource corsConfigurationSource(final String tokenEndpoint) {
|
||||||
|
UrlBasedCorsConfigurationSource urlBasedCorsConfigurationSource = new UrlBasedCorsConfigurationSource();
|
||||||
|
CorsConfiguration allowPublicClients = new CorsConfiguration();
|
||||||
|
allowPublicClients.setAllowedOrigins(List.of("*"));
|
||||||
|
allowPublicClients.setAllowedMethods(List.of("POST", "OPTIONS"));
|
||||||
|
allowPublicClients.setAllowCredentials(false);
|
||||||
|
urlBasedCorsConfigurationSource.registerCorsConfiguration(tokenEndpoint, allowPublicClients);
|
||||||
|
return urlBasedCorsConfigurationSource;
|
||||||
|
}
|
||||||
|
|
||||||
/// The [OAuth2AuthorizationConsentAuthenticationProvider], which is the component
|
/// The [OAuth2AuthorizationConsentAuthenticationProvider], which is the component
|
||||||
/// that handles incoming consent, requires there to be at least one scope that is
|
/// that handles incoming consent, requires there to be at least one scope that is
|
||||||
/// approved by the user. This is a problem since we want to allow the user to give
|
/// approved by the user. This is a problem since we want to allow the user to give
|
||||||
|
|||||||
@ -18,8 +18,11 @@ import java.security.MessageDigest;
|
|||||||
import java.util.Base64;
|
import java.util.Base64;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
|
import static org.hamcrest.Matchers.containsString;
|
||||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||||
|
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.options;
|
||||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
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.jsonPath;
|
||||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrlPattern;
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrlPattern;
|
||||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||||
@ -112,4 +115,48 @@ public class PublicClientCodeFlowTest extends AbstractMetadataCodeFlowTest {
|
|||||||
.andExpect(status().isOk())
|
.andExpect(status().isOk())
|
||||||
.andExpect(jsonPath("$.refresh_token").doesNotExist());
|
.andExpect(jsonPath("$.refresh_token").doesNotExist());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void cors_preflight_on_token_endpoint() throws Exception {
|
||||||
|
mockMvc.perform(options(getTokenEndpoint())
|
||||||
|
.header("Origin", "https://some-public-client.example")
|
||||||
|
.header("Access-Control-Request-Method", "POST"))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(header().string("Access-Control-Allow-Origin", "*"))
|
||||||
|
.andExpect(header().string("Access-Control-Allow-Methods", containsString("POST")));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void cors_on_token_endpoint() throws Exception {
|
||||||
|
MessageDigest sha256 = MessageDigest.getInstance("SHA-256");
|
||||||
|
String codeVerifier = "some-new-code-verifier";
|
||||||
|
byte[] codeChallengeDigest = sha256.digest(codeVerifier.getBytes(StandardCharsets.US_ASCII));
|
||||||
|
String codeChallenge = Base64.getUrlEncoder()
|
||||||
|
.withoutPadding()
|
||||||
|
.encodeToString(codeChallengeDigest);
|
||||||
|
|
||||||
|
MvcResult mvcResult = mockMvc.perform(get(getAuthorizationEndpoint())
|
||||||
|
.with(remoteUser("random-end-user"))
|
||||||
|
.queryParam("response_type", "code")
|
||||||
|
.queryParam("client_id", newClient.clientId())
|
||||||
|
.queryParam("redirect_uri", REDIRECT_URI)
|
||||||
|
.queryParam("scope", "openid offline_access")
|
||||||
|
.queryParam("code_challenge", codeChallenge)
|
||||||
|
.queryParam("code_challenge_method", "S256"))
|
||||||
|
.andExpect(status().is3xxRedirection())
|
||||||
|
.andExpect(redirectedUrlPattern("http://localhost/public?code=*"))
|
||||||
|
.andReturn();
|
||||||
|
|
||||||
|
String code = extractCode(mvcResult.getResponse().getRedirectedUrl());
|
||||||
|
|
||||||
|
mockMvc.perform(post(getTokenEndpoint())
|
||||||
|
.header("Origin", "https://some-public-client.example")
|
||||||
|
.param("grant_type", "authorization_code")
|
||||||
|
.param("code", code)
|
||||||
|
.param("redirect_uri", REDIRECT_URI)
|
||||||
|
.param("client_id", newClient.clientId())
|
||||||
|
.param("code_verifier", codeVerifier))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(header().string("Access-Control-Allow-Origin", "*"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user