Pārlūkot izejas kodu

Update oauth login samples

Closes gh-29
Steve Riesenberg 4 gadi atpakaļ
vecāks
revīzija
52cc331d9c

+ 99 - 12
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, Spring Authorization Server>>
 * <<google-login, Google>>
 * <<github-login, GitHub>>
 * <<facebook-login, Facebook>>
 * <<okta-login, Okta>>
 
+[[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>>
+* <<spring-redirect-uri,Setting the redirect URI>>
+* <<spring-application-config,Configure application.yml>>
+* <<spring-boot-application,Boot up the application>>
+
+[[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.

+ 65 - 0
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 <code>127.0.0.1</code> 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:
+ *
+ * <pre>
+ *     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.
+ * </pre>
+ *
+ * @author Steve Riesenberg
+ * @see <a href=
+ * "https://tools.ietf.org/html/draft-ietf-oauth-v2-1-01#section-9.7.1">Loopback Redirect
+ * Considerations in Native Apps</a>
+ */
+@Component
+@Order(Ordered.HIGHEST_PRECEDENCE)
+public class LoopbackIpRedirectWebFilter implements WebFilter {
+
+	@Override
+	public Mono<Void> 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);
+	}
+
+}

+ 13 - 0
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

+ 6 - 46
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.
 
 * <<running-the-tests, Running the tests>>
 * <<running-the-app, Running the app>>
@@ -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.
-
-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
-```
+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.
 
-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

+ 11 - 0
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

+ 0 - 48
servlet/spring-boot/java/oauth2/authorization-server/src/main/java/example/Jwks.java

@@ -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
-	}
-
-}

+ 0 - 44
servlet/spring-boot/java/oauth2/authorization-server/src/main/java/example/KeyGeneratorUtils.java

@@ -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;
-	}
-
-}

+ 75 - 31
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<SecurityContext> jwkSource() {
-		RSAKey rsaKey = Jwks.generateRsa();
+	public JWKSource<SecurityContext> 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<SecurityContext> jwkSource) {
-		Set<JWSAlgorithm> jwsAlgs = new HashSet<>();
-		jwsAlgs.addAll(JWSAlgorithm.Family.RSA);
-		jwsAlgs.addAll(JWSAlgorithm.Family.EC);
-		jwsAlgs.addAll(JWSAlgorithm.Family.HMAC_SHA);
-		ConfigurableJWTProcessor<SecurityContext> jwtProcessor = new DefaultJWTProcessor<>();
-		JWSKeySelector<SecurityContext> 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;
+	}
+
 }

+ 98 - 9
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, Spring Authorization Server>>
 * <<google-login, Google>>
 * <<github-login, GitHub>>
 * <<facebook-login, Facebook>>
 * <<okta-login, Okta>>
 
+[[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>>
+* <<spring-redirect-uri,Setting the redirect URI>>
+* <<spring-application-config,Configure application.yml>>
+* <<spring-boot-application,Boot up the application>>
+
+[[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.

+ 7 - 3
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<HtmlAnchor> 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());
 		}
 	}
 

+ 68 - 0
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 <code>127.0.0.1</code> 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:
+ *
+ * <pre>
+ *     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.
+ * </pre>
+ *
+ * @author Steve Riesenberg
+ * @see <a href=
+ * "https://tools.ietf.org/html/draft-ietf-oauth-v2-1-01#section-9.7.1">Loopback Redirect
+ * Considerations in Native Apps</a>
+ */
+@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);
+	}
+
+}

+ 13 - 0
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