Procházet zdrojové kódy

Federated Identity sample

Issue gh-538
Issue gh-499
Issue gh-106
Steve Riesenberg před 3 roky
rodič
revize
3fe6f86659
15 změnil soubory, kde provedl 1072 přidání a 3 odebrání
  1. 128 3
      samples/README.adoc
  2. 19 0
      samples/federated-identity-authorizationserver/samples-federated-identity-authorizationserver.gradle
  3. 32 0
      samples/federated-identity-authorizationserver/src/main/java/sample/FederatedIdentityAuthorizationServerApplication.java
  4. 137 0
      samples/federated-identity-authorizationserver/src/main/java/sample/config/AuthorizationServerConfig.java
  5. 67 0
      samples/federated-identity-authorizationserver/src/main/java/sample/config/DefaultSecurityConfig.java
  6. 74 0
      samples/federated-identity-authorizationserver/src/main/java/sample/jose/Jwks.java
  7. 85 0
      samples/federated-identity-authorizationserver/src/main/java/sample/jose/KeyGeneratorUtils.java
  8. 82 0
      samples/federated-identity-authorizationserver/src/main/java/sample/security/FederatedIdentityAuthenticationEntryPoint.java
  9. 68 0
      samples/federated-identity-authorizationserver/src/main/java/sample/security/FederatedIdentityAuthenticationSuccessHandler.java
  10. 125 0
      samples/federated-identity-authorizationserver/src/main/java/sample/security/FederatedIdentityConfigurer.java
  11. 91 0
      samples/federated-identity-authorizationserver/src/main/java/sample/security/FederatedIdentityIdTokenCustomizer.java
  12. 57 0
      samples/federated-identity-authorizationserver/src/main/java/sample/security/UserRepositoryOAuth2UserHandler.java
  13. 33 0
      samples/federated-identity-authorizationserver/src/main/java/sample/web/LoginController.java
  14. 33 0
      samples/federated-identity-authorizationserver/src/main/resources/application.yml
  15. 41 0
      samples/federated-identity-authorizationserver/src/main/resources/templates/login.html

+ 128 - 3
samples/README.adoc

@@ -1,12 +1,137 @@
-= Messages Sample
+= Samples
 
-This sample integrates `spring-security-oauth2-client` and `spring-security-oauth2-resource-server` with *Spring Authorization Server*.
+[[messages-sample]]
+== Messages Sample
+
+The messages sample integrates `spring-security-oauth2-client` and `spring-security-oauth2-resource-server` with *Spring Authorization Server*.
 
 The username is `user1` and the password is `password`.
 
-== Run the Sample
+[[run-messages-sample]]
+=== Run the Sample
 
 * Run Authorization Server -> `./gradlew -b samples/default-authorizationserver/samples-default-authorizationserver.gradle bootRun`
 * Run Resource Server -> `./gradlew -b samples/messages-resource/samples-messages-resource.gradle bootRun`
 * Run Client -> `./gradlew -b samples/messages-client/samples-messages-client.gradle bootRun`
 * Go to `http://127.0.0.1:8080`
