From 321080fbc2adc5ec9cc03fc617ac12baffcd9932 Mon Sep 17 00:00:00 2001 From: Steve Riesenberg Date: Fri, 5 May 2023 12:37:42 -0500 Subject: [PATCH] Add How-to: Authenticate using a Single Page Application with PKCE Closes gh-539 --- .../main/java/sample/pkce/ClientConfig.java | 56 +++++++++++ .../main/java/sample/pkce/SecurityConfig.java | 95 +++++++++++++++++++ .../src/main/java/sample/pkce/application.yml | 19 ++++ .../sample/AuthorizationCodeGrantFlow.java | 67 ++++++++++++- .../java/sample/pkce/PublicClientTests.java | 87 +++++++++++++++++ .../src/docs/asciidoc/guides/how-to-pkce.adoc | 77 +++++++++++++++ .../asciidoc/guides/how-to-social-login.adoc | 10 +- docs/src/docs/asciidoc/how-to.adoc | 1 + 8 files changed, 402 insertions(+), 10 deletions(-) create mode 100644 docs/src/docs/asciidoc/examples/src/main/java/sample/pkce/ClientConfig.java create mode 100644 docs/src/docs/asciidoc/examples/src/main/java/sample/pkce/SecurityConfig.java create mode 100644 docs/src/docs/asciidoc/examples/src/main/java/sample/pkce/application.yml create mode 100644 docs/src/docs/asciidoc/examples/src/test/java/sample/pkce/PublicClientTests.java create mode 100644 docs/src/docs/asciidoc/guides/how-to-pkce.adoc diff --git a/docs/src/docs/asciidoc/examples/src/main/java/sample/pkce/ClientConfig.java b/docs/src/docs/asciidoc/examples/src/main/java/sample/pkce/ClientConfig.java new file mode 100644 index 00000000..29ec9d8a --- /dev/null +++ b/docs/src/docs/asciidoc/examples/src/main/java/sample/pkce/ClientConfig.java @@ -0,0 +1,56 @@ +/* + * 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.UUID; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +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.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.settings.ClientSettings; + +@Configuration +public class ClientConfig { + + // tag::client[] + @Bean + public RegisteredClientRepository registeredClientRepository() { + // @formatter:off + RegisteredClient publicClient = RegisteredClient.withId(UUID.randomUUID().toString()) + .clientId("public-client") + .clientAuthenticationMethod(ClientAuthenticationMethod.NONE) + .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) + .redirectUri("http://127.0.0.1:4200") + .scope(OidcScopes.OPENID) + .scope(OidcScopes.PROFILE) + .clientSettings(ClientSettings.builder() + .requireAuthorizationConsent(true) + .requireProofKey(true) + .build() + ) + .build(); + // @formatter:on + + return new InMemoryRegisteredClientRepository(publicClient); + } + // end::client[] + +} diff --git a/docs/src/docs/asciidoc/examples/src/main/java/sample/pkce/SecurityConfig.java b/docs/src/docs/asciidoc/examples/src/main/java/sample/pkce/SecurityConfig.java new file mode 100644 index 00000000..045ddc98 --- /dev/null +++ b/docs/src/docs/asciidoc/examples/src/main/java/sample/pkce/SecurityConfig.java @@ -0,0 +1,95 @@ +/* + * 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 org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +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.server.authorization.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration; +import org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2AuthorizationServerConfigurer; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint; +import org.springframework.security.web.util.matcher.MediaTypeRequestMatcher; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.CorsConfigurationSource; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; + +@Configuration +@EnableWebSecurity +public class SecurityConfig { + + @Bean + @Order(1) + public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) + throws Exception { + // @fold:on + OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http); + http.getConfigurer(OAuth2AuthorizationServerConfigurer.class) + .oidc(Customizer.withDefaults()); // Enable OpenID Connect 1.0 + // @formatter:off + http + // Redirect to the login page when not authenticated from the + // authorization endpoint + .exceptionHandling((exceptions) -> exceptions + .defaultAuthenticationEntryPointFor( + new LoginUrlAuthenticationEntryPoint("/login"), + new MediaTypeRequestMatcher(MediaType.TEXT_HTML) + ) + ) + // Accept access tokens for User Info and/or Client Registration + .oauth2ResourceServer((oauth2) -> oauth2.jwt(Customizer.withDefaults())); + // @formatter:on + + // @fold:off + return http.cors(Customizer.withDefaults()).build(); + } + + @Bean + @Order(2) + public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) + throws Exception { + // @fold:on + // @formatter:off + http + .authorizeHttpRequests((authorize) -> authorize + .anyRequest().authenticated() + ) + // Form login handles the redirect to the login page from the + // authorization server filter chain + .formLogin(Customizer.withDefaults()); + // @formatter:on + + // @fold:off + return http.cors(Customizer.withDefaults()).build(); + } + + @Bean + public CorsConfigurationSource corsConfigurationSource() { + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + CorsConfiguration config = new CorsConfiguration(); + config.addAllowedHeader("*"); + config.addAllowedMethod("*"); + config.addAllowedOrigin("http://127.0.0.1:4200"); + config.setAllowCredentials(true); + source.registerCorsConfiguration("/**", config); + return source; + } + +} \ No newline at end of file diff --git a/docs/src/docs/asciidoc/examples/src/main/java/sample/pkce/application.yml b/docs/src/docs/asciidoc/examples/src/main/java/sample/pkce/application.yml new file mode 100644 index 00000000..4c6f8b57 --- /dev/null +++ b/docs/src/docs/asciidoc/examples/src/main/java/sample/pkce/application.yml @@ -0,0 +1,19 @@ +spring: + security: + oauth2: + authorizationserver: + client: + public-client: + registration: + client-id: "public-client" + client-authentication-methods: + - "none" + authorization-grant-types: + - "authorization_code" + redirect-uris: + - "http://127.0.0.1:4200" + scopes: + - "openid" + - "profile" + require-authorization-consent: true + require-proof-key: true diff --git a/docs/src/docs/asciidoc/examples/src/test/java/sample/AuthorizationCodeGrantFlow.java b/docs/src/docs/asciidoc/examples/src/test/java/sample/AuthorizationCodeGrantFlow.java index e66bb000..9c35a4db 100644 --- a/docs/src/docs/asciidoc/examples/src/test/java/sample/AuthorizationCodeGrantFlow.java +++ b/docs/src/docs/asciidoc/examples/src/test/java/sample/AuthorizationCodeGrantFlow.java @@ -31,6 +31,7 @@ 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; @@ -85,6 +86,17 @@ public class AuthorizationCodeGrantFlow { * @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 additionalParameters) throws Exception { MultiValueMap parameters = new LinkedMultiValueMap<>(); parameters.set(OAuth2ParameterNames.RESPONSE_TYPE, OAuth2AuthorizationResponseType.CODE.getValue()); parameters.set(OAuth2ParameterNames.CLIENT_ID, registeredClient.getClientId()); @@ -92,13 +104,18 @@ public class AuthorizationCodeGrantFlow { 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); @@ -120,14 +137,16 @@ public class AuthorizationCodeGrantFlow { 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("http://127.0.0.1:8080/\\S+\\?code=.{15,}&state=state"); + assertThat(redirectedUrl).matches("\\S+\\?code=.{15,}&state=state"); String locationHeader = URLDecoder.decode(redirectedUrl, StandardCharsets.UTF_8.name()); UriComponents uriComponents = UriComponentsBuilder.fromUriString(locationHeader).build(); @@ -143,29 +162,67 @@ public class AuthorizationCodeGrantFlow { * @return The token response */ public Map 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 getTokenResponse(RegisteredClient registeredClient, String authorizationCode, MultiValueMap additionalParameters) throws Exception { MultiValueMap 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); + } - HttpHeaders basicAuth = new HttpHeaders(); - basicAuth.setBasicAuth(registeredClient.getClientId(), "secret"); + 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(basicAuth)) + .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(jsonPath("$.refresh_token").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 withCodeChallenge() { + MultiValueMap parameters = new LinkedMultiValueMap<>(); + parameters.set(PkceParameterNames.CODE_CHALLENGE, "BqZZ8pTVLsiA3t3tDOys2flJTSH7LoL3Pp5ZqM_YOnE"); + parameters.set(PkceParameterNames.CODE_CHALLENGE_METHOD, "S256"); + return parameters; + } + + public static MultiValueMap withCodeVerifier() { + MultiValueMap parameters = new LinkedMultiValueMap<>(); + parameters.set(PkceParameterNames.CODE_VERIFIER, "yZ6eB-lEB4BBhIzqoDPqXTTATC0Vkgov7qDF8ar2qT4"); + return parameters; + } } diff --git a/docs/src/docs/asciidoc/examples/src/test/java/sample/pkce/PublicClientTests.java b/docs/src/docs/asciidoc/examples/src/test/java/sample/pkce/PublicClientTests.java new file mode 100644 index 00000000..317ae92e --- /dev/null +++ b/docs/src/docs/asciidoc/examples/src/test/java/sample/pkce/PublicClientTests.java @@ -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 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 { + + } + +} diff --git a/docs/src/docs/asciidoc/guides/how-to-pkce.adoc b/docs/src/docs/asciidoc/guides/how-to-pkce.adoc new file mode 100644 index 00000000..ff7c040f --- /dev/null +++ b/docs/src/docs/asciidoc/guides/how-to-pkce.adoc @@ -0,0 +1,77 @@ +[[how-to-pkce]] += How-to: Authenticate using a Single Page Application with PKCE +:index-link: ../how-to.html +:docs-dir: .. +:examples-dir: {docs-dir}/examples + +This guide shows how to configure xref:{docs-dir}/index.adoc#top[Spring Authorization Server] to support a Single Page Application (SPA) with Proof Key for Code Exchange (PKCE). +The purpose of this guide is to demonstrate how to support a public client and require PKCE for client authentication. + +NOTE: Spring Authorization Server will not issue refresh tokens for a public client. We recommend the backend for frontend (BFF) pattern as an alternative to exposing a public client. See https://github.com/spring-projects/spring-authorization-server/issues/297#issue-896744390[gh-297] for more information. + +* <> +* <> +* <> + +[[enable-cors]] +== Enable CORS + +A SPA consists of static resources that can be deployed in a variety of ways. +It can be deployed separately from the backend such as with a CDN or separate web server, or it can be deployed along side the backend using Spring Boot. + +When a SPA is hosted under a different domain, Cross Origin Resource Sharing (CORS) can be used to allow the application to communicate with the backend. + +For example, if you have an Angular dev server running locally on port `4200`, you can define a `CorsConfigurationSource` `@Bean` and configure Spring Security to allow pre-flight requests using the `cors()` DSL as in the following example: + +[[enable-cors-configuration]] +.Enable CORS +[source,java] +---- +include::{examples-dir}/src/main/java/sample/pkce/SecurityConfig.java[] +---- + +TIP: Click on the "Expand folded text" icon in the code sample above to display the full example. + +[[configure-public-client]] +== Configure a Public Client + +A SPA cannot securely store credentials and therefore must be treated as a https://datatracker.ietf.org/doc/html/rfc6749#section-2.1[public client^]. +Public clients should be required to use https://datatracker.ietf.org/doc/html/rfc7636#section-4[Proof Key for Code Exchange] (PKCE). + +Continuing the <> example, you can configure Spring Authorization Server to support a public client using the Client Authentication Method `none` and require PKCE as in the following example: + +[[configure-public-client-example]] +.Yaml +[source,yaml,role="primary"] +---- +include::{examples-dir}/src/main/java/sample/pkce/application.yml[] +---- + +.Java +[source,java,role="secondary"] +---- +include::{examples-dir}/src/main/java/sample/pkce/ClientConfig.java[tag=client,indent=0] +---- + +NOTE: The `requireProofKey` setting is helpful in situations where you forget to include the `code_challenge` and `code_challenge_method` query parameters because you will receive an error indicating PKCE is required during the xref:{docs-dir}/protocol-endpoints.adoc#oauth2-authorization-endpoint[Authorization Request] instead of a general client authentication error during the xref:{docs-dir}/protocol-endpoints.adoc#oauth2-token-endpoint[Token Request]. + +[[authenticate-with-client]] +== Authenticate with the Client + +Once the server is configured to support a public client, a common question is: _How do I authenticate the client and get an access token?_ +The short answer is: The same way you would with any other client. + +NOTE: A SPA is a browser-based application and therefore uses the same redirection-based flow as any other client. This question is usually related to an expectation that authentication can be performed via a REST API, which is not the case with OAuth2. + +A more detailed answer requires an understanding of the flow(s) involved in OAuth2 and OpenID Connect, in this case the Authorization Code flow. +The steps of the Authorization Code flow are as follows: + +1. The client initiates an OAuth2 request via a redirect to the xref:{docs-dir}/protocol-endpoints.adoc#oauth2-authorization-endpoint[Authorization Endpoint]. For a public client, this step includes generating the `code_verifier` and calculating the `code_challenge`, which is then sent as a query parameter. +2. If the user is not authenticated, the authorization server will redirect to the login page. After authentication, the user is redirected back to the Authorization Endpoint again. +3. If the user has not consented to the requested scope(s) and consent is required, the consent page is displayed. +4. Once the user has consented, the authorization server generates an `authorization_code` and redirects back to the client via the `redirect_uri`. +5. The client obtains the `authorization_code` via a query parameter and performs a request to the xref:{docs-dir}/protocol-endpoints.adoc#oauth2-token-endpoint[Token Endpoint]. For a public client, this step includes sending the `code_verifier` parameter instead of credentials for authentication. + +As you can see, the flow is fairly involved and this overview only scratches the surface. + +TIP: It is recommended that you use a robust client-side library supported by your single-page app framework to handle the Authorization Code flow. diff --git a/docs/src/docs/asciidoc/guides/how-to-social-login.adoc b/docs/src/docs/asciidoc/guides/how-to-social-login.adoc index 755aa14f..2b07a48e 100644 --- a/docs/src/docs/asciidoc/guides/how-to-social-login.adoc +++ b/docs/src/docs/asciidoc/guides/how-to-social-login.adoc @@ -135,7 +135,7 @@ For example, assuming Google is configured as a social login provider with a `re .`FederatedIdentityAuthenticationEntryPoint` [source,java] ---- -include::{samples-dir}/demo-authorizationserver/src/main/java/sample/federation/FederatedIdentityAuthenticationEntryPoint.java[] +include::{samples-dir}/demo-authorizationserver/src/main/java/sample/federation/FederatedIdentityAuthenticationEntryPoint.java[tags=imports;class] ---- [[advanced-use-cases-capture-users]] @@ -146,7 +146,7 @@ The following example `AuthenticationSuccessHandler` uses a custom component to .`FederatedIdentityAuthenticationSuccessHandler` [source,java] ---- -include::{samples-dir}/demo-authorizationserver/src/main/java/sample/federation/FederatedIdentityAuthenticationSuccessHandler.java[] +include::{samples-dir}/demo-authorizationserver/src/main/java/sample/federation/FederatedIdentityAuthenticationSuccessHandler.java[tags=imports;class] ---- Using the `AuthenticationSuccessHandler` above, you can plug in your own `Consumer` that can capture users in a database or other data store for concepts like Federated Account Linking or JIT Account Provisioning. @@ -155,7 +155,7 @@ Here is an example that simply stores users in-memory: .`UserRepositoryOAuth2UserHandler` [source,java] ---- -include::{samples-dir}/demo-authorizationserver/src/main/java/sample/federation/UserRepositoryOAuth2UserHandler.java[] +include::{samples-dir}/demo-authorizationserver/src/main/java/sample/federation/UserRepositoryOAuth2UserHandler.java[tags=imports;class] ---- [[advanced-use-cases-map-claims]] @@ -166,7 +166,7 @@ The following example `OAuth2TokenCustomizer` maps a user's claims from an authe .`FederatedIdentityIdTokenCustomizer` [source,java] ---- -include::{samples-dir}/demo-authorizationserver/src/main/java/sample/federation/FederatedIdentityIdTokenCustomizer.java[] +include::{samples-dir}/demo-authorizationserver/src/main/java/sample/federation/FederatedIdentityIdTokenCustomizer.java[tags=imports;class] ---- You can configure Spring Authorization Server to use this customizer by publishing it as a `@Bean` as in the following example: @@ -188,7 +188,7 @@ The following example `SecurityConfigurer` combines configuration for all of the .`FederatedIdentityConfigurer` [source,java] ---- -include::{samples-dir}/demo-authorizationserver/src/main/java/sample/federation/FederatedIdentityConfigurer.java[] +include::{samples-dir}/demo-authorizationserver/src/main/java/sample/federation/FederatedIdentityConfigurer.java[tags=imports;class] ---- The configurer can be applied using the Spring Security DSL as in the following example: diff --git a/docs/src/docs/asciidoc/how-to.adoc b/docs/src/docs/asciidoc/how-to.adoc index 56cae8a9..a5d22d16 100644 --- a/docs/src/docs/asciidoc/how-to.adoc +++ b/docs/src/docs/asciidoc/how-to.adoc @@ -4,6 +4,7 @@ [[how-to-overview]] == List of Guides +* xref:guides/how-to-pkce.adoc[Authenticate using a Single Page Application with PKCE] * xref:guides/how-to-social-login.adoc[Authenticate using Social Login] * xref:guides/how-to-userinfo.adoc[Customize the OpenID Connect 1.0 UserInfo response] * xref:guides/how-to-jpa.adoc[Implement core services with JPA]