Add How-to: Authenticate using a Single Page Application with PKCE
Closes gh-539
This commit is contained in:
@@ -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[]
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
@@ -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<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 {
|
||||
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<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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
Normal file
77
docs/src/docs/asciidoc/guides/how-to-pkce.adoc
Normal file
@@ -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.
|
||||
@@ -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<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:
|
||||
.`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:
|
||||
|
||||
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user