+
+[[federated-identity-sample]]
+== Federated Identity Sample
+
+The federated identity sample builds on the messages sample above, adding social login and federated identity features to *Spring Authorization Server* using custom configuration.
+
+[[google-login]]
+=== Login with Google
+
+This section shows how to configure Spring Security using Google as an Authentication Provider.
+
+[[google-initial-setup]]
+==== Initial setup
+
+To use Google's OAuth 2.0 authentication system for login, you must set up a project in the Google API Console to obtain OAuth 2.0 credentials.
+
+NOTE: https://developers.google.com/identity/protocols/OpenIDConnect[Google's OAuth 2.0 implementation] for authentication conforms to the
+https://openid.net/connect/[OpenID Connect 1.0] specification and is https://openid.net/certification/[OpenID Certified].
+
+Follow the instructions on the https://developers.google.com/identity/protocols/OpenIDConnect[OpenID Connect] page, starting in the section, "Setting up OAuth 2.0".
+
+After completing the "Obtain OAuth 2.0 credentials" instructions, you should have a new OAuth Client with credentials consisting of a Client ID and a Client Secret.
+
+[[google-redirect-uri]]
+==== Setting the redirect URI
+
+The redirect URI is the path in the application that the end-user's user-agent is redirected back to after they have authenticated with Google
+and have granted access to the OAuth Client _(created in the previous step)_ on the Consent page.
+
+In the "Set a redirect URI" sub-section, ensure that the *Authorized redirect URIs* field is set to `http://localhost:9000/login/oauth2/code/google-idp`.
+
+TIP: The default redirect URI template is `{baseUrl}/login/oauth2/code/{registrationId}`.
+The *_registrationId_* is a unique identifier for the `ClientRegistration`.
+
+[[google-application-config]]
+==== Configure application.yml
+
+Now that you have a new OAuth Client with Google, you need to configure the application to use the OAuth Client for the _authentication flow_. To do so:
+
+. Go to `application.yml` and set the following configuration:
++
+[source,yaml]
+----
+spring:
+  security:
+    oauth2:
+      client:
+        registration:	<1>
+          google-idp:	<2>
+            provider: google
+            client-id: google-client-id
+            client-secret: google-client-secret
+----
++
+.OAuth Client properties
+====
+<1> `spring.security.oauth2.client.registration` is the base property prefix for OAuth Client properties.
+<2> Following the base property prefix is the ID for the `ClientRegistration`, such as google-idp.
+====
+
+. Replace the values in the `client-id` and `client-secret` property with the OAuth 2.0 credentials you created earlier.
+Alternatively, you can set the following environment variables in the Spring Boot application:
+    * `GOOGLE_CLIENT_ID`
+    * `GOOGLE_CLIENT_SECRET`
+
+[[github-login]]
+=== Login with GitHub
+
+This section shows how to configure Spring Security using Github as an Authentication Provider.
+
+[[github-register-application]]
+==== Register OAuth application
+
+To use GitHub's OAuth 2.0 authentication system for login, you must https://github.com/settings/applications/new[Register a new OAuth application].
+
+When registering the OAuth application, ensure the *Authorization callback URL* is set to `http://localhost:9000/login/oauth2/code/github-idp`.
+
+The Authorization callback URL (redirect URI) is the path in the application that the end-user's user-agent is redirected back to after they have authenticated with GitHub
+and have granted access to the OAuth application on the _Authorize application_ page.
+
+TIP: The default redirect URI template is `{baseUrl}/login/oauth2/code/{registrationId}`.
+The *_registrationId_* is a unique identifier for the `ClientRegistration`.
+
+[[github-application-config]]
+==== Configure application.yml
+
+Now that you have a new OAuth application with GitHub, you need to configure the application to use the OAuth application for the _authentication flow_. To do so:
+
+. Go to `application.yml` and set the following configuration:
++
+[source,yaml]
+----
+spring:
+  security:
+    oauth2:
+      client:
+        registration:	<1>
+          github-idp:	<2>
+            provider: github
+            client-id: github-client-id
+            client-secret: github-client-secret
+----
++
+.OAuth Client properties
+====
+<1> `spring.security.oauth2.client.registration` is the base property prefix for OAuth Client properties.
+<2> Following the base property prefix is the ID for the `ClientRegistration`, such as github-idp.
+====
+
+. Replace the values in the `client-id` and `client-secret` property with the OAuth 2.0 credentials you created earlier.
+Alternatively, you can set the following environment variables in the Spring Boot application:
+* `GITHUB_CLIENT_ID`
+* `GITHUB_CLIENT_SECRET`
+
+[[run-federated-identity-sample]]
+=== Run the Sample
+
+* Run Authorization Server -> `./gradlew -b samples/federated-identity-authorizationserver/samples-federated-identity-authorizationserver.gradle bootRun`
+* Run Resource Server -> `./gradlew -b samples/messages-resource/samples-messages-resource.gradle bootRun`
+* Run Client -> `./gradlew -b samples/messages-client/samples-messages-client.gradle bootRun`
+* Go to `http://127.0.0.1:8080`

+ 19 - 0
samples/federated-identity-authorizationserver/samples-federated-identity-authorizationserver.gradle

@@ -0,0 +1,19 @@
+apply plugin: 'io.spring.convention.spring-sample-boot'
+
+dependencies {
+	compile 'org.springframework.boot:spring-boot-starter-web'
+	compile 'org.springframework.boot:spring-boot-starter-security'
+	compile 'org.springframework.boot:spring-boot-starter-oauth2-client'
+	compile 'org.springframework.boot:spring-boot-starter-thymeleaf'
+	compile 'org.thymeleaf.extras:thymeleaf-extras-springsecurity5'
+	compile 'org.webjars:webjars-locator-core'
+	compile 'org.webjars:bootstrap:3.4.1'
+	compile 'org.webjars:jquery:3.4.1'
+	compile 'org.springframework.boot:spring-boot-starter-jdbc'
+	compile project(':spring-security-oauth2-authorization-server')
+	runtimeOnly 'com.h2database:h2'
+
+	testCompile 'org.springframework.boot:spring-boot-starter-test'
+	testCompile 'org.springframework.security:spring-security-test'
+	testCompile 'net.sourceforge.htmlunit:htmlunit'
+}

+ 32 - 0
samples/federated-identity-authorizationserver/src/main/java/sample/FederatedIdentityAuthorizationServerApplication.java

@@ -0,0 +1,32 @@
+/*
+ * 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;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+/**
+ * @author Steve Riesenberg
+ * @since 0.2.3
+ */
+@SpringBootApplication
+public class FederatedIdentityAuthorizationServerApplication {
+
+	public static void main(String[] args) {
+		SpringApplication.run(FederatedIdentityAuthorizationServerApplication.class, args);
+	}
+
+}

