diff --git a/samples/README.adoc b/samples/README.adoc index b3501cef..bd0bc87c 100644 --- a/samples/README.adoc +++ b/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` \ 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 00000000..e9d43819 --- /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 00000000..4a6b99e4 --- /dev/null +++ b/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); + } + +} 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 00000000..9ea71019 --- /dev/null +++ b/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 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 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/DefaultSecurityConfig.java b/samples/federated-identity-authorizationserver/src/main/java/sample/config/DefaultSecurityConfig.java new file mode 100644 index 00000000..34855b16 --- /dev/null +++ b/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 + +} 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 00000000..43285fa5 --- /dev/null +++ b/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 + } +} 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 00000000..29024726 --- /dev/null +++ b/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; + } +} 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 00000000..0b492cda --- /dev/null +++ b/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; + } + +} 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 00000000..104eb6d5 --- /dev/null +++ b/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 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 00000000..6cf39a2e --- /dev/null +++ b/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 { + + 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 OAuth 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 + +} 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 00000000..32000861 --- /dev/null +++ b/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 { + + 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 00000000..b7094681 --- /dev/null +++ b/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 { + + 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 00000000..3de4f669 --- /dev/null +++ b/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"; + } + +} 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 00000000..71bbbd96 --- /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: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 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 00000000..7c8323f5 --- /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