diff --git a/spring-security-modules/spring-security-auth-server/pom.xml b/spring-security-modules/spring-security-auth-server/pom.xml index 77b4deb8fe51..2d11796e9415 100644 --- a/spring-security-modules/spring-security-auth-server/pom.xml +++ b/spring-security-modules/spring-security-auth-server/pom.xml @@ -13,7 +13,7 @@ spring-security-auth-server 21 - 4.0.1 + 4.0.5 1.5.22 6.0.1 5.20.0 @@ -21,6 +21,7 @@ 3.27.6 2.0.17 6.0.1 + 1.22.2 @@ -29,6 +30,13 @@ spring-boot-starter-security-oauth2-authorization-server ${spring-boot.version} + + + org.springframework.boot + spring-boot-starter-thymeleaf + ${spring-boot.version} + + org.springframework.boot spring-boot-starter-security-oauth2-authorization-server-test @@ -40,6 +48,12 @@ ${junit-platform.version} test + + org.jsoup + jsoup + ${jsoup.version} + test + @@ -59,6 +73,13 @@ + + org.springframework.boot + spring-boot-maven-plugin + + true + + diff --git a/spring-security-modules/spring-security-auth-server/src/main/java/com/baeldung/auth/server/dynamicscopes/DynamicScopesAuthServerApplication.java b/spring-security-modules/spring-security-auth-server/src/main/java/com/baeldung/auth/server/dynamicscopes/DynamicScopesAuthServerApplication.java new file mode 100644 index 000000000000..d0adcbfa480c --- /dev/null +++ b/spring-security-modules/spring-security-auth-server/src/main/java/com/baeldung/auth/server/dynamicscopes/DynamicScopesAuthServerApplication.java @@ -0,0 +1,14 @@ +package com.baeldung.auth.server.dynamicscopes; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Import; + +@SpringBootApplication +@Import(com.baeldung.auth.server.dynamicscopes.config.AuthServerConfiguration.class) +public class DynamicScopesAuthServerApplication { + + public static void main(String[] args) { + SpringApplication.run(DynamicScopesAuthServerApplication.class, args); + } +} diff --git a/spring-security-modules/spring-security-auth-server/src/main/java/com/baeldung/auth/server/dynamicscopes/components/DynamicScopeService.java b/spring-security-modules/spring-security-auth-server/src/main/java/com/baeldung/auth/server/dynamicscopes/components/DynamicScopeService.java new file mode 100644 index 000000000000..8e90feeaa321 --- /dev/null +++ b/spring-security-modules/spring-security-auth-server/src/main/java/com/baeldung/auth/server/dynamicscopes/components/DynamicScopeService.java @@ -0,0 +1,10 @@ +package com.baeldung.auth.server.dynamicscopes.components; + +import java.util.Set; + +public interface DynamicScopeService { + + boolean validate(String clientId, Set scopes); + + boolean isConsentNeeded(String clientId, Set requestedScopes); +} diff --git a/spring-security-modules/spring-security-auth-server/src/main/java/com/baeldung/auth/server/dynamicscopes/components/impl/DynamicScopeServiceImpl.java b/spring-security-modules/spring-security-auth-server/src/main/java/com/baeldung/auth/server/dynamicscopes/components/impl/DynamicScopeServiceImpl.java new file mode 100644 index 000000000000..cbb08744f7e2 --- /dev/null +++ b/spring-security-modules/spring-security-auth-server/src/main/java/com/baeldung/auth/server/dynamicscopes/components/impl/DynamicScopeServiceImpl.java @@ -0,0 +1,27 @@ +package com.baeldung.auth.server.dynamicscopes.components.impl; + +import com.baeldung.auth.server.dynamicscopes.components.DynamicScopeService; +import org.slf4j.Logger; +import org.springframework.stereotype.Service; + +import java.util.Set; + +@Service +public class DynamicScopeServiceImpl implements DynamicScopeService { + private static final Logger log = org.slf4j.LoggerFactory.getLogger(DynamicScopeServiceImpl.class); + @Override + public boolean validate(String clientId, Set scopes) { + // Any scope starting with TX: is valid + return scopes.stream() + .filter(scope -> scope.toUpperCase().startsWith("TX:")) + .map(scope -> true) + .findFirst() + .orElse(false); + } + + @Override + public boolean isConsentNeeded(String clientId, Set requestedScopes) { + log.debug("isConsentNeeded: clientId={}, requestedStopes={}",clientId, requestedScopes); + return true; + } +} diff --git a/spring-security-modules/spring-security-auth-server/src/main/java/com/baeldung/auth/server/dynamicscopes/config/AuthServerConfiguration.java b/spring-security-modules/spring-security-auth-server/src/main/java/com/baeldung/auth/server/dynamicscopes/config/AuthServerConfiguration.java new file mode 100644 index 000000000000..2cdae779dcb1 --- /dev/null +++ b/spring-security-modules/spring-security-auth-server/src/main/java/com/baeldung/auth/server/dynamicscopes/config/AuthServerConfiguration.java @@ -0,0 +1,168 @@ +package com.baeldung.auth.server.dynamicscopes.config; + +import com.baeldung.auth.server.dynamicscopes.components.DynamicScopeService; +import org.slf4j.Logger; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.security.autoconfigure.web.servlet.SecurityFilterProperties; +import org.springframework.boot.security.oauth2.server.authorization.autoconfigure.servlet.OAuth2AuthorizationServerAutoConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.http.MediaType; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configurers.oauth2.server.authorization.OAuth2AuthorizationServerConfigurer; +import org.springframework.security.oauth2.core.OAuth2Error; +import org.springframework.security.oauth2.core.OAuth2ErrorCodes; +import org.springframework.security.oauth2.server.authorization.InMemoryOAuth2AuthorizationConsentService; +import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsentService; +import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeRequestAuthenticationContext; +import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeRequestAuthenticationException; +import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeRequestAuthenticationProvider; +import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeRequestAuthenticationToken; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint; +import org.springframework.security.web.util.matcher.MediaTypeRequestMatcher; +import org.springframework.security.web.util.matcher.RequestMatcher; + +import java.util.HashSet; +import java.util.Set; +import java.util.function.Consumer; +import java.util.function.Predicate; + +import static org.springframework.security.config.Customizer.withDefaults; + +@Configuration +public class AuthServerConfiguration { + private static final Logger log = org.slf4j.LoggerFactory.getLogger(AuthServerConfiguration.class); + + private final DynamicScopeService dynamicScopeService; + + public AuthServerConfiguration(DynamicScopeService dynamicScopeService) { + this.dynamicScopeService = dynamicScopeService; + } + + @Bean + @Order(Ordered.HIGHEST_PRECEDENCE) + SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) { + + log.info("Creating custom authorizationServer configuration"); + + http.oauth2AuthorizationServer(authorizationServer -> { + http.securityMatcher(authorizationServer.getEndpointsMatcher()); + + authorizationServer + .oidc(withDefaults()) + .authorizationEndpoint(ap -> { + ap.consentPage("/consent"); + ap.authenticationProviders(providers -> { + providers.stream() + .filter(OAuth2AuthorizationCodeRequestAuthenticationProvider.class::isInstance) + .map(p -> (OAuth2AuthorizationCodeRequestAuthenticationProvider)p) + .findFirst() + .ifPresent(p -> { + p.setAuthenticationValidator(dynamicScopesAuthenticationValidator()); + p.setAuthorizationConsentRequired(dynamicScopesConsentValidator()); + }); + }); + }); + + }); + http.authorizeHttpRequests(authorize -> authorize.anyRequest().authenticated()); + http.oauth2ResourceServer(resourceServer -> resourceServer.jwt(withDefaults())); + http.exceptionHandling(exceptions -> exceptions.defaultAuthenticationEntryPointFor( + new LoginUrlAuthenticationEntryPoint("/login"), createRequestMatcher())); + return http.build(); + } + + @Bean + @Order(SecurityFilterProperties.BASIC_AUTH_ORDER) + SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) { + http.authorizeHttpRequests(authorize -> { + authorize.anyRequest().authenticated(); + }) + .formLogin(withDefaults()); + return http.build(); + } + + private static RequestMatcher createRequestMatcher() { + MediaTypeRequestMatcher requestMatcher = new MediaTypeRequestMatcher(MediaType.TEXT_HTML); + requestMatcher.setIgnoredMediaTypes(Set.of(MediaType.ALL)); + return requestMatcher; + } + + + private Consumer dynamicScopesAuthenticationValidator() { + + return ctx -> { + + OAuth2AuthorizationCodeRequestAuthenticationToken auth = ctx.getAuthentication(); + var registeredClient = ctx.getRegisteredClient(); + + var requestedScopes = new HashSet<>(auth.getScopes()); + if ( requestedScopes.isEmpty() ) { + // No scopes requested. This is fine. + return; + } + + // Filter out dynamic scopes from the requested scopes. We will handle them separately. + var allowedScopes = registeredClient.getScopes(); + requestedScopes.removeIf(allowedScopes::contains); + if (requestedScopes.isEmpty() ) { + // Request contains only static scopes. This is fine. + return; + } + + // Now, let's validate the remaining scopes using the provided validation service + try { + if (!dynamicScopeService.validate(registeredClient.getId(), requestedScopes)) { + throw new OAuth2AuthorizationCodeRequestAuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_SCOPE), auth); + } + } catch (Exception ex) { + // Spring Security requires that any error should be reported wrapped in an OAuth2AuthorizationCodeRequestAuthenticationException, + // so we do that here. + throw new OAuth2AuthorizationCodeRequestAuthenticationException(new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR), ex, auth); + } + }; + + + } + + private Predicate dynamicScopesConsentValidator() { + return ctx -> { + + var lastConsent = ctx.getAuthorizationConsent(); + + if ( lastConsent == null ) { + // First consent, so consent is required + return true; + } + + OAuth2AuthorizationCodeRequestAuthenticationToken auth = ctx.getAuthentication(); + var requestedScopes = new HashSet<>(auth.getScopes()); // + if ( requestedScopes.isEmpty() ) { + // No scopes requested, so no consent required + return false; + } + + // Remove already consented scopes + var alreadyConsented = new HashSet<>(lastConsent.getScopes()); + requestedScopes.removeIf(alreadyConsented::contains); + if (requestedScopes.isEmpty() ) { + // Request contains only previously consented scopes. No consent required. + return false; + } + + // Any remaining scopes are dynamic scopes or static ones with no previous consent. Ask the service + return dynamicScopeService.isConsentNeeded(ctx.getRegisteredClient().getId(), requestedScopes); + }; + } + + @Bean + OAuth2AuthorizationConsentService dynamicScopesConsentService() { + return new InMemoryOAuth2AuthorizationConsentService(); + } + + + +} diff --git a/spring-security-modules/spring-security-auth-server/src/main/java/com/baeldung/auth/server/dynamicscopes/controller/ConsentController.java b/spring-security-modules/spring-security-auth-server/src/main/java/com/baeldung/auth/server/dynamicscopes/controller/ConsentController.java new file mode 100644 index 000000000000..804a82ab6bdd --- /dev/null +++ b/spring-security-modules/spring-security-auth-server/src/main/java/com/baeldung/auth/server/dynamicscopes/controller/ConsentController.java @@ -0,0 +1,57 @@ +package com.baeldung.auth.server.dynamicscopes.controller; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; +import org.springframework.security.oauth2.core.oidc.OidcScopes; +import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsentService; +import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; + +import java.security.Principal; +import java.util.Set; + +@Controller +public class ConsentController { + private static final Logger log = LoggerFactory.getLogger(ConsentController.class); + + private final RegisteredClientRepository registeredClientRepository; + private final OAuth2AuthorizationConsentService authorizationConsentService; + + public ConsentController(RegisteredClientRepository registeredClientRepository, OAuth2AuthorizationConsentService authorizationConsentService) { + this.registeredClientRepository = registeredClientRepository; + this.authorizationConsentService = authorizationConsentService; + } + + @GetMapping("/consent") + public String consent(Principal principal, Model model, + @RequestParam(name = OAuth2ParameterNames.CLIENT_ID) String clientId, + @RequestParam(name = OAuth2ParameterNames.SCOPE) String scope, + @RequestParam(name = OAuth2ParameterNames.STATE) String state) { + + log.info("Principal: {}", principal); + + var client = registeredClientRepository.findByClientId(clientId); + assert client != null; + var currentConsent = authorizationConsentService.findById(client.getId(), principal.getName()); + Set authorizedScopes = currentConsent != null ? currentConsent.getScopes() : Set.of(); + + // Remove already authorized scopes from the requested scopes and the special 'openid' scope. + var neededScopes = Set.of(scope.split(" ")).stream() + .filter(s -> !authorizedScopes.contains(s) && !OidcScopes.OPENID.equals(s)) + .toList(); + + + model.addAttribute("clientId", clientId); + model.addAttribute("clientName", client.getClientName() != null ? client.getClientName() : client.getClientId()); + model.addAttribute("scopes", neededScopes); + model.addAttribute("state", state); + model.addAttribute("authorizedScopes", authorizedScopes); + + return "consent"; + + } +} diff --git a/spring-security-modules/spring-security-auth-server/src/main/resources/application-dynamic-scopes.yaml b/spring-security-modules/spring-security-auth-server/src/main/resources/application-dynamic-scopes.yaml new file mode 100644 index 000000000000..f4039ecfaa1d --- /dev/null +++ b/spring-security-modules/spring-security-auth-server/src/main/resources/application-dynamic-scopes.yaml @@ -0,0 +1,31 @@ +spring: + security: + user: + name: user + password: "{noop}password" + oauth2: + authorizationserver: + client: + client1: + require-proof-key: false + registration: + client-name: "Client 1 - Issuer 1" + client-id: client1 + client-secret: "{noop}secret1" + client-authentication-methods: + - client_secret_basic + redirect-uris: + - http://localhost:9090/login/oauth2/code/issuer1client1 + authorization-grant-types: + - client_credentials + - authorization_code + - refresh_token + scopes: + - openid + - email + web: + error: + include-message: always + include-exception: true + include-stacktrace: always + diff --git a/spring-security-modules/spring-security-auth-server/src/main/resources/templates/consent.html b/spring-security-modules/spring-security-auth-server/src/main/resources/templates/consent.html new file mode 100644 index 000000000000..d3a5ec28288e --- /dev/null +++ b/spring-security-modules/spring-security-auth-server/src/main/resources/templates/consent.html @@ -0,0 +1,42 @@ + + + + + Custom Consent Page + + + + + +

Custom consent page

+ +

The client CLIENT NAME is asking your consent for the following new scopes:

+ +
+ +
+ + +
+ +

Do you want to grant access?

+ + +

+ + +

+
+ + \ No newline at end of file diff --git a/spring-security-modules/spring-security-auth-server/src/main/resources/templates/error.html b/spring-security-modules/spring-security-auth-server/src/main/resources/templates/error.html new file mode 100644 index 000000000000..22ba7de964f1 --- /dev/null +++ b/spring-security-modules/spring-security-auth-server/src/main/resources/templates/error.html @@ -0,0 +1,21 @@ + + + + Error Page + + +

Something went wrong!

+ +
+

Status: 500

+

Error: Internal Server Error

+

Message: Error message here

+
+ + +
+

Stack Trace:

+

+
+ + \ No newline at end of file diff --git a/spring-security-modules/spring-security-auth-server/src/test/java/com/baeldung/auth/server/dynamicscopes/DynamicScopesAuthServerUnitTest.java b/spring-security-modules/spring-security-auth-server/src/test/java/com/baeldung/auth/server/dynamicscopes/DynamicScopesAuthServerUnitTest.java new file mode 100644 index 000000000000..8994d4db322c --- /dev/null +++ b/spring-security-modules/spring-security-auth-server/src/test/java/com/baeldung/auth/server/dynamicscopes/DynamicScopesAuthServerUnitTest.java @@ -0,0 +1,200 @@ +package com.baeldung.auth.server.dynamicscopes; + +import org.jsoup.Jsoup; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.context.ApplicationContext; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.client.JdkClientHttpRequestFactory; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.web.servlet.client.RestTestClient; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.web.util.UriComponentsBuilder; + +import java.net.CookieManager; +import java.net.URI; +import java.net.http.HttpClient; +import java.util.Base64; +import java.util.Map; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.*; +import static org.springframework.test.web.servlet.client.RestTestClient.bindToServer; + +/** + * Integrations tests for the {@link DynamicScopesAuthServerApplication} + */ +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@ActiveProfiles("dynamic-scopes") +public class DynamicScopesAuthServerUnitTest { + + @LocalServerPort + int port; + + RestTestClient restTestClient, noRedirecRestTestClient; + + private static final String ACCEPT_HEADER_VALUE = "text/html"; + + + // Happy path integration test + @Test + void whenAuthorizationRequestWithDynamicScope_thenSuccess() { + + var response = restTestClient.get().uri("/.well-known/openid-configuration").exchange(); + + // sanity check + var result = response.returnResult(Map.class); + var config = result.getResponseBody(); + assertTrue(result.getStatus().is2xxSuccessful()); + assertNotNull(config); + assertTrue(config.containsKey("token_endpoint")); + assertNotNull(config.get("token_endpoint")); + assertTrue(config.containsKey("authorization_endpoint")); + assertNotNull(config.get("authorization_endpoint")); + + var authEndpoint = URI.create(config.get("authorization_endpoint").toString()); + var tokenEndpoint = config.get("token_endpoint").toString(); + + // Build auth request + var txId = UUID.randomUUID().toString(); + var state = UUID.randomUUID().toString(); + var redirectUri = "http://localhost:9090/login/oauth2/code/issuer1client1"; + var authResponse = restTestClient.get() + .uri( b -> b.path(authEndpoint.getPath()) + .queryParam("response_type", "code") + .queryParam("client_id", "client1") + .queryParam("scope", String.join(" ","openid","TX:" + txId)) + .queryParam("redirect_uri", redirectUri) + .queryParam("state", state) + .build()) + .header("Accept", ACCEPT_HEADER_VALUE) + .header("Cache-Control", "no-cache") + .exchange(); + var authResult = authResponse.returnResult(); + + assertEquals(HttpStatus.OK,authResult.getStatus()); + var loginPage = new String(authResult.getResponseBodyContent()); + var doc = Jsoup.parse(loginPage); + + // Extract the login form submit URI and the csrf token + var loginFormCsrfToken = doc.expectForm(".login-form").select("input[name=_csrf]").first().val(); + assertNotNull(loginFormCsrfToken); + + // Extract the URI to submit the credentials + var loginUri = doc.expectForm(".login-form").attr("action"); + + // Submit the credentials + var loginBody = new LinkedMultiValueMap(); + loginBody.add("username", "user"); + loginBody.add("password", "password"); + loginBody.add("_csrf", loginFormCsrfToken); + + var loginResponse = restTestClient.post() + .uri(loginUri) + .header("Accept", ACCEPT_HEADER_VALUE) + .header("Cache-Control", "no-cache") + .contentType(MediaType.APPLICATION_FORM_URLENCODED) + .body(loginBody) + .exchange(); + + var loginResult = loginResponse.returnResult(); + assertEquals(HttpStatus.OK,loginResult.getStatus()); + + // We should be redirected to the consent page with the requested scopes + var consentPage = new String(loginResult.getResponseBodyContent()); + var consentDoc = Jsoup.parse(consentPage); + + // The list of scopes should contain the dynamic scope with the transaction id + var scopes = consentDoc.select("input[name=scope]"); + assertEquals("TX:" + txId,scopes.getFirst().val()); + + // Get the new csrf token + var consentFormCsrfToken = consentDoc.expectForm("form[name=consent_form]").select("input[name=_csrf]").first().val(); + assertNotNull(consentFormCsrfToken); + + // Get the consent state. Notice that this value is not the same as the initial state + var consentState = consentDoc.select("input[name=state]").first().val(); + assertNotNull(consentState); + + // Get the consent form target + var consentUri = consentDoc.expectForm("form[name=consent_form]").attr("action"); + var consentBody = new LinkedMultiValueMap(); + consentBody.add("scope", "TX:" + txId); + consentBody.add("client_id", "client1"); + consentBody.add("state", consentState); + consentBody.add("_csrf", consentFormCsrfToken); + + var consentResponse = noRedirecRestTestClient.post() + .uri(consentUri) + .contentType(MediaType.APPLICATION_FORM_URLENCODED) + .body(consentBody) + .exchange(); + + var consentResult = consentResponse.returnResult(); + assertEquals(HttpStatus.FOUND,consentResult.getStatus()); + var location = consentResult.getResponseHeaders().getLocation(); + assertNotNull(location); + assertTrue(location.getQuery().contains("code=")); + + var locationParams = UriComponentsBuilder.fromUri(location).build().getQueryParams(); + assertNotNull(locationParams.get("code")); + + var tokenRequestBody = new LinkedMultiValueMap(); + tokenRequestBody.add("grant_type", "authorization_code"); + tokenRequestBody.add("client_id", "client1"); + tokenRequestBody.add("redirect_uri", redirectUri); + tokenRequestBody.add("code", locationParams.getFirst("code")); + + // Generate an access token + var tokenResponse = restTestClient.post() + .uri(tokenEndpoint) + .header("Authorization", "Basic " + Base64.getEncoder().encodeToString("client1:secret1".getBytes())) + .body(tokenRequestBody) + .exchange(); + + var tokenResult = tokenResponse.returnResult(Map.class); + assertEquals(HttpStatus.OK, tokenResult.getStatus()); + var body = tokenResult.getResponseBody(); + assertNotNull(body); + assertTrue(body.containsKey("access_token")); + assertTrue(body.containsKey("scope")); + + // The returned scope should include the requested dynamic scope + assertTrue(body.get("scope").toString().contains("TX:" + txId)); + + } + + @BeforeEach + void setupRestClient() { + + CookieManager cookieManager = new CookieManager(); + + var followRedirectsHttpClient = HttpClient.newBuilder() + .followRedirects(HttpClient.Redirect.ALWAYS) + .cookieHandler(cookieManager) + .build(); + + var followRedirectsRequestFactory = new JdkClientHttpRequestFactory(followRedirectsHttpClient); + restTestClient = RestTestClient + .bindToServer(followRedirectsRequestFactory) + .baseUrl("http://localhost:" + port) + .build(); + + var noRedirectsHttpClient = HttpClient.newBuilder() + .followRedirects(HttpClient.Redirect.NEVER) + .cookieHandler(cookieManager) + .build(); + + var noRedirectsRequestFactory = new JdkClientHttpRequestFactory(noRedirectsHttpClient); + noRedirecRestTestClient = RestTestClient + .bindToServer(noRedirectsRequestFactory) + .baseUrl("http://localhost:" + port) + .build(); + + } + +} \ No newline at end of file diff --git a/spring-security-modules/spring-security-auth-server/src/test/java/com/baeldung/auth/server/dynamicscopes/TestOAuthClient.java b/spring-security-modules/spring-security-auth-server/src/test/java/com/baeldung/auth/server/dynamicscopes/TestOAuthClient.java new file mode 100644 index 000000000000..2b1aa6f85192 --- /dev/null +++ b/spring-security-modules/spring-security-auth-server/src/test/java/com/baeldung/auth/server/dynamicscopes/TestOAuthClient.java @@ -0,0 +1,25 @@ +package com.baeldung.auth.server.dynamicscopes; + +// Simple http server that acts as an OAuth client for tests +class TestOAuthClient implements Runnable { + + + TestOAuthClient() { + + } + + @Override + public void run() { + + } + + /** + * Starts the http server and returns the port it is listening on + * @return + */ + static int startServer() { + + + return 0; + } +}