+ 137 - 0
samples/federated-identity-authorizationserver/src/main/java/sample/config/AuthorizationServerConfig.java

@@ -0,0 +1,137 @@
+/*
+ * 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.config;
+
+import java.util.UUID;
+
+import com.nimbusds.jose.jwk.JWKSet;
+import com.nimbusds.jose.jwk.RSAKey;
+import com.nimbusds.jose.jwk.source.JWKSource;
+import com.nimbusds.jose.proc.SecurityContext;
+import sample.jose.Jwks;
+import sample.security.FederatedIdentityConfigurer;
+import sample.security.FederatedIdentityIdTokenCustomizer;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.core.Ordered;
+import org.springframework.core.annotation.Order;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.jdbc.datasource.embedded.EmbeddedDatabase;
+import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
+import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;
+import org.springframework.security.config.Customizer;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration;
+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.JdbcOAuth2AuthorizationConsentService;
+import org.springframework.security.oauth2.server.authorization.JdbcOAuth2AuthorizationService;
+import org.springframework.security.oauth2.server.authorization.JwtEncodingContext;
+import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsentService;
+import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
+import org.springframework.security.oauth2.server.authorization.OAuth2TokenCustomizer;
+import org.springframework.security.oauth2.server.authorization.client.JdbcRegisteredClientRepository;
+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.config.ClientSettings;
+import org.springframework.security.oauth2.server.authorization.config.ProviderSettings;
+import org.springframework.security.web.SecurityFilterChain;
+
+/**
+ * @author Steve Riesenberg
+ * @since 0.2.3
+ */
+@Configuration(proxyBeanMethods = false)
+public class AuthorizationServerConfig {
+
+	@Bean
+	@Order(Ordered.HIGHEST_PRECEDENCE)
+	public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
+		OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
+		http.apply(new FederatedIdentityConfigurer());
+		return http.formLogin(Customizer.withDefaults()).build();
+	}
+
+	@Bean
+	public OAuth2TokenCustomizer<JwtEncodingContext> idTokenCustomizer() {
+		return new FederatedIdentityIdTokenCustomizer();
+	}
+
+	// @formatter:off
+	@Bean
+	public RegisteredClientRepository registeredClientRepository(JdbcTemplate jdbcTemplate) {
+		RegisteredClient registeredClient = RegisteredClient.withId(UUID.randomUUID().toString())
+				.clientId("messaging-client")
+				.clientSecret("{noop}secret")
+				.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
+				.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
+				.authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
+				.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
+				.redirectUri("http://127.0.0.1:8080/login/oauth2/code/messaging-client-oidc")
+				.redirectUri("http://127.0.0.1:8080/authorized")
+				.scope(OidcScopes.OPENID)
+				.scope("message.read")
+				.scope("message.write")
+				.clientSettings(ClientSettings.builder().requireAuthorizationConsent(true).build())
+				.build();
+
+		// Save registered client in db as if in-memory
+		JdbcRegisteredClientRepository registeredClientRepository = new JdbcRegisteredClientRepository(jdbcTemplate);
+		registeredClientRepository.save(registeredClient);
+
+		return registeredClientRepository;
+	}
+	// @formatter:on
+
+	@Bean
+	public OAuth2AuthorizationService authorizationService(JdbcTemplate jdbcTemplate, RegisteredClientRepository registeredClientRepository) {
+		return new JdbcOAuth2AuthorizationService(jdbcTemplate, registeredClientRepository);
+	}
+
+	@Bean
+	public OAuth2AuthorizationConsentService authorizationConsentService(JdbcTemplate jdbcTemplate, RegisteredClientRepository registeredClientRepository) {
+		return new JdbcOAuth2AuthorizationConsentService(jdbcTemplate, registeredClientRepository);
+	}
+
+	@Bean
+	public JWKSource<SecurityContext> jwkSource() {
+		RSAKey rsaKey = Jwks.generateRsa();
+		JWKSet jwkSet = new JWKSet(rsaKey);
+		return (jwkSelector, securityContext) -> jwkSelector.select(jwkSet);
+	}
+
+	@Bean
+	public ProviderSettings providerSettings() {
+		return ProviderSettings.builder().issuer("http://localhost:9000").build();
+	}
+
+	@Bean
+	public EmbeddedDatabase embeddedDatabase() {
+		// @formatter:off
+		return new EmbeddedDatabaseBuilder()
+				.generateUniqueName(true)
+				.setType(EmbeddedDatabaseType.H2)
+				.setScriptEncoding("UTF-8")
+				.addScript("org/springframework/security/oauth2/server/authorization/oauth2-authorization-schema.sql")
+				.addScript("org/springframework/security/oauth2/server/authorization/oauth2-authorization-consent-schema.sql")
+				.addScript("org/springframework/security/oauth2/server/authorization/client/oauth2-registered-client-schema.sql")
+				.build();
+		// @formatter:on
+	}
+
+}

