Browse Source

Add How-to: Authenticate using a Single Page Application with PKCE

Closes gh-539
pull/1210/head
Steve Riesenberg 3 years ago
parent
commit
321080fbc2
No known key found for this signature in database
GPG Key ID: 5F311AB48A55D521
  1. 56
      docs/src/docs/asciidoc/examples/src/main/java/sample/pkce/ClientConfig.java
  2. 95
      docs/src/docs/asciidoc/examples/src/main/java/sample/pkce/SecurityConfig.java
  3. 19
      docs/src/docs/asciidoc/examples/src/main/java/sample/pkce/application.yml
  4. 67
      docs/src/docs/asciidoc/examples/src/test/java/sample/AuthorizationCodeGrantFlow.java
  5. 87
      docs/src/docs/asciidoc/examples/src/test/java/sample/pkce/PublicClientTests.java
  6. 77
      docs/src/docs/asciidoc/guides/how-to-pkce.adoc
  7. 10
      docs/src/docs/asciidoc/guides/how-to-social-login.adoc
  8. 1
      docs/src/docs/asciidoc/how-to.adoc

56
docs/src/docs/asciidoc/examples/src/main/java/sample/pkce/ClientConfig.java

@ -0,0 +1,56 @@ @@ -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[]
}

95
docs/src/docs/asciidoc/examples/src/main/java/sample/pkce/SecurityConfig.java

@ -0,0 +1,95 @@ @@ -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;
}
}

19
docs/src/docs/asciidoc/examples/src/main/java/sample/pkce/application.yml

@ -0,0 +1,19 @@ @@ -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

67
docs/src/docs/asciidoc/examples/src/test/java/sample/AuthorizationCodeGrantFlow.java

@ -31,6 +31,7 @@ import org.springframework.http.MediaType; @@ -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 { @@ -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<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());
@ -92,13 +104,18 @@ public class AuthorizationCodeGrantFlow { @@ -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 { @@ -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 { @@ -143,29 +162,67 @@ public class AuthorizationCodeGrantFlow {
* @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);
}
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<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;
}
}

87
docs/src/docs/asciidoc/examples/src/test/java/sample/pkce/PublicClientTests.java

@ -0,0 +1,87 @@ @@ -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 {
}
}

77
docs/src/docs/asciidoc/guides/how-to-pkce.adoc

@ -0,0 +1,77 @@ @@ -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>>
* <<configure-public-client>>
* <<authenticate-with-client>>
[[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 <<enable-cors-configuration,earlier>> 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.

10
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 @@ -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 @@ -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<OAuth2User>` 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: @@ -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 @@ -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 @@ -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:

1
docs/src/docs/asciidoc/how-to.adoc

@ -4,6 +4,7 @@ @@ -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]

Loading…
Cancel
Save