Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
5b4abb6
Merge remote-tracking branch 'origin/master'
psevestre Oct 28, 2025
68e7399
[BAEL-9510] WIP
psevestre Nov 4, 2025
619960f
Merge remote-tracking branch 'origin/master'
psevestre Nov 4, 2025
56c5761
[BAEL-9510] WIP
psevestre Nov 6, 2025
d1a7236
Merge remote-tracking branch 'origin/master'
psevestre Nov 6, 2025
a1249ff
WIP
psevestre Nov 11, 2025
e530173
[BAEL-9515] WIP - LiveTest
psevestre Nov 12, 2025
d081722
Merge remote-tracking branch 'origin/master'
psevestre Nov 12, 2025
e5242f4
[BAEL-9515] Integration Test for Happy Path
psevestre Nov 17, 2025
ec32c43
[BAEL-9515] Integration Test for Happy Path
psevestre Nov 17, 2025
22f39d1
Merge remote-tracking branch 'origin/master'
psevestre Nov 17, 2025
9efb738
Merge remote-tracking branch 'origin/master'
psevestre Nov 18, 2025
66c333d
Merge remote-tracking branch 'origin/master'
psevestre Nov 18, 2025
adb23c3
[BAEL-9510] Code cleanup
psevestre Nov 18, 2025
86bfdba
[BAEl-9510] WIP: Code cleanup
psevestre Nov 19, 2025
14e6608
Merge remote-tracking branch 'origin/master'
psevestre Nov 24, 2025
e2347e6
[BAEL-9510] code cleanup and test improvements
psevestre Nov 24, 2025
91f85d2
Merge remote-tracking branch 'origin/master'
psevestre Jan 7, 2026
f4b98cc
Merge remote-tracking branch 'origin/master'
psevestre Jan 15, 2026
b7581dd
[BAEL-8408] WIP
psevestre Jan 16, 2026
2099771
Merge remote-tracking branch 'origin/master'
psevestre Jan 16, 2026
bdef830
[BAEL-8408] Multitenant Spring Auth Server
psevestre Jan 18, 2026
062090b
Merge remote-tracking branch 'origin/master'
psevestre Jan 18, 2026
a90fe06
[BAEL-8408] Fix SB4 Tests
psevestre Jan 18, 2026
1fbcc0f
[BAEL-8408] Remove log files from commit
psevestre Jan 18, 2026
8a32ca3
[BAEL-8408] Code cleanup
psevestre Jan 22, 2026
b5ac50d
Merge remote-tracking branch 'origin/master'
psevestre Jan 22, 2026
f0042cb
[BAEL-8408] Fix dependencies
psevestre Jan 22, 2026
4784ded
[BAEL-8408] throw exception for unknown issuer
psevestre Jan 23, 2026
563e079
Merge remote-tracking branch 'origin/master'
psevestre Jan 23, 2026
769786e
Merge remote-tracking branch 'origin/master'
psevestre Apr 4, 2026
1acf4ce
[BAEL-9649] WIP
psevestre May 12, 2026
d41f950
Merge remote-tracking branch 'origin/master'
psevestre May 12, 2026
a6a6bcc
WIP: Initial project structure
psevestre May 12, 2026
e063c3b
[BAEL-9649] WIP
psevestre May 22, 2026
f1972f1
Merge remote-tracking branch 'origin/master'
psevestre May 22, 2026
93e7a1b
[BAEL-9649] Dynamic scopes
psevestre May 28, 2026
8de30f9
Merge remote-tracking branch 'origin/master'
psevestre May 28, 2026
f85b97f
[BAEL-9649] UnitTest
psevestre May 28, 2026
79add9d
Merge remote-tracking branch 'origin/master'
psevestre May 29, 2026
d86defa
Code cleanup
psevestre May 31, 2026
d0ed23f
Merge remote-tracking branch 'origin/master'
psevestre May 31, 2026
82af2a4
[BAEL-9649] Code cleanup
psevestre Jun 1, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 22 additions & 1 deletion spring-security-modules/spring-security-auth-server/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,15 @@
<artifactId>spring-security-auth-server</artifactId>
<properties>
<java.version>21</java.version>
<spring-boot.version>4.0.1</spring-boot.version>
<spring-boot.version>4.0.5</spring-boot.version>
<logback.version>1.5.22</logback.version>
<junit-jupiter.version>6.0.1</junit-jupiter.version>
<mockito.version>5.20.0</mockito.version>
<hamcrest.version>3.0</hamcrest.version>
<assertj.version>3.27.6</assertj.version>
<org.slf4j.version>2.0.17</org.slf4j.version>
<junit-platform.version>6.0.1</junit-platform.version>
<jsoup.version>1.22.2</jsoup.version>
</properties>

<dependencies>
Expand All @@ -29,6 +30,13 @@
<artifactId>spring-boot-starter-security-oauth2-authorization-server</artifactId>
<version>${spring-boot.version}</version>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
<version>${spring-boot.version}</version>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security-oauth2-authorization-server-test</artifactId>
Expand All @@ -40,6 +48,12 @@
<version>${junit-platform.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jsoup</groupId>
<artifactId>jsoup</artifactId>
<version>${jsoup.version}</version>
<scope>test</scope>
</dependency>
</dependencies>

<build>
Expand All @@ -59,6 +73,13 @@
</annotationProcessorPaths>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<skip>true</skip>
</configuration>
</plugin>
</plugins>
</pluginManagement>
</build>
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.baeldung.auth.server.dynamicscopes.components;

import java.util.Set;

public interface DynamicScopeService {

boolean validate(String clientId, Set<String> scopes);

boolean isConsentNeeded(String clientId, Set<String> requestedScopes);
}
Original file line number Diff line number Diff line change
@@ -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<String> 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<String> requestedScopes) {
log.debug("isConsentNeeded: clientId={}, requestedStopes={}",clientId, requestedScopes);
return true;
}
}
Original file line number Diff line number Diff line change
@@ -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<OAuth2AuthorizationCodeRequestAuthenticationContext> 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<OAuth2AuthorizationCodeRequestAuthenticationContext> 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();
}



}
Original file line number Diff line number Diff line change
@@ -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<String> 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";

}
}
Original file line number Diff line number Diff line change
@@ -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

Loading