+ 67 - 0
samples/federated-identity-authorizationserver/src/main/java/sample/config/DefaultSecurityConfig.java

@@ -0,0 +1,67 @@
+/*
+ * 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.config;
+
+import sample.security.FederatedIdentityConfigurer;
+import sample.security.UserRepositoryOAuth2UserHandler;
+
+import org.springframework.context.annotation.Bean;
+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.core.userdetails.User;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.security.core.userdetails.UserDetailsService;
+import org.springframework.security.provisioning.InMemoryUserDetailsManager;
+import org.springframework.security.web.SecurityFilterChain;
+
+/**
+ * @author Steve Riesenberg
+ * @since 0.2.3
+ */
+@EnableWebSecurity
+public class DefaultSecurityConfig {
+
+	// @formatter:off
+	@Bean
+	public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
+		FederatedIdentityConfigurer federatedIdentityConfigurer = new FederatedIdentityConfigurer()
+			.oauth2UserHandler(new UserRepositoryOAuth2UserHandler());
+		http
+			.authorizeRequests(authorizeRequests ->
+				authorizeRequests
+					.mvcMatchers("/assets/**", "/webjars/**", "/login").permitAll()
+					.anyRequest().authenticated()
+			)
+			.formLogin(Customizer.withDefaults())
+			.apply(federatedIdentityConfigurer);
+		return http.build();
+	}
+	// @formatter:on
+
+	// @formatter:off
+	@Bean
+	public UserDetailsService users() {
+		UserDetails user = User.withDefaultPasswordEncoder()
+				.username("user1")
+				.password("password")
+				.roles("USER")
+				.build();
+		return new InMemoryUserDetailsManager(user);
+	}
+	// @formatter:on
+
+}

+ 74 - 0
samples/federated-identity-authorizationserver/src/main/java/sample/jose/Jwks.java

@@ -0,0 +1,74 @@
+/*
+ * 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.jose;
+
+import java.security.KeyPair;
+import java.security.interfaces.ECPrivateKey;
+import java.security.interfaces.ECPublicKey;
+import java.security.interfaces.RSAPrivateKey;
+import java.security.interfaces.RSAPublicKey;
+import java.util.UUID;
+
+import javax.crypto.SecretKey;
+
+import com.nimbusds.jose.jwk.Curve;
+import com.nimbusds.jose.jwk.ECKey;
+import com.nimbusds.jose.jwk.OctetSequenceKey;
+import com.nimbusds.jose.jwk.RSAKey;
+
+/**
+ * @author Joe Grandja
+ * @since 0.1.0
+ */
+public final class Jwks {
+
+	private Jwks() {
+	}
+
+	public static RSAKey generateRsa() {
+		KeyPair keyPair = KeyGeneratorUtils.generateRsaKey();
+		RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
+		RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
+		// @formatter:off
+		return new RSAKey.Builder(publicKey)
+				.privateKey(privateKey)
+				.keyID(UUID.randomUUID().toString())
+				.build();
+		// @formatter:on
+	}
+
+	public static ECKey generateEc() {
+		KeyPair keyPair = KeyGeneratorUtils.generateEcKey();
+		ECPublicKey publicKey = (ECPublicKey) keyPair.getPublic();
+		ECPrivateKey privateKey = (ECPrivateKey) keyPair.getPrivate();
+		Curve curve = Curve.forECParameterSpec(publicKey.getParams());
+		// @formatter:off
+		return new ECKey.Builder(curve, publicKey)
+				.privateKey(privateKey)
+				.keyID(UUID.randomUUID().toString())
+				.build();
+		// @formatter:on
+	}
+
+	public static OctetSequenceKey generateSecret() {
+		SecretKey secretKey = KeyGeneratorUtils.generateSecretKey();
+		// @formatter:off
+		return new OctetSequenceKey.Builder(secretKey)
+				.keyID(UUID.randomUUID().toString())
+				.build();
+		// @formatter:on
+	}
+}

+ 85 - 0
samples/federated-identity-authorizationserver/src/main/java/sample/jose/KeyGeneratorUtils.java

