From 23db0925a6247f627b3e6fed68d3a9561ca3c83a Mon Sep 17 00:00:00 2001 From: Steve Riesenberg Date: Thu, 3 Feb 2022 16:20:37 -0600 Subject: [PATCH] Federated Identity sample Issue gh-538 Issue gh-499 Issue gh-106 --- .../config/AuthorizationServerConfig.java | 2 +- .../sample/config/DefaultSecurityConfig.java | 1 + .../config/AuthorizationServerConfig.java | 4 +- .../main/java/sample/config/CorsConfig.java | 39 +++++ .../sample/config/DefaultSecurityConfig.java | 1 + .../main/java/sample/web/LoginController.java | 32 ++++ .../src/main/resources/templates/login.html | 41 +++++ ...erated-identity-authorizationserver.gradle | 19 +++ ...dentityAuthorizationServerApplication.java | 32 ++++ .../config/AuthorizationServerConfig.java | 141 ++++++++++++++++++ .../main/java/sample/config/CorsConfig.java | 39 +++++ .../sample/config/DefaultSecurityConfig.java | 67 +++++++++ .../src/main/java/sample/jose/Jwks.java | 74 +++++++++ .../java/sample/jose/KeyGeneratorUtils.java | 85 +++++++++++ ...ratedIdentityAuthenticationEntryPoint.java | 81 ++++++++++ ...dIdentityAuthenticationSuccessHandler.java | 69 +++++++++ .../security/FederatedIdentityConfigurer.java | 140 +++++++++++++++++ .../FederatedIdentityIdTokenCustomizer.java | 90 +++++++++++ .../UserRepositoryOAuth2UserHandler.java | 56 +++++++ .../main/java/sample/web/LoginController.java | 32 ++++ .../src/main/resources/application.yml | 33 ++++ .../src/main/resources/templates/login.html | 41 +++++ ...tyAuthorizationServerApplicationTests.java | 137 +++++++++++++++++ .../src/main/resources/application.yml | 3 +- .../sample/config/ResourceServerConfig.java | 16 ++ .../src/main/resources/application.yml | 2 +- 26 files changed, 1272 insertions(+), 5 deletions(-) create mode 100644 samples/default-authorizationserver/src/main/java/sample/config/CorsConfig.java create mode 100644 samples/default-authorizationserver/src/main/java/sample/web/LoginController.java create mode 100644 samples/default-authorizationserver/src/main/resources/templates/login.html create mode 100644 samples/federated-identity-authorizationserver/samples-federated-identity-authorizationserver.gradle create mode 100644 samples/federated-identity-authorizationserver/src/main/java/sample/FederatedIdentityAuthorizationServerApplication.java create mode 100644 samples/federated-identity-authorizationserver/src/main/java/sample/config/AuthorizationServerConfig.java create mode 100644 samples/federated-identity-authorizationserver/src/main/java/sample/config/CorsConfig.java create mode 100644 samples/federated-identity-authorizationserver/src/main/java/sample/config/DefaultSecurityConfig.java create mode 100644 samples/federated-identity-authorizationserver/src/main/java/sample/jose/Jwks.java create mode 100644 samples/federated-identity-authorizationserver/src/main/java/sample/jose/KeyGeneratorUtils.java create mode 100644 samples/federated-identity-authorizationserver/src/main/java/sample/security/FederatedIdentityAuthenticationEntryPoint.java create mode 100644 samples/federated-identity-authorizationserver/src/main/java/sample/security/FederatedIdentityAuthenticationSuccessHandler.java create mode 100644 samples/federated-identity-authorizationserver/src/main/java/sample/security/FederatedIdentityConfigurer.java create mode 100644 samples/federated-identity-authorizationserver/src/main/java/sample/security/FederatedIdentityIdTokenCustomizer.java create mode 100644 samples/federated-identity-authorizationserver/src/main/java/sample/security/UserRepositoryOAuth2UserHandler.java create mode 100644 samples/federated-identity-authorizationserver/src/main/java/sample/web/LoginController.java create mode 100644 samples/federated-identity-authorizationserver/src/main/resources/application.yml create mode 100644 samples/federated-identity-authorizationserver/src/main/resources/templates/login.html create mode 100644 samples/federated-identity-authorizationserver/src/test/java/sample/FederatedIdentityAuthorizationServerApplicationTests.java diff --git a/samples/custom-consent-authorizationserver/src/main/java/sample/config/AuthorizationServerConfig.java b/samples/custom-consent-authorizationserver/src/main/java/sample/config/AuthorizationServerConfig.java index d7064bb4cc..4a08a0e47d 100644 --- a/samples/custom-consent-authorizationserver/src/main/java/sample/config/AuthorizationServerConfig.java +++ b/samples/custom-consent-authorizationserver/src/main/java/sample/config/AuthorizationServerConfig.java @@ -103,7 +103,7 @@ public JWKSource jwkSource() { @Bean public ProviderSettings providerSettings() { - return ProviderSettings.builder().issuer("http://auth-server:9000").build(); + return ProviderSettings.builder().issuer("http://localhost:9000").build(); } @Bean diff --git a/samples/custom-consent-authorizationserver/src/main/java/sample/config/DefaultSecurityConfig.java b/samples/custom-consent-authorizationserver/src/main/java/sample/config/DefaultSecurityConfig.java index e70d509d2e..18d830f913 100644 --- a/samples/custom-consent-authorizationserver/src/main/java/sample/config/DefaultSecurityConfig.java +++ b/samples/custom-consent-authorizationserver/src/main/java/sample/config/DefaultSecurityConfig.java @@ -39,6 +39,7 @@ SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Excepti .authorizeRequests(authorizeRequests -> authorizeRequests.anyRequest().authenticated() ) + .cors(withDefaults()) .formLogin(withDefaults()); return http.build(); } diff --git a/samples/default-authorizationserver/src/main/java/sample/config/AuthorizationServerConfig.java b/samples/default-authorizationserver/src/main/java/sample/config/AuthorizationServerConfig.java index e9cacc63a5..2caa76ba0f 100644 --- a/samples/default-authorizationserver/src/main/java/sample/config/AuthorizationServerConfig.java +++ b/samples/default-authorizationserver/src/main/java/sample/config/AuthorizationServerConfig.java @@ -59,7 +59,7 @@ public class AuthorizationServerConfig { @Order(Ordered.HIGHEST_PRECEDENCE) public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception { OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http); - return http.formLogin(Customizer.withDefaults()).build(); + return http.cors(Customizer.withDefaults()).formLogin(Customizer.withDefaults()).build(); } // @formatter:off @@ -107,7 +107,7 @@ public JWKSource jwkSource() { @Bean public ProviderSettings providerSettings() { - return ProviderSettings.builder().issuer("http://auth-server:9000").build(); + return ProviderSettings.builder().issuer("http://localhost:9000").build(); } @Bean diff --git a/samples/default-authorizationserver/src/main/java/sample/config/CorsConfig.java b/samples/default-authorizationserver/src/main/java/sample/config/CorsConfig.java new file mode 100644 index 0000000000..5a64c975a2 --- /dev/null +++ b/samples/default-authorizationserver/src/main/java/sample/config/CorsConfig.java @@ -0,0 +1,39 @@ +/* + * 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 org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.CorsConfigurationSource; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; + +@Configuration +public class CorsConfig { + + @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; + } + +} diff --git a/samples/default-authorizationserver/src/main/java/sample/config/DefaultSecurityConfig.java b/samples/default-authorizationserver/src/main/java/sample/config/DefaultSecurityConfig.java index 1eaca369c3..938bccac85 100644 --- a/samples/default-authorizationserver/src/main/java/sample/config/DefaultSecurityConfig.java +++ b/samples/default-authorizationserver/src/main/java/sample/config/DefaultSecurityConfig.java @@ -40,6 +40,7 @@ SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Excepti .authorizeRequests(authorizeRequests -> authorizeRequests.anyRequest().authenticated() ) + .cors(withDefaults()) .formLogin(withDefaults()); return http.build(); } diff --git a/samples/default-authorizationserver/src/main/java/sample/web/LoginController.java b/samples/default-authorizationserver/src/main/java/sample/web/LoginController.java new file mode 100644 index 0000000000..58d56e1ca6 --- /dev/null +++ b/samples/default-authorizationserver/src/main/java/sample/web/LoginController.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.web; + +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; + +/** + * @author Steve Riesenberg + */ +@Controller +public class LoginController { + + @GetMapping("/login") + public String login() { + return "login"; + } + +} diff --git a/samples/default-authorizationserver/src/main/resources/templates/login.html b/samples/default-authorizationserver/src/main/resources/templates/login.html new file mode 100644 index 0000000000..3de1034f92 --- /dev/null +++ b/samples/default-authorizationserver/src/main/resources/templates/login.html @@ -0,0 +1,41 @@ + + + + + + Spring Security Example + + + + +
+ +
+ + \ No newline at end of file diff --git a/samples/federated-identity-authorizationserver/samples-federated-identity-authorizationserver.gradle b/samples/federated-identity-authorizationserver/samples-federated-identity-authorizationserver.gradle new file mode 100644 index 0000000000..e9d4381995 --- /dev/null +++ b/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' +} diff --git a/samples/federated-identity-authorizationserver/src/main/java/sample/FederatedIdentityAuthorizationServerApplication.java b/samples/federated-identity-authorizationserver/src/main/java/sample/FederatedIdentityAuthorizationServerApplication.java new file mode 100644 index 0000000000..d1cad27c3c --- /dev/null +++ b/samples/federated-identity-authorizationserver/src/main/java/sample/FederatedIdentityAuthorizationServerApplication.java @@ -0,0 +1,32 @@ +/* + * Copyright 2020-2021 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 Joe Grandja + * @since 0.0.1 + */ +@SpringBootApplication +public class FederatedIdentityAuthorizationServerApplication { + + public static void main(String[] args) { + SpringApplication.run(FederatedIdentityAuthorizationServerApplication.class, args); + } + +} diff --git a/samples/federated-identity-authorizationserver/src/main/java/sample/config/AuthorizationServerConfig.java b/samples/federated-identity-authorizationserver/src/main/java/sample/config/AuthorizationServerConfig.java new file mode 100644 index 0000000000..a47c7d8ed3 --- /dev/null +++ b/samples/federated-identity-authorizationserver/src/main/java/sample/config/AuthorizationServerConfig.java @@ -0,0 +1,141 @@ +/* + * Copyright 2020-2021 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 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.OAuth2AuthorizationConsentService; +import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService; +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 + */ +@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.cors(Customizer.withDefaults()).formLogin(Customizer.withDefaults()).build(); + } + + @Bean + public RegisteredClientRepository registeredClientRepository(JdbcTemplate jdbcTemplate) { + // @formatter:off + RegisteredClient confidentialClient = 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(); + RegisteredClient publicClient = RegisteredClient.withId(UUID.randomUUID().toString()) + .clientId("public-client") + .clientAuthenticationMethod(ClientAuthenticationMethod.NONE) + .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) + .redirectUri("http://127.0.0.1:4200") + .redirectUri("http://127.0.0.1:4200/silent-renew.html") + .scope(OidcScopes.OPENID) + .scope("message.read") + .scope("message.write") + .clientSettings(ClientSettings.builder().requireAuthorizationConsent(true).requireProofKey(true).build()) + .build(); + // @formatter:on + + // Save registered client in db as if in-memory + JdbcRegisteredClientRepository registeredClientRepository = new JdbcRegisteredClientRepository(jdbcTemplate); + registeredClientRepository.save(confidentialClient); + registeredClientRepository.save(publicClient); + + return registeredClientRepository; + } + + @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 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 + } + +} diff --git a/samples/federated-identity-authorizationserver/src/main/java/sample/config/CorsConfig.java b/samples/federated-identity-authorizationserver/src/main/java/sample/config/CorsConfig.java new file mode 100644 index 0000000000..5a64c975a2 --- /dev/null +++ b/samples/federated-identity-authorizationserver/src/main/java/sample/config/CorsConfig.java @@ -0,0 +1,39 @@ +/* + * 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 org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.CorsConfigurationSource; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; + +@Configuration +public class CorsConfig { + + @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; + } + +} diff --git a/samples/federated-identity-authorizationserver/src/main/java/sample/config/DefaultSecurityConfig.java b/samples/federated-identity-authorizationserver/src/main/java/sample/config/DefaultSecurityConfig.java new file mode 100644 index 0000000000..477eaee44c --- /dev/null +++ b/samples/federated-identity-authorizationserver/src/main/java/sample/config/DefaultSecurityConfig.java @@ -0,0 +1,67 @@ +/* + * Copyright 2020-2021 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 + */ +@EnableWebSecurity +public class DefaultSecurityConfig { + + // @formatter:off + @Bean + SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception { + http + .authorizeRequests(authorizeRequests -> + authorizeRequests + .mvcMatchers("/assets/**", "/webjars/**", "/login").permitAll() + .anyRequest().authenticated() + ) + .cors(Customizer.withDefaults()) + .formLogin(Customizer.withDefaults()) + .apply(new FederatedIdentityConfigurer() + .oauth2UserHandler(new UserRepositoryOAuth2UserHandler()) + ); + return http.build(); + } + // @formatter:on + + // @formatter:off + @Bean + UserDetailsService users() { + UserDetails user = User.withDefaultPasswordEncoder() + .username("user1") + .password("password") + .roles("USER") + .build(); + return new InMemoryUserDetailsManager(user); + } + // @formatter:on + +} diff --git a/samples/federated-identity-authorizationserver/src/main/java/sample/jose/Jwks.java b/samples/federated-identity-authorizationserver/src/main/java/sample/jose/Jwks.java new file mode 100644 index 0000000000..0a02e6ccbb --- /dev/null +++ b/samples/federated-identity-authorizationserver/src/main/java/sample/jose/Jwks.java @@ -0,0 +1,74 @@ +/* + * Copyright 2020-2021 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 + } +} diff --git a/samples/federated-identity-authorizationserver/src/main/java/sample/jose/KeyGeneratorUtils.java b/samples/federated-identity-authorizationserver/src/main/java/sample/jose/KeyGeneratorUtils.java new file mode 100644 index 0000000000..babaf2858e --- /dev/null +++ b/samples/federated-identity-authorizationserver/src/main/java/sample/jose/KeyGeneratorUtils.java @@ -0,0 +1,85 @@ +/* + * Copyright 2020-2021 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; + } +} diff --git a/samples/federated-identity-authorizationserver/src/main/java/sample/security/FederatedIdentityAuthenticationEntryPoint.java b/samples/federated-identity-authorizationserver/src/main/java/sample/security/FederatedIdentityAuthenticationEntryPoint.java new file mode 100644 index 0000000000..77eb49f155 --- /dev/null +++ b/samples/federated-identity-authorizationserver/src/main/java/sample/security/FederatedIdentityAuthenticationEntryPoint.java @@ -0,0 +1,81 @@ +/* + * 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 + */ +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; + } + +} diff --git a/samples/federated-identity-authorizationserver/src/main/java/sample/security/FederatedIdentityAuthenticationSuccessHandler.java b/samples/federated-identity-authorizationserver/src/main/java/sample/security/FederatedIdentityAuthenticationSuccessHandler.java new file mode 100644 index 0000000000..9f685de554 --- /dev/null +++ b/samples/federated-identity-authorizationserver/src/main/java/sample/security/FederatedIdentityAuthenticationSuccessHandler.java @@ -0,0 +1,69 @@ +/* + * 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 + * @see + * What is Federated Identity Management? + */ +public final class FederatedIdentityAuthenticationSuccessHandler implements AuthenticationSuccessHandler { + + private final AuthenticationSuccessHandler delegate = new SavedRequestAwareAuthenticationSuccessHandler(); + + private Consumer oauth2UserHandler = (user) -> {}; + + private Consumer 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 oauth2UserHandler) { + this.oauth2UserHandler = oauth2UserHandler; + } + + public void setOidcUserHandler(Consumer oidcUserHandler) { + this.oidcUserHandler = oidcUserHandler; + } + +} diff --git a/samples/federated-identity-authorizationserver/src/main/java/sample/security/FederatedIdentityConfigurer.java b/samples/federated-identity-authorizationserver/src/main/java/sample/security/FederatedIdentityConfigurer.java new file mode 100644 index 0000000000..81ad898187 --- /dev/null +++ b/samples/federated-identity-authorizationserver/src/main/java/sample/security/FederatedIdentityConfigurer.java @@ -0,0 +1,140 @@ +/* + * 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.security.oauth2.server.authorization.JwtEncodingContext; +import org.springframework.security.oauth2.server.authorization.OAuth2TokenCustomizer; +import org.springframework.util.Assert; + +/** + * A configurer for setting up Federated Identity Management. + * + * @author Steve Riesenberg + * @see + * What is Federated Identity Management? + */ +public final class FederatedIdentityConfigurer extends AbstractHttpConfigurer { + + private String loginPageUrl = "/login"; + + private String authorizationRequestUri; + + private Consumer oauth2UserHandler; + + private Consumer 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 Auth 2.0 IDP + * @return This configurer for additional configuration + */ + public FederatedIdentityConfigurer oauth2UserHandler(Consumer 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 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 + + @Override + public void setBuilder(HttpSecurity http) { + // Set the default the OAuth2TokenCustomizer so the OAuth2AuthorizationServerConfigurer can use it + http.setSharedObject(OAuth2TokenCustomizer.class, idTokenCustomizer()); + + super.setBuilder(http); + } + + private static OAuth2TokenCustomizer idTokenCustomizer() { + return new FederatedIdentityIdTokenCustomizer(); + } + +} diff --git a/samples/federated-identity-authorizationserver/src/main/java/sample/security/FederatedIdentityIdTokenCustomizer.java b/samples/federated-identity-authorizationserver/src/main/java/sample/security/FederatedIdentityIdTokenCustomizer.java new file mode 100644 index 0000000000..85829995ba --- /dev/null +++ b/samples/federated-identity-authorizationserver/src/main/java/sample/security/FederatedIdentityIdTokenCustomizer.java @@ -0,0 +1,90 @@ +/* + * 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 + */ +public final class FederatedIdentityIdTokenCustomizer implements OAuth2TokenCustomizer { + + private static final Set 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 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 extractClaims(Authentication principal) { + Map 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); + } + +} diff --git a/samples/federated-identity-authorizationserver/src/main/java/sample/security/UserRepositoryOAuth2UserHandler.java b/samples/federated-identity-authorizationserver/src/main/java/sample/security/UserRepositoryOAuth2UserHandler.java new file mode 100644 index 0000000000..eb7915cabc --- /dev/null +++ b/samples/federated-identity-authorizationserver/src/main/java/sample/security/UserRepositoryOAuth2UserHandler.java @@ -0,0 +1,56 @@ +/* + * 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 + */ +public final class UserRepositoryOAuth2UserHandler implements Consumer { + + 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 userCache = new ConcurrentHashMap<>(); + + public OAuth2User findByName(String name) { + return this.userCache.get(name); + } + + public void save(OAuth2User oauth2User) { + this.userCache.put(oauth2User.getName(), oauth2User); + } + + } + +} diff --git a/samples/federated-identity-authorizationserver/src/main/java/sample/web/LoginController.java b/samples/federated-identity-authorizationserver/src/main/java/sample/web/LoginController.java new file mode 100644 index 0000000000..58d56e1ca6 --- /dev/null +++ b/samples/federated-identity-authorizationserver/src/main/java/sample/web/LoginController.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.web; + +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; + +/** + * @author Steve Riesenberg + */ +@Controller +public class LoginController { + + @GetMapping("/login") + public String login() { + return "login"; + } + +} diff --git a/samples/federated-identity-authorizationserver/src/main/resources/application.yml b/samples/federated-identity-authorizationserver/src/main/resources/application.yml new file mode 100644 index 0000000000..db254085fc --- /dev/null +++ b/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:test} + client-secret: ${GOOGLE_CLIENT_SECRET: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:test} + client-secret: ${GITHUB_CLIENT_SECRET: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 diff --git a/samples/federated-identity-authorizationserver/src/main/resources/templates/login.html b/samples/federated-identity-authorizationserver/src/main/resources/templates/login.html new file mode 100644 index 0000000000..3de1034f92 --- /dev/null +++ b/samples/federated-identity-authorizationserver/src/main/resources/templates/login.html @@ -0,0 +1,41 @@ + + + + + + Spring Security Example + + + + +
+ +
+ + \ No newline at end of file diff --git a/samples/federated-identity-authorizationserver/src/test/java/sample/FederatedIdentityAuthorizationServerApplicationTests.java b/samples/federated-identity-authorizationserver/src/test/java/sample/FederatedIdentityAuthorizationServerApplicationTests.java new file mode 100644 index 0000000000..e0ccd96f74 --- /dev/null +++ b/samples/federated-identity-authorizationserver/src/test/java/sample/FederatedIdentityAuthorizationServerApplicationTests.java @@ -0,0 +1,137 @@ +/* + * Copyright 2020-2021 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 java.io.IOException; + +import com.gargoylesoftware.htmlunit.Page; +import com.gargoylesoftware.htmlunit.WebClient; +import com.gargoylesoftware.htmlunit.WebResponse; +import com.gargoylesoftware.htmlunit.html.HtmlButton; +import com.gargoylesoftware.htmlunit.html.HtmlElement; +import com.gargoylesoftware.htmlunit.html.HtmlInput; +import com.gargoylesoftware.htmlunit.html.HtmlPage; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.HttpStatus; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.web.util.UriComponentsBuilder; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration tests for the sample Authorization Server. + * + * @author Daniel Garnier-Moiroux + */ +@RunWith(SpringRunner.class) +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@AutoConfigureMockMvc +public class FederatedIdentityAuthorizationServerApplicationTests { + private static final String REDIRECT_URI = "http://127.0.0.1:8080/login/oauth2/code/messaging-client-oidc"; + + private static final String AUTHORIZATION_REQUEST = UriComponentsBuilder + .fromPath("/oauth2/authorize") + .queryParam("response_type", "code") + .queryParam("client_id", "messaging-client") + .queryParam("scope", "openid") + .queryParam("state", "some-state") + .queryParam("redirect_uri", REDIRECT_URI) + .toUriString(); + + @Autowired + private WebClient webClient; + + @Before + public void setUp() { + this.webClient.getOptions().setThrowExceptionOnFailingStatusCode(true); + this.webClient.getOptions().setRedirectEnabled(true); + this.webClient.getCookieManager().clearCookies(); // log out + } + + @Test + public void whenLoginSuccessfulThenDisplayNotFoundError() throws IOException { + HtmlPage page = this.webClient.getPage("/"); + + assertLoginPage(page); + + this.webClient.getOptions().setThrowExceptionOnFailingStatusCode(false); + WebResponse signInResponse = signIn(page, "user1", "password").getWebResponse(); + assertThat(signInResponse.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND.value()); // there is no "default" index page + } + + @Test + public void whenLoginFailsThenDisplayBadCredentials() throws IOException { + HtmlPage page = this.webClient.getPage("/"); + + HtmlPage loginErrorPage = signIn(page, "user1", "wrong-password"); + + HtmlElement alert = loginErrorPage.querySelector("div[role=\"alert\"]"); + assertThat(alert).isNotNull(); + assertThat(alert.getTextContent()).isEqualTo("Bad credentials"); + } + + @Test + public void whenNotLoggedInAndRequestingTokenThenRedirectsToLogin() throws IOException { + HtmlPage page = this.webClient.getPage(AUTHORIZATION_REQUEST); + + assertLoginPage(page); + } + + @Test + public void whenLoggingInAndRequestingTokenThenRedirectsToClientApplication() throws IOException { + // Log in + this.webClient.getOptions().setThrowExceptionOnFailingStatusCode(false); + this.webClient.getOptions().setRedirectEnabled(false); + signIn(this.webClient.getPage("/login"), "user1", "password"); + + // Request token + WebResponse response = this.webClient.getPage(AUTHORIZATION_REQUEST).getWebResponse(); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.MOVED_PERMANENTLY.value()); + String location = response.getResponseHeaderValue("location"); + assertThat(location).startsWith(REDIRECT_URI); + assertThat(location).contains("code="); + } + + private static

P signIn(HtmlPage page, String username, String password) throws IOException { + HtmlInput usernameInput = page.querySelector("input[name=\"username\"]"); + HtmlInput passwordInput = page.querySelector("input[name=\"password\"]"); + HtmlButton signInButton = page.querySelector("button"); + + usernameInput.type(username); + passwordInput.type(password); + return signInButton.click(); + } + + private static void assertLoginPage(HtmlPage page) { + assertThat(page.getUrl().toString()).endsWith("/login"); + + HtmlInput usernameInput = page.querySelector("input[name=\"username\"]"); + HtmlInput passwordInput = page.querySelector("input[name=\"password\"]"); + HtmlButton signInButton = page.querySelector("button"); + + assertThat(usernameInput).isNotNull(); + assertThat(passwordInput).isNotNull(); + assertThat(signInButton.getTextContent()).isEqualTo("Sign in"); + } + +} diff --git a/samples/messages-client/src/main/resources/application.yml b/samples/messages-client/src/main/resources/application.yml index fb78d63b94..a9317e17bb 100644 --- a/samples/messages-client/src/main/resources/application.yml +++ b/samples/messages-client/src/main/resources/application.yml @@ -41,7 +41,8 @@ spring: client-name: messaging-client-client-credentials provider: spring: - issuer-uri: http://auth-server:9000 + issuer-uri: http://localhost:9000 + #authorization-uri: ${spring.security.oauth2.client.provider.spring.issuer-uri}/oauth2/authorize?idp=google-idp messages: base-uri: http://127.0.0.1:8090/messages diff --git a/samples/messages-resource/src/main/java/sample/config/ResourceServerConfig.java b/samples/messages-resource/src/main/java/sample/config/ResourceServerConfig.java index d12e7d69d6..d74c2e696c 100644 --- a/samples/messages-resource/src/main/java/sample/config/ResourceServerConfig.java +++ b/samples/messages-resource/src/main/java/sample/config/ResourceServerConfig.java @@ -19,6 +19,9 @@ import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.web.SecurityFilterChain; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.CorsConfigurationSource; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; /** * @author Joe Grandja @@ -35,10 +38,23 @@ SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { .authorizeRequests() .mvcMatchers("/messages/**").access("hasAuthority('SCOPE_message.read')") .and() + .cors() + .and() .oauth2ResourceServer() .jwt(); return http.build(); } // @formatter:on + @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; + } } diff --git a/samples/messages-resource/src/main/resources/application.yml b/samples/messages-resource/src/main/resources/application.yml index af58f116ba..1e01f9455c 100644 --- a/samples/messages-resource/src/main/resources/application.yml +++ b/samples/messages-resource/src/main/resources/application.yml @@ -14,4 +14,4 @@ spring: oauth2: resourceserver: jwt: - issuer-uri: http://auth-server:9000 + issuer-uri: http://localhost:9000