From 048896ef7434641f31e20f78791109bd2a4b184f Mon Sep 17 00:00:00 2001 From: Steve Riesenberg Date: Thu, 4 May 2023 09:46:01 -0500 Subject: [PATCH] Add How-to: Authenticate using Social Login Closes gh-538 --- docs/spring-authorization-server-docs.gradle | 2 + .../sample/socialLogin/SecurityConfig.java | 76 ++++++ .../java/sample/socialLogin/application.yml | 23 ++ .../asciidoc/guides/how-to-social-login.adoc | 217 ++++++++++++++++++ docs/src/docs/asciidoc/how-to.adoc | 1 + ...ratedIdentityAuthenticationEntryPoint.java | 4 + ...dIdentityAuthenticationSuccessHandler.java | 4 + .../FederatedIdentityConfigurer.java | 4 + .../FederatedIdentityIdTokenCustomizer.java | 4 + .../UserRepositoryOAuth2UserHandler.java | 4 + 10 files changed, 339 insertions(+) create mode 100644 docs/src/docs/asciidoc/examples/src/main/java/sample/socialLogin/SecurityConfig.java create mode 100644 docs/src/docs/asciidoc/examples/src/main/java/sample/socialLogin/application.yml create mode 100644 docs/src/docs/asciidoc/guides/how-to-social-login.adoc diff --git a/docs/spring-authorization-server-docs.gradle b/docs/spring-authorization-server-docs.gradle index fd180ddb..ce3e644d 100644 --- a/docs/spring-authorization-server-docs.gradle +++ b/docs/spring-authorization-server-docs.gradle @@ -7,7 +7,9 @@ asciidoctor { "spring-authorization-server-version": project.version, "spring-security-reference-base-url": "https://docs.spring.io/spring-security/reference", "spring-security-api-base-url": "https://docs.spring.io/spring-security/site/docs/current/api", + "spring-boot-reference-base-url": "https://docs.spring.io/spring-boot/docs/current/reference/html", "examples-dir": "examples", + "samples-dir": "$rootDir/samples", "docs-java": "$sourceDir/examples/src/main/java", "chomp": "default headers packages", "toc": "left", diff --git a/docs/src/docs/asciidoc/examples/src/main/java/sample/socialLogin/SecurityConfig.java b/docs/src/docs/asciidoc/examples/src/main/java/sample/socialLogin/SecurityConfig.java new file mode 100644 index 00000000..7583ae0b --- /dev/null +++ b/docs/src/docs/asciidoc/examples/src/main/java/sample/socialLogin/SecurityConfig.java @@ -0,0 +1,76 @@ +/* + * Copyright 2020-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package sample.socialLogin; + +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; + +@Configuration +@EnableWebSecurity +public class SecurityConfig { + + @Bean // <1> + @Order(1) + public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) + throws Exception { + OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http); + http.getConfigurer(OAuth2AuthorizationServerConfigurer.class) + .oidc(Customizer.withDefaults()); // Enable OpenID Connect 1.0 + // @formatter:off + http + // Redirect to the OAuth 2.0 Login endpoint when not authenticated + // from the authorization endpoint + .exceptionHandling((exceptions) -> exceptions + .defaultAuthenticationEntryPointFor( // <2> + new LoginUrlAuthenticationEntryPoint("/oauth2/authorization/my-client"), + new MediaTypeRequestMatcher(MediaType.TEXT_HTML) + ) + ) + // Accept access tokens for User Info and/or Client Registration + .oauth2ResourceServer((oauth2) -> oauth2.jwt(Customizer.withDefaults())); + // @formatter:on + + return http.build(); + } + + @Bean // <3> + @Order(2) + public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) + throws Exception { + // @formatter:off + http + .authorizeHttpRequests((authorize) -> authorize + .anyRequest().authenticated() + ) + // OAuth2 Login handles the redirect to the OAuth 2.0 Login endpoint + // from the authorization server filter chain + .oauth2Login(Customizer.withDefaults()); // <4> + // @formatter:on + + return http.build(); + } + +} diff --git a/docs/src/docs/asciidoc/examples/src/main/java/sample/socialLogin/application.yml b/docs/src/docs/asciidoc/examples/src/main/java/sample/socialLogin/application.yml new file mode 100644 index 00000000..5e28adfc --- /dev/null +++ b/docs/src/docs/asciidoc/examples/src/main/java/sample/socialLogin/application.yml @@ -0,0 +1,23 @@ +okta: + base-url: ${OKTA_BASE_URL} + +spring: + security: + oauth2: + client: + registration: + my-client: + provider: okta + client-id: ${OKTA_CLIENT_ID} + client-secret: ${OKTA_CLIENT_SECRET} + scope: + - openid + - profile + - email + provider: + okta: + authorization-uri: ${okta.base-url}/oauth2/v1/authorize + token-uri: ${okta.base-url}/oauth2/v1/token + user-info-uri: ${okta.base-url}/oauth2/v1/userinfo + jwk-set-uri: ${okta.base-url}/oauth2/v1/keys + user-name-attribute: sub \ No newline at end of file diff --git a/docs/src/docs/asciidoc/guides/how-to-social-login.adoc b/docs/src/docs/asciidoc/guides/how-to-social-login.adoc new file mode 100644 index 00000000..755aa14f --- /dev/null +++ b/docs/src/docs/asciidoc/guides/how-to-social-login.adoc @@ -0,0 +1,217 @@ +[[how-to-social-login]] += How-to: Authenticate using Social Login +:index-link: ../how-to.html +:docs-dir: .. +:examples-dir: {docs-dir}/examples +:samples-dir: {docs-dir}/../../../../samples +:github-ref: main +:github-base-url: https://github.com/spring-projects/spring-authorization-server/blob/{github-ref} + +This guide shows how to configure xref:{docs-dir}/index.adoc#top[Spring Authorization Server] with a social login provider (such as Google, GitHub, etc.) for {spring-security-reference-base-url}/servlet/authentication/index.html[authentication]. +The purpose of this guide is to demonstrate how to replace {spring-security-reference-base-url}/servlet/authentication/passwords/form.html[Form Login] with {spring-security-reference-base-url}/servlet/oauth2/login/index.html[OAuth 2.0 Login]. + +NOTE: Spring Authorization Server is built on {spring-security-reference-base-url}/index.html[Spring Security] and we will be using Spring Security concepts throughout this guide. + +* <> +* <> +* <> + +[[register-social-login-provider]] +== Register with Social Login Provider + +To get started, you will need to set up an application with your chosen social login provider. +Common providers include: + +* https://developers.google.com/identity/openid-connect/openid-connect#appsetup[Google] +* https://github.com/settings/developers[GitHub] +* https://developers.facebook.com/apps[Facebook] +* https://www.okta.com/developer/signup[Okta] + +Follow the steps for your provider until you are asked to specify a Redirect URI. +To set up a Redirect URI, choose a `registrationId` (such as `google`, `my-client` or any other unique identifier you wish) which you will use to configure both Spring Security **and** your provider. + +NOTE: The `registrationId` is a unique identifier for the `ClientRegistration` in Spring Security. The default Redirect URI template is `\{baseUrl\}/login/oauth2/code/\{registrationId\}`. See {spring-security-reference-base-url}/servlet/oauth2/login/core.html#oauth2login-sample-redirect-uri[Setting the Redirect URI] in the Spring Security reference for more information. + +TIP: For example, testing locally on port `9000` with a `registrationId` of `google`, your Redirect URI would be `http://localhost:9000/login/oauth2/code/google`. Enter this value as the Redirect URI when setting up the application with your provider. + +Once you've completed the set-up process with your social login provider, you should have obtained credentials (a Client ID and Client Secret). +In addition, you will need to reference the provider's documentation and take note of the following values: + +* **Authorization URI**: The endpoint that is used to initiate the `authorization_code` flow at the provider. +* **Token URI**: The endpoint that is used to exchange an `authorization_code` for an `access_token` and optionally an `id_token`. +* **JWK Set URI**: The endpoint that is used to obtain keys for verifying the signature of a JWT, which is required when an `id_token` is available. +* **User Info URI**: The endpoint that is used to obtain user information, which is required when an `id_token` is not available. +* **User Name Attribute**: The claim in either the `id_token` or the User Info Response containing the username of the user. + +[[configure-oauth2-login]] +== Configure OAuth 2.0 Login + +Once you've <> with a social login provider, you can proceed to configuring Spring Security for {spring-security-reference-base-url}/servlet/oauth2/login/index.html[OAuth 2.0 Login]. + +* <> +* <> +* <> + +[[configure-oauth2-login-dependency]] +=== Add OAuth2 Client Dependency + +First, add the following dependency: + +[[configure-oauth2-login-maven-dependency]] +.Maven +[source,xml,role="primary",subs="attributes,verbatim"] +---- + + org.springframework.boot + spring-boot-starter-oauth2-client + +---- + +[[configure-oauth2-login-gradle-dependency]] +.Gradle +[source,gradle,role="secondary",subs="attributes,verbatim"] +---- +implementation "org.springframework.boot:spring-boot-starter-oauth2-client" +---- + +[[configure-oauth2-login-client-registration]] +=== Register a Client + +Next, configure the `ClientRegistration` with the values obtained <>. +Using Okta as an example, configure the following properties: + +[[configure-oauth2-login-okta-example]] +.application.yml +[source,yaml] +---- +include::{examples-dir}/src/main/java/sample/socialLogin/application.yml[] +---- + +NOTE: The `registrationId` in the above example is `my-client`. + +TIP: The above example demonstrates the *recommended* way to set the Provider URL, Client ID and Client Secret using environment variables (`OKTA_BASE_URL`, `OKTA_CLIENT_ID` and `OKTA_CLIENT_SECRET`). See {spring-boot-reference-base-url}/features.html#features.external-config[Externalized Configuration] in the Spring Boot reference for more information. + +This simple example demonstrates a typical configuration, but some providers will require additional configuration. +For more information about configuring the `ClientRegistration`, see {spring-security-reference-base-url}/servlet/oauth2/login/core.html#oauth2login-boot-property-mappings[Spring Boot Property Mappings] in the Spring Security reference. + +[[configure-oauth2-login-authentication]] +=== Configure Authentication + +Finally, to configure Spring Authorization Server to use a social login provider for authentication, you can use `oauth2Login()` instead of `formLogin()`. +You can also automatically redirect an unauthenticated user to the provider by configuring `exceptionHandling()` with an `AuthenticationEntryPoint`. + +Continuing our <>, configure Spring Security using a `@Configuration` as in the following example: + +.Configure OAuth 2.0 Login +[source,java] +---- +include::{examples-dir}/src/main/java/sample/socialLogin/SecurityConfig.java[] +---- + +<1> A Spring Security filter chain for the xref:protocol-endpoints.adoc[Protocol Endpoints]. +<2> Configure an `AuthenticationEntryPoint` for redirecting to the {spring-security-reference-base-url}/servlet/oauth2/login/advanced.html#oauth2login-advanced-login-page[OAuth 2.0 Login endpoint]. +<3> A Spring Security filter chain for https://docs.spring.io/spring-security/reference/servlet/authentication/index.html[authentication]. +<4> Configure {spring-security-reference-base-url}/servlet/oauth2/login/index.html[OAuth 2.0 Login] for authentication. + +If you configured a `UserDetailsService` when xref:{docs-dir}/getting-started.adoc#developing-your-first-application[getting started], you can remove it now. + +[[advanced-use-cases]] +== Advanced Use Cases + +The https://github.com/spring-projects/spring-authorization-server/tree/{github-ref}/samples#demo-sample[demo authorization server sample^] demonstrates advanced configuration options for federating identity providers. +Select from the following use cases to see an example of each: + +* I want to <> +* I want to <> +* I want to <> +* I want to <> + +[[advanced-use-cases-automatically-redirect]] +=== Automatically Redirect to a Provider + +The following example `AuthenticationEntryPoint` uses a query parameter as a hint from the client to indicate which provider to automatically redirect to for authentication. +For example, assuming Google is configured as a social login provider with a `registrationId` of `google`, a request to `/oauth2/authorize?idp=google&...` will redirect an unauthenticated user to `/oauth2/authorization/google` which will initiate logging in with Google: + +.`FederatedIdentityAuthenticationEntryPoint` +[source,java] +---- +include::{samples-dir}/demo-authorizationserver/src/main/java/sample/federation/FederatedIdentityAuthenticationEntryPoint.java[] +---- + +[[advanced-use-cases-capture-users]] +=== Capture Users in a Database + +The following example `AuthenticationSuccessHandler` uses a custom component to capture users in a local database when they first log in: + +.`FederatedIdentityAuthenticationSuccessHandler` +[source,java] +---- +include::{samples-dir}/demo-authorizationserver/src/main/java/sample/federation/FederatedIdentityAuthenticationSuccessHandler.java[] +---- + +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. +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[] +---- + +[[advanced-use-cases-map-claims]] +=== Map Claims to an ID Token + +The following example `OAuth2TokenCustomizer` maps a user's claims from an authentication provider to the `id_token` produced by Spring Authorization Server: + +.`FederatedIdentityIdTokenCustomizer` +[source,java] +---- +include::{samples-dir}/demo-authorizationserver/src/main/java/sample/federation/FederatedIdentityIdTokenCustomizer.java[] +---- + +You can configure Spring Authorization Server to use this customizer by publishing it as a `@Bean` as in the following example: + +.Configure `FederatedIdentityIdTokenCustomizer` +[source,java] +---- +@Bean +public OAuth2TokenCustomizer idTokenCustomizer() { + return new FederatedIdentityIdTokenCustomizer(); +} +---- + +[[advanced-use-cases-configurer]] +=== Create My Own Configurer + +The following example `SecurityConfigurer` combines configuration for all of the above examples into a single reusable component: + +.`FederatedIdentityConfigurer` +[source,java] +---- +include::{samples-dir}/demo-authorizationserver/src/main/java/sample/federation/FederatedIdentityConfigurer.java[] +---- + +The configurer can be applied using the Spring Security DSL as in the following example: + +.Apply Configurer +[source,java] +---- +http.apply(new FederatedIdentityConfigurer()); +---- + +The configurer also has its own DSL to customize the defaults. +Here's a full example: + +.Customize using Configurer +[source,java] +---- +http.apply(new FederatedIdentityConfigurer()) + .loginPageUrl("/social/login") + .authorizationRequestUri("/social/login/{registrationId}") + .oauth2UserHandler((oauth2User) -> { + // TODO: Handle login of an OAuth2 user... + }) + .oidcUserHandler((oidcUser) -> { + // TODO: Handle login of an OIDC user... + }); +---- diff --git a/docs/src/docs/asciidoc/how-to.adoc b/docs/src/docs/asciidoc/how-to.adoc index 6d82989b..56cae8a9 100644 --- a/docs/src/docs/asciidoc/how-to.adoc +++ b/docs/src/docs/asciidoc/how-to.adoc @@ -4,5 +4,6 @@ [[how-to-overview]] == List of Guides +* 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] diff --git a/samples/demo-authorizationserver/src/main/java/sample/federation/FederatedIdentityAuthenticationEntryPoint.java b/samples/demo-authorizationserver/src/main/java/sample/federation/FederatedIdentityAuthenticationEntryPoint.java index bbf2c40f..54efecb3 100644 --- a/samples/demo-authorizationserver/src/main/java/sample/federation/FederatedIdentityAuthenticationEntryPoint.java +++ b/samples/demo-authorizationserver/src/main/java/sample/federation/FederatedIdentityAuthenticationEntryPoint.java @@ -15,6 +15,7 @@ */ package sample.federation; +// tag::imports[] import java.io.IOException; import jakarta.servlet.ServletException; @@ -31,6 +32,7 @@ import org.springframework.security.web.DefaultRedirectStrategy; import org.springframework.security.web.RedirectStrategy; import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint; import org.springframework.web.util.UriComponentsBuilder; +// end::imports[] /** * An {@link AuthenticationEntryPoint} for initiating the login flow to an @@ -40,6 +42,7 @@ import org.springframework.web.util.UriComponentsBuilder; * @author Steve Riesenberg * @since 1.1 */ +// tag::class[] public final class FederatedIdentityAuthenticationEntryPoint implements AuthenticationEntryPoint { private final RedirectStrategy redirectStrategy = new DefaultRedirectStrategy(); @@ -80,3 +83,4 @@ public final class FederatedIdentityAuthenticationEntryPoint implements Authenti } } +// end::class[] diff --git a/samples/demo-authorizationserver/src/main/java/sample/federation/FederatedIdentityAuthenticationSuccessHandler.java b/samples/demo-authorizationserver/src/main/java/sample/federation/FederatedIdentityAuthenticationSuccessHandler.java index 496c6000..ed4c2409 100644 --- a/samples/demo-authorizationserver/src/main/java/sample/federation/FederatedIdentityAuthenticationSuccessHandler.java +++ b/samples/demo-authorizationserver/src/main/java/sample/federation/FederatedIdentityAuthenticationSuccessHandler.java @@ -15,6 +15,7 @@ */ package sample.federation; +// tag::imports[] import java.io.IOException; import java.util.function.Consumer; @@ -28,6 +29,7 @@ import org.springframework.security.oauth2.core.oidc.user.OidcUser; import org.springframework.security.oauth2.core.user.OAuth2User; import org.springframework.security.web.authentication.AuthenticationSuccessHandler; import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler; +// end::imports[] /** * An {@link AuthenticationSuccessHandler} for capturing the {@link OidcUser} or @@ -36,6 +38,7 @@ import org.springframework.security.web.authentication.SavedRequestAwareAuthenti * @author Steve Riesenberg * @since 1.1 */ +// tag::class[] public final class FederatedIdentityAuthenticationSuccessHandler implements AuthenticationSuccessHandler { private final AuthenticationSuccessHandler delegate = new SavedRequestAwareAuthenticationSuccessHandler(); @@ -66,3 +69,4 @@ public final class FederatedIdentityAuthenticationSuccessHandler implements Auth } } +// end::class[] diff --git a/samples/demo-authorizationserver/src/main/java/sample/federation/FederatedIdentityConfigurer.java b/samples/demo-authorizationserver/src/main/java/sample/federation/FederatedIdentityConfigurer.java index fea57679..13d4ba02 100644 --- a/samples/demo-authorizationserver/src/main/java/sample/federation/FederatedIdentityConfigurer.java +++ b/samples/demo-authorizationserver/src/main/java/sample/federation/FederatedIdentityConfigurer.java @@ -15,6 +15,7 @@ */ package sample.federation; +// tag::imports[] import java.util.function.Consumer; import org.springframework.context.ApplicationContext; @@ -24,6 +25,7 @@ import org.springframework.security.oauth2.client.registration.ClientRegistratio import org.springframework.security.oauth2.core.oidc.user.OidcUser; import org.springframework.security.oauth2.core.user.OAuth2User; import org.springframework.util.Assert; +// end::imports[] /** * A configurer for setting up Federated Identity Management. @@ -31,6 +33,7 @@ import org.springframework.util.Assert; * @author Steve Riesenberg * @since 1.1 */ +// tag::class[] public final class FederatedIdentityConfigurer extends AbstractHttpConfigurer { private String loginPageUrl = "/login"; @@ -123,3 +126,4 @@ public final class FederatedIdentityConfigurer extends AbstractHttpConfigurer { private static final Set ID_TOKEN_CLAIMS = Collections.unmodifiableSet(new HashSet<>(Arrays.asList( @@ -89,3 +92,4 @@ public final class FederatedIdentityIdTokenCustomizer implements OAuth2TokenCust } } +// end::class[] diff --git a/samples/demo-authorizationserver/src/main/java/sample/federation/UserRepositoryOAuth2UserHandler.java b/samples/demo-authorizationserver/src/main/java/sample/federation/UserRepositoryOAuth2UserHandler.java index dd022289..95030dee 100644 --- a/samples/demo-authorizationserver/src/main/java/sample/federation/UserRepositoryOAuth2UserHandler.java +++ b/samples/demo-authorizationserver/src/main/java/sample/federation/UserRepositoryOAuth2UserHandler.java @@ -15,11 +15,13 @@ */ package sample.federation; +// tag::imports[] import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.function.Consumer; import org.springframework.security.oauth2.core.user.OAuth2User; +// end::imports[] /** * Example {@link Consumer} to perform JIT provisioning of an {@link OAuth2User}. @@ -27,6 +29,7 @@ import org.springframework.security.oauth2.core.user.OAuth2User; * @author Steve Riesenberg * @since 1.1 */ +// tag::class[] public final class UserRepositoryOAuth2UserHandler implements Consumer { private final UserRepository userRepository = new UserRepository(); @@ -55,3 +58,4 @@ public final class UserRepositoryOAuth2UserHandler implements Consumer