@@ -0,0 +1,85 @@
+/*
+ * 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.jose;
+
+import java.math.BigInteger;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.spec.ECFieldFp;
+import java.security.spec.ECParameterSpec;
+import java.security.spec.ECPoint;
+import java.security.spec.EllipticCurve;
+
+import javax.crypto.KeyGenerator;
+import javax.crypto.SecretKey;
+
+/**
+ * @author Joe Grandja
+ * @since 0.1.0
+ */
+final class KeyGeneratorUtils {
+
+	private KeyGeneratorUtils() {
+	}
+
+	static SecretKey generateSecretKey() {
+		SecretKey hmacKey;
+		try {
+			hmacKey = KeyGenerator.getInstance("HmacSha256").generateKey();
+		} catch (Exception ex) {
+			throw new IllegalStateException(ex);
+		}
+		return hmacKey;
+	}
+
+	static KeyPair generateRsaKey() {
+		KeyPair keyPair;
+		try {
+			KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
+			keyPairGenerator.initialize(2048);
+			keyPair = keyPairGenerator.generateKeyPair();
+		} catch (Exception ex) {
+			throw new IllegalStateException(ex);
+		}
+		return keyPair;
+	}
+
+	static KeyPair generateEcKey() {
+		EllipticCurve ellipticCurve = new EllipticCurve(
+				new ECFieldFp(
+						new BigInteger("115792089210356248762697446949407573530086143415290314195533631308867097853951")),
+				new BigInteger("115792089210356248762697446949407573530086143415290314195533631308867097853948"),
+				new BigInteger("41058363725152142129326129780047268409114441015993725554835256314039467401291"));
+		ECPoint ecPoint = new ECPoint(
+				new BigInteger("48439561293906451759052585252797914202762949526041747995844080717082404635286"),
+				new BigInteger("36134250956749795798585127919587881956611106672985015071877198253568414405109"));
+		ECParameterSpec ecParameterSpec = new ECParameterSpec(
+				ellipticCurve,
+				ecPoint,
+				new BigInteger("115792089210356248762697446949407573529996955224135760342422259061068512044369"),
+				1);
+
+		KeyPair keyPair;
+		try {
+			KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("EC");
+			keyPairGenerator.initialize(ecParameterSpec);
+			keyPair = keyPairGenerator.generateKeyPair();
+		} catch (Exception ex) {
+			throw new IllegalStateException(ex);
+		}
+		return keyPair;
+	}
+}

+ 82 - 0
samples/federated-identity-authorizationserver/src/main/java/sample/security/FederatedIdentityAuthenticationEntryPoint.java

@@ -0,0 +1,82 @@
+/*
+ * 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.security;
+
+import java.io.IOException;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.springframework.http.server.ServletServerHttpRequest;
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.oauth2.client.registration.ClientRegistration;
+import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
+import org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestRedirectFilter;
+import org.springframework.security.web.AuthenticationEntryPoint;
+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;
+
+/**
+ * An {@link AuthenticationEntryPoint} for initiating the login flow to an
+ * external provider using the {@code idp} query parameter, which represents the
+ * {@code registrationId} of the desired {@link ClientRegistration}.
+ *
+ * @author Steve Riesenberg
+ * @since 0.2.3
+ */
+public final class FederatedIdentityAuthenticationEntryPoint implements AuthenticationEntryPoint {
+
+	private final RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
+
+	private String authorizationRequestUri = OAuth2AuthorizationRequestRedirectFilter.DEFAULT_AUTHORIZATION_REQUEST_BASE_URI
+			+ "/{registrationId}";
+
+	private final AuthenticationEntryPoint delegate;
+
+	private final ClientRegistrationRepository clientRegistrationRepository;
+
+	public FederatedIdentityAuthenticationEntryPoint(String loginPageUrl, ClientRegistrationRepository clientRegistrationRepository) {
+		this.delegate = new LoginUrlAuthenticationEntryPoint(loginPageUrl);
+		this.clientRegistrationRepository = clientRegistrationRepository;
+	}
+
+	@Override
+	public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authenticationException) throws IOException, ServletException {
+		String idp = request.getParameter("idp");
+		if (idp != null) {
+			ClientRegistration clientRegistration = this.clientRegistrationRepository.findByRegistrationId(idp);
+			if (clientRegistration != null) {
+				String redirectUri = UriComponentsBuilder.fromHttpRequest(new ServletServerHttpRequest(request))
+						.replaceQuery(null)
+						.replacePath(this.authorizationRequestUri)
+						.buildAndExpand(clientRegistration.getRegistrationId())
+						.toUriString();
+				this.redirectStrategy.sendRedirect(request, response, redirectUri);
+				return;
+			}
+		}
+
+		this.delegate.commence(request, response, authenticationException);
+	}
+
+	public void setAuthorizationRequestUri(String authorizationRequestUri) {
+		this.authorizationRequestUri = authorizationRequestUri;
+	}
+
+}

+ 68 - 0
samples/federated-identity-authorizationserver/src/main/java/sample/security/FederatedIdentityAuthenticationSuccessHandler.java

