From 52cc331d9c3f8637a282916e11b804bd349b5704 Mon Sep 17 00:00:00 2001 From: Steve Riesenberg Date: Thu, 29 Jul 2021 17:04:34 -0500 Subject: [PATCH] Update oauth login samples Closes gh-29 --- .../webflux/java/oauth2/login/README.adoc | 111 ++++++++++++++++-- .../example/LoopbackIpRedirectWebFilter.java | 65 ++++++++++ .../login/src/main/resources/application.yml | 13 ++ .../oauth2/authorization-server/README.adoc | 52 +------- ...2AuthorizationServerApplicationITests.java | 11 ++ .../src/main/java/example/Jwks.java | 48 -------- .../main/java/example/KeyGeneratorUtils.java | 44 ------- ...horizationServerSecurityConfiguration.java | 106 ++++++++++++----- .../spring-boot/java/oauth2/login/README.adoc | 107 +++++++++++++++-- .../example/OAuth2LoginApplicationTests.java | 10 +- .../filter/LoopbackIpRedirectFilter.java | 68 +++++++++++ .../login/src/main/resources/application.yml | 13 ++ 12 files changed, 455 insertions(+), 193 deletions(-) create mode 100644 reactive/webflux/java/oauth2/login/src/main/java/example/LoopbackIpRedirectWebFilter.java delete mode 100644 servlet/spring-boot/java/oauth2/authorization-server/src/main/java/example/Jwks.java delete mode 100644 servlet/spring-boot/java/oauth2/authorization-server/src/main/java/example/KeyGeneratorUtils.java create mode 100644 servlet/spring-boot/java/oauth2/login/src/main/java/example/filter/LoopbackIpRedirectFilter.java diff --git a/reactive/webflux/java/oauth2/login/README.adoc b/reactive/webflux/java/oauth2/login/README.adoc index a96fec3..fd56d02 100644 --- a/reactive/webflux/java/oauth2/login/README.adoc +++ b/reactive/webflux/java/oauth2/login/README.adoc @@ -1,18 +1,105 @@ -NOTE: Spring Security Reactive OAuth only supports authentication using a user info endpoint. -Support for JWT validation will be added in https://github.com/spring-projects/spring-security/issues/5330[gh-5330]. - = OAuth 2.0 Login Sample This guide provides instructions on setting up the sample application with OAuth 2.0 Login using an OAuth 2.0 Provider or OpenID Connect 1.0 Provider. -The sample application uses Spring Boot 2.0.0.M6 and the `spring-security-oauth2-client` module which is new in Spring Security 5.0. +The sample application uses Spring Boot 2.5 and the `spring-security-oauth2-client` module which is new in Spring Security 5.0. The following sections provide detailed steps for setting up OAuth 2.0 Login for these Providers: +* <> * <> * <> * <> * <> +[[spring-login]] +== Login with Spring Authorization Server + +This section shows how to configure the sample application using Spring Authorization Server as the Authentication Provider and covers the following topics: + +* <> +* <> +* <> +* <> + +[[spring-initial-setup]] +=== Initial setup + +The sample application is pre-configured to work out of the box with Spring Authorization Server, which runs locally on port `9000`. See the https://github.com/spring-projects/spring-security-samples/tree/main/servlet/spring-boot/java/oauth2/authorization-server[authorization-server sample] to run the authorization server used in this section. + +NOTE: https://github.com/spring-projects-external/spring-authorization-server[Spring Authorization Server] supports the https://openid.net/connect/[OpenID Connect 1.0] specification. + +[[spring-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 Spring Authorization Server +and have granted access to the OAuth Client on the Consent page. + +The default redirect URI is `http://127.0.0.1:8080/login/oauth2/code/login-client`. No special setup is required to use the sample locally. + +TIP: The default redirect URI template is `{baseUrl}/login/oauth2/code/{registrationId}`. +The *_registrationId_* is a unique identifier for the `ClientRegistration`. + +IMPORTANT: If the application is running behind a proxy server, it is recommended to check https://docs.spring.io/spring-security/site/docs/current/reference/htmlsingle/#appendix-proxy-server[Proxy Server Configuration] to ensure the application is correctly configured. +Also, see the supported https://docs.spring.io/spring-security/site/docs/current/reference/htmlsingle/#oauth2Client-auth-code-redirect-uri[`URI` template variables] for `redirect-uri`. + +[[spring-application-config]] +=== Configure application.yml + +If you wish to customize the OAuth Client to work with a non-local deployment of Spring Authorization Server, 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> + login-client: <2> + provider: spring <3> + client-id: login-client + client-secret: openid-connect + client-authentication-method: client_secret_basic + authorization-grant-type: authorization_code + redirect-uri: http://127.0.0.1:8080/login/oauth2/code/login-client + scope: openid,profile <4> + client-name: Spring + provider:<5> + spring: + authorization-uri: http://localhost:9000/oauth2/authorize + token-uri: http://localhost:9000/oauth2/token + jwk-set-uri: http://localhost:9000/oauth2/jwks + issuer-uri: http://localhost:9000 +---- ++ +.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 login-client. +<3> The `provider` property specifies which provider configuration is used by this `ClientRegistration`. +<4> The `openid` scope is required by Spring Authorization Server to perform https://openid.net/specs/openid-connect-core-1_0.html#CodeFlowAuth[authentication using OpenID Connect 1.0]. +<5> `spring.security.oauth2.client.provider` is the base property prefix for OAuth Provider properties. +==== + +. Replace the values in the `client-id` and `client-secret` property with the OAuth 2.0 credentials for your Spring Authorization Server. As well, replace `http://localhost:9000` in `authorization-uri`, `token-uri` and `jwk-set-uri` with the actual domain of your authorization server. + +[[spring-boot-application]] +=== Boot up the application + +Launch the Spring Boot 2.0 sample and go to `http://127.0.0.1:8080`. +You are then redirected to the default _auto-generated_ login page, which displays a link for Spring. + +Click on the Spring link, and you are then redirected to the Spring Authorization Server for authentication. + +After authenticating with your credentials (`user` and `password` by default), the next page presented to you is the Consent screen. +The Consent screen asks you to either allow or deny access to the OAuth Client. Select "profile" and +click *Submit Consent* to authorize the OAuth Client to access your basic profile information. + +At this point, the OAuth Client retrieves your basic profile information via the https://openid.net/specs/openid-connect-core-1_0.html#CodeIDToken[ID Token] and establishes an authenticated session. + +NOTE: Spring Authorization Server does not currently support the https://openid.net/specs/openid-connect-core-1_0.html#UserInfo[UserInfo Endpoint], which is optional in OpenID Connect 1.0. See https://github.com/spring-projects-experimental/spring-authorization-server/issues/176[#176] fo more information. + [[google-login]] == Login with Google @@ -41,7 +128,7 @@ After completing the "Obtain OAuth 2.0 credentials" instructions, you should hav 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:8080/login/oauth2/code/google`. +In the "Set a redirect URI" sub-section, ensure that the *Authorized redirect URIs* field is set to `http://127.0.0.1:8080/login/oauth2/code/google`. TIP: The default redirect URI template is `{baseUrl}/login/oauth2/code/{registrationId}`. The *_registrationId_* is a unique identifier for the `ClientRegistration`. @@ -79,7 +166,7 @@ spring: [[google-boot-application]] === Boot up the application -Launch the Spring Boot 2.0 sample and go to `http://localhost:8080`. +Launch the Spring Boot 2.0 sample and go to `http://127.0.0.1:8080`. You are then redirected to the default _auto-generated_ login page, which displays a link for Google. Click on the Google link, and you are then redirected to Google for authentication. @@ -105,7 +192,7 @@ This section shows how to configure the sample application using GitHub as the A 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:8080/login/oauth2/code/github`. +When registering the OAuth application, ensure the *Authorization callback URL* is set to `http://127.0.0.1:8080/login/oauth2/code/github`. 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. @@ -146,7 +233,7 @@ spring: [[github-boot-application]] === Boot up the application -Launch the Spring Boot 2.0 sample and go to `http://localhost:8080`. +Launch the Spring Boot 2.0 sample and go to `http://127.0.0.1:8080`. You are then redirected to the default _auto-generated_ login page, which displays a link for GitHub. Click on the GitHub link, and you are then redirected to GitHub for authentication. @@ -183,7 +270,7 @@ NOTE: The selection for the _Category_ field is not relevant but it's a required The next page presented is "Product Setup". Click the "Get Started" button for the *Facebook Login* product. In the left sidebar, under _Products -> Facebook Login_, select _Settings_. -For the field *Valid OAuth redirect URIs*, enter `http://localhost:8080/login/oauth2/code/facebook` then click _Save Changes_. +For the field *Valid OAuth redirect URIs*, enter `http://127.0.0.1:8080/login/oauth2/code/facebook` then click _Save Changes_. The OAuth redirect URI is the path in the application that the end-user's user-agent is redirected back to after they have authenticated with Facebook and have granted access to the application on the _Authorize application_ page. @@ -224,7 +311,7 @@ spring: [[facebook-boot-application]] === Boot up the application -Launch the Spring Boot 2.0 sample and go to `http://localhost:8080`. +Launch the Spring Boot 2.0 sample and go to `http://127.0.0.1:8080`. You are then redirected to the default _auto-generated_ login page, which displays a link for Facebook. Click on the Facebook link, and you are then redirected to Facebook for authentication. @@ -259,7 +346,7 @@ From the "Add Application" page, select the "Create New App" button and enter th Select the _Create_ button. On the "General Settings" page, enter the Application Name (for example, "Spring Security Okta Login") and then select the _Next_ button. -On the "Configure OpenID Connect" page, enter `http://localhost:8080/login/oauth2/code/okta` for the field *Redirect URIs* and then select _Finish_. +On the "Configure OpenID Connect" page, enter `http://127.0.0.1:8080/login/oauth2/code/okta` for the field *Redirect URIs* and then select _Finish_. 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 Okta and have granted access to the application on the _Authorize application_ page. @@ -315,7 +402,7 @@ As well, replace `https://your-subdomain.oktapreview.com` in `authorization-uri` [[okta-boot-application]] === Boot up the application -Launch the Spring Boot 2.0 sample and go to `http://localhost:8080`. +Launch the Spring Boot 2.0 sample and go to `http://127.0.0.1:8080`. You are then redirected to the default _auto-generated_ login page, which displays a link for Okta. Click on the Okta link, and you are then redirected to Okta for authentication. diff --git a/reactive/webflux/java/oauth2/login/src/main/java/example/LoopbackIpRedirectWebFilter.java b/reactive/webflux/java/oauth2/login/src/main/java/example/LoopbackIpRedirectWebFilter.java new file mode 100644 index 0000000..b8c36ed --- /dev/null +++ b/reactive/webflux/java/oauth2/login/src/main/java/example/LoopbackIpRedirectWebFilter.java @@ -0,0 +1,65 @@ +/* + * Copyright 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 example; + +import reactor.core.publisher.Mono; + +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Component; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.WebFilter; +import org.springframework.web.server.WebFilterChain; +import org.springframework.web.util.UriComponents; +import org.springframework.web.util.UriComponentsBuilder; + +/** + * This filter ensures that the loopback IP 127.0.0.1 is used to access the + * application so that the sample works correctly, due to the fact that redirect URIs with + * "localhost" are rejected by the Spring Authorization Server, because the OAuth 2.1 + * draft specification states: + * + *
+ *     While redirect URIs using localhost (i.e.,
+ *     "http://localhost:{port}/{path}") function similarly to loopback IP
+ *     redirects described in Section 10.3.3, the use of "localhost" is NOT
+ *     RECOMMENDED.
+ * 
+ * + * @author Steve Riesenberg + * @see Loopback Redirect + * Considerations in Native Apps + */ +@Component +@Order(Ordered.HIGHEST_PRECEDENCE) +public class LoopbackIpRedirectWebFilter implements WebFilter { + + @Override + public Mono filter(ServerWebExchange exchange, WebFilterChain chain) { + String host = exchange.getRequest().getURI().getHost(); + if (host != null && host.equals("localhost")) { + UriComponents uri = UriComponentsBuilder.fromHttpRequest(exchange.getRequest()).host("127.0.0.1").build(); + exchange.getResponse().setStatusCode(HttpStatus.PERMANENT_REDIRECT); + exchange.getResponse().getHeaders().setLocation(uri.toUri()); + return Mono.empty(); + } + return chain.filter(exchange); + } + +} diff --git a/reactive/webflux/java/oauth2/login/src/main/resources/application.yml b/reactive/webflux/java/oauth2/login/src/main/resources/application.yml index 73b08fb..d7159ca 100644 --- a/reactive/webflux/java/oauth2/login/src/main/resources/application.yml +++ b/reactive/webflux/java/oauth2/login/src/main/resources/application.yml @@ -15,6 +15,15 @@ spring: oauth2: client: registration: + login-client: + provider: spring + client-id: login-client + client-secret: openid-connect + client-authentication-method: client_secret_basic + authorization-grant-type: authorization_code + redirect-uri: http://127.0.0.1:8080/login/oauth2/code/login-client + scope: openid,profile + client-name: Spring google: client-id: your-app-client-id client-secret: your-app-client-secret @@ -28,6 +37,10 @@ spring: client-id: your-app-client-id client-secret: your-app-client-secret provider: + spring: + authorization-uri: http://localhost:9000/oauth2/authorize + token-uri: http://localhost:9000/oauth2/token + jwk-set-uri: http://localhost:9000/oauth2/jwks okta: authorization-uri: https://your-subdomain.oktapreview.com/oauth2/v1/authorize token-uri: https://your-subdomain.oktapreview.com/oauth2/v1/token diff --git a/servlet/spring-boot/java/oauth2/authorization-server/README.adoc b/servlet/spring-boot/java/oauth2/authorization-server/README.adoc index d1d2ccf..6d43ee6 100644 --- a/servlet/spring-boot/java/oauth2/authorization-server/README.adoc +++ b/servlet/spring-boot/java/oauth2/authorization-server/README.adoc @@ -1,6 +1,6 @@ = OAuth 2.0 Authorization Server Sample -This sample demonstrates Authorization Server with the `client_credentials` grant type. This authorization server is configured to generate JWT tokens signed with the `RS256` algorithm. +This sample demonstrates Authorization Server with the `authorization_code` and `client_credentials` grant types, as well as OpenID Connect 1.0. This authorization server is configured to generate JWT tokens signed with the `RS256` algorithm. * <> * <> @@ -19,29 +19,11 @@ Or import the project into your IDE and run `OAuth2AuthorizationServerApplicatio === What is it doing? -The tests are making requests to the token endpoint with the `client_credentials` grant type using the `client_secret_basic` authentication method, and subsequently verifying them using the token introspection endpoint. +The tests are making requests to the token endpoint with the `client_credentials` grant type using the `client_secret_basic` authentication method, and subsequently verifying the access token from the response using the token introspection endpoint. -The introspection endpoint response is used to verify the token (decode the JWT in this case), returning the payload including the requested scope: +The introspection endpoint response is used to verify the token (decode the JWT in this case), returning the payload including the requested scope. -```json -{ - "active": true, - "aud": [ - "messaging-client" - ], - "client_id": "messaging-client", - "exp": 1627070941, - "iat": 1627070641, - "iss": "http://localhost:9000", - "jti": "987599e3-1048-4fe8-89df-ad113aef2d6c", - "nbf": 1627070641, - "scope": "message:read", - "sub": "messaging-client", - "token_type": "Bearer" -} -``` - -Note that Spring Security does not require the token introspection endpoint when configured to use the Bearer scheme with JWTs, this is simply used for demonstration purposes. +NOTE: Spring Security does not require the token introspection endpoint when configured to use the Bearer scheme with JWTs, this is simply used for demonstration purposes. [[running-the-app]] == Running the app @@ -106,31 +88,9 @@ Which will return something like the following: [[testing-with-a-resource-server]] == Testing with a resource server -This sample can be used in conjunction with a resource server, such as the https://github.com/spring-projects/spring-security-samples/tree/main/servlet/spring-boot/java/oauth2/resource-server/hello-security[resource-server sample] in this project. +This sample can be used in conjunction with a resource server, such as the https://github.com/spring-projects/spring-security-samples/tree/main/servlet/spring-boot/java/oauth2/resource-server/hello-security[resource-server sample] in this project which is pre-configured to work with this authorization server sample out of the box. -To change the sample to point to this authorization server, simply find this property in that project's `application.yml`: - -```yaml -spring: - security: - oauth2: - resourceserver: - jwt: - jwk-set-uri: ${mockwebserver.url}/.well-known/jwks.json -``` - -And change the property to: - -```yaml -spring: - security: - oauth2: - resourceserver: - jwt: - jwk-set-uri: http://localhost:9000/oauth2/jwks -``` - -And then you can run that app similarly to the authorization server: +You can run that app similarly to the authorization server: ```bash ./gradlew bootRun diff --git a/servlet/spring-boot/java/oauth2/authorization-server/src/integTest/java/example/OAuth2AuthorizationServerApplicationITests.java b/servlet/spring-boot/java/oauth2/authorization-server/src/integTest/java/example/OAuth2AuthorizationServerApplicationITests.java index 431976b..9588324 100644 --- a/servlet/spring-boot/java/oauth2/authorization-server/src/integTest/java/example/OAuth2AuthorizationServerApplicationITests.java +++ b/servlet/spring-boot/java/oauth2/authorization-server/src/integTest/java/example/OAuth2AuthorizationServerApplicationITests.java @@ -106,6 +106,17 @@ public class OAuth2AuthorizationServerApplicationITests { // @formatter:on } + @Test + void performTokenRequestWhenGrantTypeNotRegisteredThenBadRequest() throws Exception { + // @formatter:off + this.mockMvc.perform(post("/oauth2/token") + .param("grant_type", "client_credentials") + .with(basicAuth("login-client", "openid-connect"))) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.error").value("unauthorized_client")); + // @formatter:on + } + @Test void performIntrospectionRequestWhenValidTokenThenOk() throws Exception { // @formatter:off diff --git a/servlet/spring-boot/java/oauth2/authorization-server/src/main/java/example/Jwks.java b/servlet/spring-boot/java/oauth2/authorization-server/src/main/java/example/Jwks.java deleted file mode 100644 index 7a6b6bd..0000000 --- a/servlet/spring-boot/java/oauth2/authorization-server/src/main/java/example/Jwks.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright 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 example; - -import java.security.KeyPair; -import java.security.interfaces.RSAPrivateKey; -import java.security.interfaces.RSAPublicKey; -import java.util.UUID; - -import com.nimbusds.jose.jwk.RSAKey; - -/** - * Utils for generating JWKs. - * - * @author Joe Grandja - */ -final class Jwks { - - private Jwks() { - } - - 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 - } - -} diff --git a/servlet/spring-boot/java/oauth2/authorization-server/src/main/java/example/KeyGeneratorUtils.java b/servlet/spring-boot/java/oauth2/authorization-server/src/main/java/example/KeyGeneratorUtils.java deleted file mode 100644 index 2def560..0000000 --- a/servlet/spring-boot/java/oauth2/authorization-server/src/main/java/example/KeyGeneratorUtils.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright 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 example; - -import java.security.KeyPair; -import java.security.KeyPairGenerator; - -/** - * Utils for generating keys. - * - * @author Joe Grandja - */ -final class KeyGeneratorUtils { - - private KeyGeneratorUtils() { - } - - 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; - } - -} diff --git a/servlet/spring-boot/java/oauth2/authorization-server/src/main/java/example/OAuth2AuthorizationServerSecurityConfiguration.java b/servlet/spring-boot/java/oauth2/authorization-server/src/main/java/example/OAuth2AuthorizationServerSecurityConfiguration.java index e965972..aa54351 100644 --- a/servlet/spring-boot/java/oauth2/authorization-server/src/main/java/example/OAuth2AuthorizationServerSecurityConfiguration.java +++ b/servlet/spring-boot/java/oauth2/authorization-server/src/main/java/example/OAuth2AuthorizationServerSecurityConfiguration.java @@ -16,28 +16,32 @@ package example; -import java.util.HashSet; -import java.util.Set; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.interfaces.RSAPrivateKey; +import java.security.interfaces.RSAPublicKey; import java.util.UUID; -import com.nimbusds.jose.JWSAlgorithm; import com.nimbusds.jose.jwk.JWKSet; import com.nimbusds.jose.jwk.RSAKey; import com.nimbusds.jose.jwk.source.ImmutableJWKSet; import com.nimbusds.jose.jwk.source.JWKSource; -import com.nimbusds.jose.proc.JWSKeySelector; -import com.nimbusds.jose.proc.JWSVerificationKeySelector; import com.nimbusds.jose.proc.SecurityContext; -import com.nimbusds.jwt.proc.ConfigurableJWTProcessor; -import com.nimbusds.jwt.proc.DefaultJWTProcessor; +import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Role; +import org.springframework.core.annotation.Order; +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.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration; -import org.springframework.security.config.http.SessionCreationPolicy; +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.oauth2.core.AuthorizationGrantType; import org.springframework.security.oauth2.core.ClientAuthenticationMethod; +import org.springframework.security.oauth2.core.oidc.OidcScopes; import org.springframework.security.oauth2.jwt.JwtDecoder; import org.springframework.security.oauth2.jwt.NimbusJwtDecoder; import org.springframework.security.oauth2.server.authorization.client.InMemoryRegisteredClientRepository; @@ -45,6 +49,7 @@ import org.springframework.security.oauth2.server.authorization.client.Registere 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.provisioning.InMemoryUserDetailsManager; import org.springframework.security.web.SecurityFilterChain; /** @@ -52,19 +57,23 @@ import org.springframework.security.web.SecurityFilterChain; * * @author Steve Riesenberg */ -@EnableWebSecurity +@Configuration public class OAuth2AuthorizationServerSecurityConfiguration { @Bean + @Order(1) public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception { OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http); + return http.formLogin(Customizer.withDefaults()).build(); + } + @Bean + @Order(2) + public SecurityFilterChain standardSecurityFilterChain(HttpSecurity http) throws Exception { // @formatter:off http - .sessionManagement((sessionManagement) -> - sessionManagement - .sessionCreationPolicy(SessionCreationPolicy.STATELESS) - ); + .authorizeRequests((requests) -> requests.anyRequest().authenticated()) + .formLogin(Customizer.withDefaults()); // @formatter:on return http.build(); @@ -73,6 +82,18 @@ public class OAuth2AuthorizationServerSecurityConfiguration { @Bean public RegisteredClientRepository registeredClientRepository() { // @formatter:off + RegisteredClient loginClient = RegisteredClient.withId(UUID.randomUUID().toString()) + .clientId("login-client") + .clientSecret("{noop}openid-connect") + .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC) + .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) + .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN) + .redirectUri("http://127.0.0.1:8080/login/oauth2/code/login-client") + .redirectUri("http://127.0.0.1:8080/authorized") + .scope(OidcScopes.OPENID) + .scope(OidcScopes.PROFILE) + .clientSettings(ClientSettings.builder().requireAuthorizationConsent(true).build()) + .build(); RegisteredClient registeredClient = RegisteredClient.withId(UUID.randomUUID().toString()) .clientId("messaging-client") .clientSecret("{noop}secret") @@ -80,34 +101,29 @@ public class OAuth2AuthorizationServerSecurityConfiguration { .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS) .scope("message:read") .scope("message:write") - .clientSettings(ClientSettings.builder().requireAuthorizationConsent(true).build()) .build(); // @formatter:on - return new InMemoryRegisteredClientRepository(registeredClient); + return new InMemoryRegisteredClientRepository(loginClient, registeredClient); } @Bean - public JWKSource jwkSource() { - RSAKey rsaKey = Jwks.generateRsa(); + public JWKSource jwkSource(KeyPair keyPair) { + RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic(); + RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate(); + // @formatter:off + RSAKey rsaKey = new RSAKey.Builder(publicKey) + .privateKey(privateKey) + .keyID(UUID.randomUUID().toString()) + .build(); + // @formatter:on JWKSet jwkSet = new JWKSet(rsaKey); return new ImmutableJWKSet<>(jwkSet); } @Bean - public JwtDecoder jwtDecoder(JWKSource jwkSource) { - Set jwsAlgs = new HashSet<>(); - jwsAlgs.addAll(JWSAlgorithm.Family.RSA); - jwsAlgs.addAll(JWSAlgorithm.Family.EC); - jwsAlgs.addAll(JWSAlgorithm.Family.HMAC_SHA); - ConfigurableJWTProcessor jwtProcessor = new DefaultJWTProcessor<>(); - JWSKeySelector jwsKeySelector = new JWSVerificationKeySelector<>(jwsAlgs, jwkSource); - jwtProcessor.setJWSKeySelector(jwsKeySelector); - // Override the default Nimbus claims set verifier as NimbusJwtDecoder handles it - // instead - jwtProcessor.setJWTClaimsSetVerifier((claims, context) -> { - }); - return new NimbusJwtDecoder(jwtProcessor); + public JwtDecoder jwtDecoder(KeyPair keyPair) { + return NimbusJwtDecoder.withPublicKey((RSAPublicKey) keyPair.getPublic()).build(); } @Bean @@ -115,4 +131,32 @@ public class OAuth2AuthorizationServerSecurityConfiguration { return ProviderSettings.builder().issuer("http://localhost:9000").build(); } + @Bean + public UserDetailsService userDetailsService() { + // @formatter:off + UserDetails userDetails = User.withDefaultPasswordEncoder() + .username("user") + .password("password") + .roles("USER") + .build(); + // @formatter:on + + return new InMemoryUserDetailsManager(userDetails); + } + + @Bean + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + 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; + } + } diff --git a/servlet/spring-boot/java/oauth2/login/README.adoc b/servlet/spring-boot/java/oauth2/login/README.adoc index e949a7f..e892662 100644 --- a/servlet/spring-boot/java/oauth2/login/README.adoc +++ b/servlet/spring-boot/java/oauth2/login/README.adoc @@ -1,15 +1,104 @@ = OAuth 2.0 Login Sample This guide provides instructions on setting up the sample application with OAuth 2.0 Login using an OAuth 2.0 Provider or OpenID Connect 1.0 Provider. -The sample application uses Spring Boot 2.0.0.M6 and the `spring-security-oauth2-client` module which is new in Spring Security 5.0. +The sample application uses Spring Boot 2.5 and the `spring-security-oauth2-client` module which is new in Spring Security 5.0. The following sections provide detailed steps for setting up OAuth 2.0 Login for these Providers: +* <> * <> * <> * <> * <> +[[spring-login]] +== Login with Spring Authorization Server + +This section shows how to configure the sample application using Spring Authorization Server as the Authentication Provider and covers the following topics: + +* <> +* <> +* <> +* <> + +[[spring-initial-setup]] +=== Initial setup + +The sample application is pre-configured to work out of the box with Spring Authorization Server, which runs locally on port `9000`. See the https://github.com/spring-projects/spring-security-samples/tree/main/servlet/spring-boot/java/oauth2/authorization-server[authorization-server sample] to run the authorization server used in this section. + +NOTE: https://github.com/spring-projects-external/spring-authorization-server[Spring Authorization Server] supports the https://openid.net/connect/[OpenID Connect 1.0] specification. + +[[spring-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 Spring Authorization Server +and have granted access to the OAuth Client on the Consent page. + +The default redirect URI is `http://127.0.0.1:8080/login/oauth2/code/login-client`. No special setup is required to use the sample locally. + +TIP: The default redirect URI template is `{baseUrl}/login/oauth2/code/{registrationId}`. +The *_registrationId_* is a unique identifier for the `ClientRegistration`. + +IMPORTANT: If the application is running behind a proxy server, it is recommended to check https://docs.spring.io/spring-security/site/docs/current/reference/htmlsingle/#appendix-proxy-server[Proxy Server Configuration] to ensure the application is correctly configured. +Also, see the supported https://docs.spring.io/spring-security/site/docs/current/reference/htmlsingle/#oauth2Client-auth-code-redirect-uri[`URI` template variables] for `redirect-uri`. + +[[spring-application-config]] +=== Configure application.yml + +If you wish to customize the OAuth Client to work with a non-local deployment of Spring Authorization Server, 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> + login-client: <2> + provider: spring <3> + client-id: login-client + client-secret: openid-connect + client-authentication-method: client_secret_basic + authorization-grant-type: authorization_code + redirect-uri: http://127.0.0.1:8080/login/oauth2/code/login-client + scope: openid,profile <4> + client-name: Spring + provider: <5> + spring: + authorization-uri: http://localhost:9000/oauth2/authorize + token-uri: http://localhost:9000/oauth2/token + jwk-set-uri: http://localhost:9000/oauth2/jwks +---- ++ +.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 login-client. +<3> The `provider` property specifies which provider configuration is used by this `ClientRegistration`. +<4> The `openid` scope is required by Spring Authorization Server to perform https://openid.net/specs/openid-connect-core-1_0.html#CodeFlowAuth[authentication using OpenID Connect 1.0]. +<5> `spring.security.oauth2.client.provider` is the base property prefix for OAuth Provider properties. +==== + +. Replace the values in the `client-id` and `client-secret` property with the OAuth 2.0 credentials for your Spring Authorization Server. As well, replace `http://localhost:9000` in `authorization-uri`, `token-uri` and `jwk-set-uri` with the actual domain of your authorization server. + +[[spring-boot-application]] +=== Boot up the application + +Launch the Spring Boot 2.0 sample and go to `http://127.0.0.1:8080`. +You are then redirected to the default _auto-generated_ login page, which displays a link for Spring. + +Click on the Spring link, and you are then redirected to the Spring Authorization Server for authentication. + +After authenticating with your credentials (`user` and `password` by default), the next page presented to you is the Consent screen. +The Consent screen asks you to either allow or deny access to the OAuth Client. Select "profile" and +click *Submit Consent* to authorize the OAuth Client to access your basic profile information. + +At this point, the OAuth Client retrieves your basic profile information via the https://openid.net/specs/openid-connect-core-1_0.html#CodeIDToken[ID Token] and establishes an authenticated session. + +NOTE: Spring Authorization Server does not currently support the https://openid.net/specs/openid-connect-core-1_0.html#UserInfo[UserInfo Endpoint], which is optional in OpenID Connect 1.0. See https://github.com/spring-projects-experimental/spring-authorization-server/issues/176[#176] fo more information. + [[google-login]] == Login with Google @@ -38,7 +127,7 @@ After completing the "Obtain OAuth 2.0 credentials" instructions, you should hav 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:8080/login/oauth2/code/google`. +In the "Set a redirect URI" sub-section, ensure that the *Authorized redirect URIs* field is set to `http://127.0.0.1:8080/login/oauth2/code/google`. TIP: The default redirect URI template is `{baseUrl}/login/oauth2/code/{registrationId}`. The *_registrationId_* is a unique identifier for the `ClientRegistration`. @@ -76,7 +165,7 @@ spring: [[google-boot-application]] === Boot up the application -Launch the Spring Boot 2.0 sample and go to `http://localhost:8080`. +Launch the Spring Boot 2.0 sample and go to `http://127.0.0.1:8080`. You are then redirected to the default _auto-generated_ login page, which displays a link for Google. Click on the Google link, and you are then redirected to Google for authentication. @@ -102,7 +191,7 @@ This section shows how to configure the sample application using GitHub as the A 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:8080/login/oauth2/code/github`. +When registering the OAuth application, ensure the *Authorization callback URL* is set to `http://127.0.0.1:8080/login/oauth2/code/github`. 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. @@ -143,7 +232,7 @@ spring: [[github-boot-application]] === Boot up the application -Launch the Spring Boot 2.0 sample and go to `http://localhost:8080`. +Launch the Spring Boot 2.0 sample and go to `http://127.0.0.1:8080`. You are then redirected to the default _auto-generated_ login page, which displays a link for GitHub. Click on the GitHub link, and you are then redirected to GitHub for authentication. @@ -180,7 +269,7 @@ NOTE: The selection for the _Category_ field is not relevant but it's a required The next page presented is "Product Setup". Click the "Get Started" button for the *Facebook Login* product. In the left sidebar, under _Products -> Facebook Login_, select _Settings_. -For the field *Valid OAuth redirect URIs*, enter `http://localhost:8080/login/oauth2/code/facebook` then click _Save Changes_. +For the field *Valid OAuth redirect URIs*, enter `http://127.0.0.1:8080/login/oauth2/code/facebook` then click _Save Changes_. The OAuth redirect URI is the path in the application that the end-user's user-agent is redirected back to after they have authenticated with Facebook and have granted access to the application on the _Authorize application_ page. @@ -221,7 +310,7 @@ spring: [[facebook-boot-application]] === Boot up the application -Launch the Spring Boot 2.0 sample and go to `http://localhost:8080`. +Launch the Spring Boot 2.0 sample and go to `http://127.0.0.1:8080`. You are then redirected to the default _auto-generated_ login page, which displays a link for Facebook. Click on the Facebook link, and you are then redirected to Facebook for authentication. @@ -256,7 +345,7 @@ From the "Add Application" page, select the "Create New App" button and enter th Select the _Create_ button. On the "General Settings" page, enter the Application Name (for example, "Spring Security Okta Login") and then select the _Next_ button. -On the "Configure OpenID Connect" page, enter `http://localhost:8080/login/oauth2/code/okta` for the field *Redirect URIs* and then select _Finish_. +On the "Configure OpenID Connect" page, enter `http://127.0.0.1:8080/login/oauth2/code/okta` for the field *Redirect URIs* and then select _Finish_. 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 Okta and have granted access to the application on the _Authorize application_ page. @@ -312,7 +401,7 @@ As well, replace `https://your-subdomain.oktapreview.com` in `authorization-uri` [[okta-boot-application]] === Boot up the application -Launch the Spring Boot 2.0 sample and go to `http://localhost:8080`. +Launch the Spring Boot 2.0 sample and go to `http://127.0.0.1:8080`. You are then redirected to the default _auto-generated_ login page, which displays a link for Okta. Click on the Okta link, and you are then redirected to Okta for authentication. diff --git a/servlet/spring-boot/java/oauth2/login/src/integTest/java/example/OAuth2LoginApplicationTests.java b/servlet/spring-boot/java/oauth2/login/src/integTest/java/example/OAuth2LoginApplicationTests.java index 3a48ffb..ddc60c8 100644 --- a/servlet/spring-boot/java/oauth2/login/src/integTest/java/example/OAuth2LoginApplicationTests.java +++ b/servlet/spring-boot/java/oauth2/login/src/integTest/java/example/OAuth2LoginApplicationTests.java @@ -267,7 +267,7 @@ public class OAuth2LoginApplicationTests { private void assertLoginPage(HtmlPage page) { assertThat(page.getTitleText()).isEqualTo("Please sign in"); - int expectedClients = 4; + int expectedClients = 5; List clientAnchorElements = page.getAnchors(); assertThat(clientAnchorElements.size()).isEqualTo(expectedClients); @@ -277,19 +277,23 @@ public class OAuth2LoginApplicationTests { ClientRegistration facebookClientRegistration = this.clientRegistrationRepository .findByRegistrationId("facebook"); ClientRegistration oktaClientRegistration = this.clientRegistrationRepository.findByRegistrationId("okta"); + ClientRegistration springClientRegistration = this.clientRegistrationRepository + .findByRegistrationId("login-client"); String baseAuthorizeUri = AUTHORIZATION_BASE_URI + "/"; String googleClientAuthorizeUri = baseAuthorizeUri + googleClientRegistration.getRegistrationId(); String githubClientAuthorizeUri = baseAuthorizeUri + githubClientRegistration.getRegistrationId(); String facebookClientAuthorizeUri = baseAuthorizeUri + facebookClientRegistration.getRegistrationId(); String oktaClientAuthorizeUri = baseAuthorizeUri + oktaClientRegistration.getRegistrationId(); + String springClientAuthorizeUri = baseAuthorizeUri + springClientRegistration.getRegistrationId(); for (int i = 0; i < expectedClients; i++) { assertThat(clientAnchorElements.get(i).getAttribute("href")).isIn(googleClientAuthorizeUri, - githubClientAuthorizeUri, facebookClientAuthorizeUri, oktaClientAuthorizeUri); + githubClientAuthorizeUri, facebookClientAuthorizeUri, oktaClientAuthorizeUri, + springClientAuthorizeUri); assertThat(clientAnchorElements.get(i).asText()).isIn(googleClientRegistration.getClientName(), githubClientRegistration.getClientName(), facebookClientRegistration.getClientName(), - oktaClientRegistration.getClientName()); + oktaClientRegistration.getClientName(), springClientRegistration.getClientName()); } } diff --git a/servlet/spring-boot/java/oauth2/login/src/main/java/example/filter/LoopbackIpRedirectFilter.java b/servlet/spring-boot/java/oauth2/login/src/main/java/example/filter/LoopbackIpRedirectFilter.java new file mode 100644 index 0000000..d9f00e5 --- /dev/null +++ b/servlet/spring-boot/java/oauth2/login/src/main/java/example/filter/LoopbackIpRedirectFilter.java @@ -0,0 +1,68 @@ +/* + * Copyright 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 example.filter; + +import java.io.IOException; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.http.server.ServletServerHttpRequest; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; +import org.springframework.web.util.UriComponents; +import org.springframework.web.util.UriComponentsBuilder; + +/** + * This filter ensures that the loopback IP 127.0.0.1 is used to access the + * application so that the sample works correctly, due to the fact that redirect URIs with + * "localhost" are rejected by the Spring Authorization Server, because the OAuth 2.1 + * draft specification states: + * + *
+ *     While redirect URIs using localhost (i.e.,
+ *     "http://localhost:{port}/{path}") function similarly to loopback IP
+ *     redirects described in Section 10.3.3, the use of "localhost" is NOT
+ *     RECOMMENDED.
+ * 
+ * + * @author Steve Riesenberg + * @see Loopback Redirect + * Considerations in Native Apps + */ +@Component +@Order(Ordered.HIGHEST_PRECEDENCE) +public class LoopbackIpRedirectFilter extends OncePerRequestFilter { + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + if (request.getServerName().equals("localhost") && request.getHeader("host") != null) { + UriComponents uri = UriComponentsBuilder.fromHttpRequest(new ServletServerHttpRequest(request)) + .host("127.0.0.1").build(); + response.sendRedirect(uri.toUriString()); + return; + } + filterChain.doFilter(request, response); + } + +} diff --git a/servlet/spring-boot/java/oauth2/login/src/main/resources/application.yml b/servlet/spring-boot/java/oauth2/login/src/main/resources/application.yml index 73b08fb..d7159ca 100644 --- a/servlet/spring-boot/java/oauth2/login/src/main/resources/application.yml +++ b/servlet/spring-boot/java/oauth2/login/src/main/resources/application.yml @@ -15,6 +15,15 @@ spring: oauth2: client: registration: + login-client: + provider: spring + client-id: login-client + client-secret: openid-connect + client-authentication-method: client_secret_basic + authorization-grant-type: authorization_code + redirect-uri: http://127.0.0.1:8080/login/oauth2/code/login-client + scope: openid,profile + client-name: Spring google: client-id: your-app-client-id client-secret: your-app-client-secret @@ -28,6 +37,10 @@ spring: client-id: your-app-client-id client-secret: your-app-client-secret provider: + spring: + authorization-uri: http://localhost:9000/oauth2/authorize + token-uri: http://localhost:9000/oauth2/token + jwk-set-uri: http://localhost:9000/oauth2/jwks okta: authorization-uri: https://your-subdomain.oktapreview.com/oauth2/v1/authorize token-uri: https://your-subdomain.oktapreview.com/oauth2/v1/token