@@ -0,0 +1,68 @@
+/*
+ * 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.security;
+
+import java.io.IOException;
+import java.util.function.Consumer;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.springframework.security.core.Authentication;
+import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
+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;
+
+/**
+ * An {@link AuthenticationSuccessHandler} for capturing the {@link OidcUser} or
+ * {@link OAuth2User} for Federated Account Linking or JIT Account Provisioning.
+ *
+ * @author Steve Riesenberg
+ * @since 0.2.3
+ */
+public final class FederatedIdentityAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
+
+	private final AuthenticationSuccessHandler delegate = new SavedRequestAwareAuthenticationSuccessHandler();
+
+	private Consumer<OAuth2User> oauth2UserHandler = (user) -> {};
+
+	private Consumer<OidcUser> oidcUserHandler = (user) -> this.oauth2UserHandler.accept(user);
+
+	@Override
+	public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
+		if (authentication instanceof OAuth2AuthenticationToken) {
+			if (authentication.getPrincipal() instanceof OidcUser) {
+				this.oidcUserHandler.accept((OidcUser) authentication.getPrincipal());
+			} else if (authentication.getPrincipal() instanceof OAuth2User) {
+				this.oauth2UserHandler.accept((OAuth2User) authentication.getPrincipal());
+			}
+		}
+
+		this.delegate.onAuthenticationSuccess(request, response, authentication);
+	}
+
+	public void setOAuth2UserHandler(Consumer<OAuth2User> oauth2UserHandler) {
+		this.oauth2UserHandler = oauth2UserHandler;
+	}
+
+	public void setOidcUserHandler(Consumer<OidcUser> oidcUserHandler) {
+		this.oidcUserHandler = oidcUserHandler;
+	}
+
+}

+ 125 - 0
samples/federated-identity-authorizationserver/src/main/java/sample/security/FederatedIdentityConfigurer.java

@@ -0,0 +1,125 @@
+/*
+ * 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.security;
+
+import java.util.function.Consumer;
+
+import org.springframework.context.ApplicationContext;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
+import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
+import org.springframework.security.oauth2.core.oidc.user.OidcUser;
+import org.springframework.security.oauth2.core.user.OAuth2User;
+import org.springframework.util.Assert;
+
+/**
+ * A configurer for setting up Federated Identity Management.
+ *
+ * @author Steve Riesenberg
+ * @since 0.2.3
+ */
+public final class FederatedIdentityConfigurer extends AbstractHttpConfigurer<FederatedIdentityConfigurer, HttpSecurity> {
+
+	private String loginPageUrl = "/login";
+
+	private String authorizationRequestUri;
+
+	private Consumer<OAuth2User> oauth2UserHandler;
+
+	private Consumer<OidcUser> oidcUserHandler;
+
+	/**
+	 * @param loginPageUrl The URL of the login page, defaults to {@code "/login"}
+	 * @return This configurer for additional configuration
+	 */
+	public FederatedIdentityConfigurer loginPageUrl(String loginPageUrl) {
+		Assert.hasText(loginPageUrl, "loginPageUrl cannot be empty");
+		this.loginPageUrl = loginPageUrl;
+		return this;
+	}
+
+	/**
+	 * @param authorizationRequestUri The authorization request URI for initiating
+	 * the login flow with an external IDP, defaults to {@code
+	 * "/oauth2/authorization/{registrationId}"}
+	 * @return This configurer for additional configuration
+	 */
+	public FederatedIdentityConfigurer authorizationRequestUri(String authorizationRequestUri) {
+		Assert.hasText(authorizationRequestUri, "authorizationRequestUri cannot be empty");
+		this.authorizationRequestUri = authorizationRequestUri;
+		return this;
+	}
+
+	/**
+	 * @param oauth2UserHandler The {@link Consumer} for performing JIT account provisioning
+	 * with an OAuth 2.0 IDP
+	 * @return This configurer for additional configuration
+	 */
+	public FederatedIdentityConfigurer oauth2UserHandler(Consumer<OAuth2User> oauth2UserHandler) {
+		Assert.notNull(oauth2UserHandler, "oauth2UserHandler cannot be null");
+		this.oauth2UserHandler = oauth2UserHandler;
+		return this;
+	}
+
+	/**
+	 * @param oidcUserHandler The {@link Consumer} for performing JIT account provisioning
+	 * with an OpenID Connect 1.0 IDP
+	 * @return This configurer for additional configuration
+	 */
+	public FederatedIdentityConfigurer oidcUserHandler(Consumer<OidcUser> oidcUserHandler) {
+		Assert.notNull(oidcUserHandler, "oidcUserHandler cannot be null");
+		this.oidcUserHandler = oidcUserHandler;
+		return this;
+	}
+
+	// @formatter:off
+	@Override
+	public void init(HttpSecurity http) throws Exception {
+		ApplicationContext applicationContext = http.getSharedObject(ApplicationContext.class);
+		ClientRegistrationRepository clientRegistrationRepository =
+			applicationContext.getBean(ClientRegistrationRepository.class);
+		FederatedIdentityAuthenticationEntryPoint authenticationEntryPoint =
+			new FederatedIdentityAuthenticationEntryPoint(this.loginPageUrl, clientRegistrationRepository);
+		if (this.authorizationRequestUri != null) {
+			authenticationEntryPoint.setAuthorizationRequestUri(this.authorizationRequestUri);
+		}
+
+		FederatedIdentityAuthenticationSuccessHandler authenticationSuccessHandler =
+			new FederatedIdentityAuthenticationSuccessHandler();
+		if (this.oauth2UserHandler != null) {
+			authenticationSuccessHandler.setOAuth2UserHandler(this.oauth2UserHandler);
+		}
+		if (this.oidcUserHandler != null) {
+			authenticationSuccessHandler.setOidcUserHandler(this.oidcUserHandler);
+		}
+
+		http
+			.exceptionHandling(exceptionHandling ->
+				exceptionHandling.authenticationEntryPoint(authenticationEntryPoint)
+			)
+			.oauth2Login(oauth2Login -> {
+				oauth2Login.successHandler(authenticationSuccessHandler);
+				if (this.authorizationRequestUri != null) {
+					String baseUri = this.authorizationRequestUri.replace("/{registrationId}", "");
+					oauth2Login.authorizationEndpoint(authorizationEndpoint ->
+						authorizationEndpoint.baseUri(baseUri)
+					);
+				}
+			});
+	}
+	// @formatter:on
+
+}

+ 91 - 0
samples/federated-identity-authorizationserver/src/main/java/sample/security/FederatedIdentityIdTokenCustomizer.java

@@ -0,0 +1,91 @@
+/*
+ * 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.security;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+import org.springframework.security.core.Authentication;
+import org.springframework.security.oauth2.core.oidc.IdTokenClaimNames;
+import org.springframework.security.oauth2.core.oidc.OidcIdToken;
+import org.springframework.security.oauth2.core.oidc.endpoint.OidcParameterNames;
+import org.springframework.security.oauth2.core.oidc.user.OidcUser;
+import org.springframework.security.oauth2.core.user.OAuth2User;
+import org.springframework.security.oauth2.server.authorization.JwtEncodingContext;
+import org.springframework.security.oauth2.server.authorization.OAuth2TokenCustomizer;
+
+/**
+ * An {@link OAuth2TokenCustomizer} to map claims from a federated identity to
+ * the {@code id_token} produced by this authorization server.
+ *
+ * @author Steve Riesenberg
+ * @since 0.2.3
+ */
+public final class FederatedIdentityIdTokenCustomizer implements OAuth2TokenCustomizer<JwtEncodingContext> {
+
+	private static final Set<String> ID_TOKEN_CLAIMS = Collections.unmodifiableSet(new HashSet<>(Arrays.asList(
+			IdTokenClaimNames.ISS,
+			IdTokenClaimNames.SUB,
+			IdTokenClaimNames.AUD,
+			IdTokenClaimNames.EXP,
+			IdTokenClaimNames.IAT,
+			IdTokenClaimNames.AUTH_TIME,
+			IdTokenClaimNames.NONCE,
+			IdTokenClaimNames.ACR,
+			IdTokenClaimNames.AMR,
+			IdTokenClaimNames.AZP,
+			IdTokenClaimNames.AT_HASH,
+			IdTokenClaimNames.C_HASH
+	)));
+
+	@Override
+	public void customize(JwtEncodingContext context) {
+		if (OidcParameterNames.ID_TOKEN.equals(context.getTokenType().getValue())) {
+			Map<String, Object> thirdPartyClaims = extractClaims(context.getPrincipal());
+			context.getClaims().claims(existingClaims -> {
+				// Remove conflicting claims set by this authorization server
+				existingClaims.keySet().forEach(thirdPartyClaims::remove);
+
+				// Remove standard id_token claims that could cause problems with clients
+				ID_TOKEN_CLAIMS.forEach(thirdPartyClaims::remove);
+
+				// Add all other claims directly to id_token
+				existingClaims.putAll(thirdPartyClaims);
+			});
+		}
+	}
+
+	private Map<String, Object> extractClaims(Authentication principal) {
+		Map<String, Object> claims;
+		if (principal.getPrincipal() instanceof OidcUser) {
+			OidcUser oidcUser = (OidcUser) principal.getPrincipal();
+			OidcIdToken idToken = oidcUser.getIdToken();
+			claims = idToken.getClaims();
+		} else if (principal.getPrincipal() instanceof OAuth2User) {
+			OAuth2User oauth2User = (OAuth2User) principal.getPrincipal();
+			claims = oauth2User.getAttributes();
+		} else {
+			claims = Collections.emptyMap();
+		}
+
+		return new HashMap<>(claims);
+	}
+
+}

+ 57 - 0
samples/federated-identity-authorizationserver/src/main/java/sample/security/UserRepositoryOAuth2UserHandler.java

@@ -0,0 +1,57 @@
+/*
+ * 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.security;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.function.Consumer;
+
+import org.springframework.security.oauth2.core.user.OAuth2User;
+
+/**
+ * Example {@link Consumer} to perform JIT provisioning of an {@link OAuth2User}.
+ *
+ * @author Steve Riesenberg
+ * @since 0.2.3
+ */
+public final class UserRepositoryOAuth2UserHandler implements Consumer<OAuth2User> {
+
+	private final UserRepository userRepository = new UserRepository();
+
+	@Override
+	public void accept(OAuth2User user) {
+		// Capture user in a local data store on first authentication
+		if (this.userRepository.findByName(user.getName()) == null) {
+			System.out.println("Saving first-time user: name=" + user.getName() + ", claims=" + user.getAttributes() + ", authorities=" + user.getAuthorities());
+			this.userRepository.save(user);
+		}
+	}
+
+	static class UserRepository {
+
+		private final Map<String, OAuth2User> userCache = new ConcurrentHashMap<>();
+
+		public OAuth2User findByName(String name) {
+			return this.userCache.get(name);
+		}
+
+		public void save(OAuth2User oauth2User) {
+			this.userCache.put(oauth2User.getName(), oauth2User);
+		}
+
+	}
+
+}

+ 33 - 0
samples/federated-identity-authorizationserver/src/main/java/sample/web/LoginController.java

@@ -0,0 +1,33 @@
+/*
+ * 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.web;
+
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.GetMapping;
+
+/**
+ * @author Steve Riesenberg
+ * @since 0.2.3
+ */
+@Controller
+public class LoginController {
+
+	@GetMapping("/login")
+	public String login() {
+		return "login";
+	}
+
+}

+ 33 - 0
samples/federated-identity-authorizationserver/src/main/resources/application.yml

@@ -0,0 +1,33 @@
+server:
+  port: 9000
+
+spring:
+  security:
+    oauth2:
+      client:
+        registration:
+          google-idp:
+            provider: google
+            client-id: ${GOOGLE_CLIENT_ID:google-client-id}
+            client-secret: ${GOOGLE_CLIENT_SECRET:google-client-secret}
+            scope: openid, https://www.googleapis.com/auth/userinfo.profile, https://www.googleapis.com/auth/userinfo.email
+            client-name: Sign in with Google
+          github-idp:
+            provider: github
+            client-id: ${GITHUB_CLIENT_ID:github-client-id}
+            client-secret: ${GITHUB_CLIENT_SECRET:github-client-secret}
+            scope: user:email, read:user
+            client-name: Sign in with GitHub
+        provider:
+          google:
+            user-name-attribute: email
+          github:
+            user-name-attribute: login
+
+logging:
+  level:
+    root: INFO
+    org.springframework.web: INFO
+    org.springframework.security: INFO
+    org.springframework.security.oauth2: INFO
+#    org.springframework.boot.autoconfigure: DEBUG

+ 41 - 0
samples/federated-identity-authorizationserver/src/main/resources/templates/login.html

@@ -0,0 +1,41 @@
+<!DOCTYPE html>
+<html lang="en"
+      xmlns="http://www.w3.org/1999/xhtml" xmlns:th="https://www.thymeleaf.org">
+<head>
+    <meta charset="utf-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
+    <title>Spring Security Example</title>
+    <link href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-/Y6pD6FV/Vv2HJnA6t+vslU6fwYXjCFtcEpHbNJ0lyAFsXTsjBbfaDjzALeQsN6M" crossorigin="anonymous">
+    <link href="https://getbootstrap.com/docs/4.0/examples/signin/signin.css" rel="stylesheet" crossorigin="anonymous"/>
+</head>
+<body>
+<div class="container">
+    <form class="form-signin" method="post" th:action="@{/login}">
+        <div th:if="${param.error}" class="alert alert-danger" role="alert">
+            Invalid username or password.
+        </div>
+        <div th:if="${param.logout}" class="alert alert-success" role="alert">
+            You have been logged out.
+        </div>
+        <h2 class="form-signin-heading">Sign In</h2>
+        <p>
+            <label for="username" class="sr-only">Username</label>
+            <input type="text" id="username" name="username" class="form-control" placeholder="Username" required autofocus>
+        </p>
+        <p>
+            <label for="password" class="sr-only">Password</label>
+            <input type="password" id="password" name="password" class="form-control" placeholder="Password" required>
+        </p>
+        <button class="btn btn-lg btn-primary btn-block" type="submit">Sign in</button>
+        <a class="btn btn-light btn-block bg-white" href="/oauth2/authorization/google-idp" role="link" style="text-transform: none;">
+            <img width="20" style="margin-right: 5px;" alt="Sign in with Google" src="https://upload.wikimedia.org/wikipedia/commons/thumb/5/53/Google_%22G%22_Logo.svg/512px-Google_%22G%22_Logo.svg.png" />
+            Sign in with Google
+        </a>
+        <a class="btn btn-light btn-block bg-white" href="/oauth2/authorization/github-idp" role="link" style="text-transform: none;">
+            <img width="24" style="margin-right: 5px;" alt="Sign in with GitHub" src="https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png" />
+            Sign in with Github
+        </a>
+    </form>
+</div>
+</body>
+</html>