This content originally appeared on DEV Community and was authored by anand jaisy
Spring Authorization Server
The Spring Authorization Server is a framework designed to implement the OAuth 2.1 and OpenID Connect 1.0 specifications, along with other related standards. Built on Spring Security, it offers a secure, lightweight, and customizable foundation for creating Identity Providers compliant with OpenID Connect 1.0 and OAuth2 Authorization Server solutions.
What is Spring Security and how does it work?
short answer
Spring Security is a powerful and highly customizable authentication and access-control framework. It is the de-facto standard for securing Spring-based applications.
At its heart, Spring Security is essentially a collection of servlet filters designed to enhance your web application with robust authentication and authorization features.
Spring Security also meshes well with frameworks like Spring Web MVC or Spring Boot, supporting standards such as OAuth2 and SAML. It automatically generates login and logout interfaces and safeguards your application from common security vulnerabilities like CSRF.
Well, that's not very helpful, is it?
Let's delve into web security to grasp the essentials of its security workflow.
To become a Spring Security expert, you must first grasp these three core concepts:
- Authentication
- Authorization
- Servlet Filters
Note - Don't bypass this section; it lays the groundwork for all Spring Security functionalities.
Authentication
You need to access your bank account online to check your balance or make a transaction. Typically this is done using UserName
and Password
User: "I'm John Doe. My username is: johndoe1985."
Bank's System: "Please verify your identity. What's your password?"
User: "My password is: secureB@nk2023."
Bank's System: "Welcome, John Doe. Here's your account overview."
Authorization
For basic applications, authentication alone might suffice: Once a user logs in, they're granted access to all areas of the application.
However, in most applications, there are permissions or roles in play.
User: "Let me play with that transaction …."
Bank's System: "One second, I need to check your permissions first…..yes Mr. John Doe, you have the right clearance level. Enjoy."
User: "I'll transfer 1M
ha ha ha … Kidding kidding
"
Servlet Filters
Now, let's explore Servlet Filters. How do they relate to authentication and authorization?
Why use Servlet Filters?
Every Spring web application revolves around a single servlet: the trusty DispatcherServlet. Its primary role is to route incoming HTTP requests (such as those from a browser) to the appropriate @Controller or @RestController for handling.
Here’s the deal: the DispatcherServlet itself doesn’t have any built-in security features, and you probably don’t want to handle raw HTTP Basic Auth headers directly in your @Controllers
. Ideally, authentication and authorization should be taken care of before a request even reaches your @Controllers
Fortunately, in the Java web environment, you can achieve this by placing filters before servlets. This means you could consider creating a SecurityFilter and setting it up in your Tomcat (servlet container/application server) to intercept and process every incoming HTTP request before it reaches your servlet.
A SecurityFilter has roughly 4 tasks
- First, the filter needs to extract a username/password from the request. It could be via a Basic Auth HTTP Header, or form fields, or a cookie, etc.
- Then the filter needs to validate that username/password combination against something, like a database.
- The filter needs to check, after successful authentication, that the user is authorized to access the requested URI.
- If the request survives all these checks, then the filter can l et the request go through to your DispatcherServlet, i.e. your @Controllers.
FilterChains
In practice, we'd break a single filter down into several, which you would then link together.
Here's how an incoming HTTP request would travel:
- First, it passes through a LoginMethodFilter...
- Next, it goes through an AuthenticationFilter...
- Then, it moves to an AuthorizationFilter...
- And finally, it reaches your servlet.
This setup is known as a FilterChain.
By using a filter (or a chain of filters), you can effectively manage all authentication and authorization challenges in your application without altering the core implementation of your @RestControllers
or @Controllers
.
Spring’s DefaultSecurityFilterChain
Imagine you’ve configured Spring Security properly and started your web application. You’ll notice a log message that looks like this:
2020-02-25 10:24:27.875 INFO 11116 --- [ main] o.s.s.web.DefaultSecurityFilterChain : Creating filter chain: any request, [org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@46320c9a, org.springframework.security.web.context.SecurityContextPersistenceFilter@4d98e41b, org.springframework.security.web.header.HeaderWriterFilter@52bd9a27, org.springframework.security.web.csrf.CsrfFilter@51c65a43, org.springframework.security.web.authentication.logout.LogoutFilter@124d26ba, org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter@61e86192, org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter@10980560, org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter@32256e68, org.springframework.security.web.authentication.www.BasicAuthenticationFilter@52d0f583, org.springframework.security.web.savedrequest.RequestCacheAwareFilter@5696c927, org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@5f025000, org.springframework.security.web.authentication.AnonymousAuthenticationFilter@5e7abaf7, org.springframework.security.web.session.SessionManagementFilter@681c0ae6, org.springframework.security.web.access.ExceptionTranslationFilter@15639d09, org.springframework.security.web.access.intercept.FilterSecurityInterceptor@4f7be6c8]|
Expanding that single line reveals that Spring Security doesn’t just add one filter—it sets up an entire filter chain with 15 (!) different filters.
When an HTTP request arrives, it passes through each of these 15 filters in sequence before finally reaching your @RestControllers. The order of these filters is crucial, as the request is processed from the top of the chain to the bottom.
Analyzing Spring’s FilterChain
Diving into the details of every filter in the chain would take us too far, but here are explanations for a few key filters. For a deeper understanding of the others, you can explore Spring Security’s source code.
- BasicAuthenticationFilter: Tries to find a Basic Auth HTTP Header on the request and if found, tries to authenticate the user with the header’s username and password.
- UsernamePasswordAuthenticationFilter: Tries to find a username/password request parameter/POST body and if found, tries to authenticate the user with those values.
- DefaultLoginPageGeneratingFilter: Generates a login page for you, if you don’t explicitly disable that feature. THIS filter is why you get a default login page when enabling Spring Security.
- DefaultLogoutPageGeneratingFilter: Generates a logout page for you, if you don’t explicitly disable that feature.
- FilterSecurityInterceptor: Does your authorization.
Joke
Question - Why did the HTTP request break up with the Spring Security filter?
Answer - Because every time it tried to get closer, the filter said, "Hold on! Let me check you out first!" 😄Yeah break ........ Whoa, hold up... that was way too much security talk for one go!
Setup Spring Authorization server
The easiest way to begin using Spring Authorization Server is by creating a Spring Boot-based application. You can use start.spring.io to generate a basic project.
The only dependency required is implementation("org.springframework.boot:spring-boot-starter-oauth2-authorization-server")
We will add two more to do more action
dependencies {
implementation("org.springframework.boot:spring-boot-starter-oauth2-authorization-server")
implementation("org.springframework.boot:spring-boot-starter-webflux")
implementation("org.springframework.boot:spring-boot-starter-validation")
}
How to configure Spring Security
With the latest Spring Security and/or Spring Boot versions, the way to configure Spring Security is by having a class that: Is annotated with @EnableWebSecurity
.
@Configuration
@EnableWebSecurity
public class SecurityConfig {
private static final String[] ALLOW_LIST = {"/oauth2/token", "/userinfo"};
//This is primarily configured to handle OAuth2 and OpenID Connect specific endpoints. It sets up the security for the authorization server, handling token endpoints, client authentication, etc.
@Bean (1)
@Order(1)
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
OAuth2AuthorizationServerConfigurer authorizationServerConfigurer = OAuth2AuthorizationServerConfigurer.authorizationServer();
http
.cors(Customizer.withDefaults())
.authorizeHttpRequests(authz -> authz
.requestMatchers(ALLOW_LIST).permitAll()
.requestMatchers("/**", "/oauth2/jwks/").hasAuthority("SCOPE_keys.write")
.anyRequest()
.authenticated())
.securityMatchers(matchers ->
matchers.requestMatchers(antMatcher("/oauth2/**"), authorizationServerConfigurer.getEndpointsMatcher()))
.with(authorizationServerConfigurer, (authorizationServer) ->
authorizationServer
.oidc(Customizer.withDefaults())) // Enable OpenID Connect 1.0
// Redirect to the login page when not authenticated from the
// authorization endpoint
.exceptionHandling((exceptions) -> exceptions
.defaultAuthenticationEntryPointFor(
new LoginUrlAuthenticationEntryPoint("/login"),
new MediaTypeRequestMatcher(MediaType.TEXT_HTML)
))
// Accept access tokens for User Info and/or Client Registration
.oauth2ResourceServer((oauth2) -> oauth2.jwt(Customizer.withDefaults()));
return http.build();
}
// This configuration is set up for general application security, handling standard web security features like form login for paths not specifically managed by the OAuth2 configuration.
@Bean (2)
@Order(2)
public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http)
throws Exception {
http
.authorizeHttpRequests((authorize) -> authorize
.requestMatchers("/login", "/error", "/main.css")
.permitAll()
.anyRequest()
.authenticated()
)
// Form login handles the redirect to the login page from the
// authorization server filter chain
.formLogin((login) -> login.loginPage("/login"));
return http.build();
}
@Bean (3)
public JWKSource<SecurityContext> jwkSource() {
KeyPair keyPair = generateRsaKey();
RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
RSAKey rsaKey = new RSAKey.Builder(publicKey)
.privateKey(privateKey)
.keyID(UUID.randomUUID().toString())
.build();
JWKSet jwkSet = new JWKSet(rsaKey);
return new ImmutableJWKSet<>(jwkSet);
}
private 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;
}
@Bean (4)
public JwtDecoder jwtDecoder(JWKSource<SecurityContext> jwkSource) {
return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource);
}
@Bean (5)
public AuthorizationServerSettings authorizationServerSettings() {
return AuthorizationServerSettings
.builder()
.build();
}
}
(1) : A Spring Security filter chain for the Protocol Endpoints.
(2) : A Spring Security filter chain for authentication.
(3) : An instance of com.nimbusds.jose.jwk.source.JWKSource
for signing access tokens.
(4) : An instance of JwtDecoder
for decoding signed access tokens.
(5) : An instance of AuthorizationServerSettings
to configure Spring Authorization Server.
Lets configure CORS to allow certain URLs to our application
@Configuration
public class CorsConfig {
@Bean
public UrlBasedCorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.addAllowedOrigin("http://localhost:3000/"); // Change to specific domains in production
configuration.addAllowedMethod("*");
configuration.addAllowedHeader("*");
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
}
CorsConfiguration
This class is used to define the CORS rules. In this case:
- addAllowedOrigin("http://localhost:3000/"): Allows requests from http://localhost:3000. This is useful for local development when your frontend is running on a different port. In production, replace this with your actual domains.
- addAllowedMethod("*"): Allows all HTTP methods (e.g., GET, POST, PUT, DELETE, etc.).
- addAllowedHeader("*"): Allows all HTTP headers in requests.
UrlBasedCorsConfigurationSource
- A class that maps URL patterns (like /**) to specific CORS configurations.
- registerCorsConfiguration("/", configuration): Applies the defined CORS rules (configuration) to all endpoints (/) in the application.
Wow, that’s a lot of configuration! But that’s the magic of the Spring Framework—it handles all the heavy lifting behind the scenes.
It is time to configure the Clients
@Configuration
public class Clients {
@Bean
public RegisteredClientRepository registeredClientRepository() {
RegisteredClient oidcClient = RegisteredClient.withId(UUID.randomUUID().toString())
.clientId("stomble")
.clientAuthenticationMethod(ClientAuthenticationMethod.NONE)
.authorizationGrantTypes(types -> {
types.add(AuthorizationGrantType.AUTHORIZATION_CODE);
types.add(AuthorizationGrantType.REFRESH_TOKEN);
})
.redirectUris(redirectUri -> {
redirectUri.add("http://localhost:3000");
redirectUri.add("https://oauth.pstmn.io/v1/callback");
redirectUri.add("http://localhost:3000/signin-callback");
})
.postLogoutRedirectUri("http://localhost:3000")
.scopes(score -> {
score.add(OidcScopes.OPENID);
score.add(OidcScopes.PROFILE);
score.add(OidcScopes.EMAIL);
})
.clientSettings(ClientSettings.builder()
.requireAuthorizationConsent(false)
.requireProofKey(true)
.build())
.build();
return new InMemoryRegisteredClientRepository(oidcClient);
}
}
Few things we have done above
- clientId: A unique Identifier to allow access
- clientAuthenticationMethod : Defining Authentication method
- redirectUris Allowing only the defined URLs
-
authorizationGrantTypes
authorization_code
UserDetailsService
UserDetailsService is used by DaoAuthenticationProvider
for retrieving a username, a password, and other attributes for authenticating with a username and password. Spring Security provides in-memory
, JDBC
, and caching
implementations of UserDetailsService.
You can define custom authentication by exposing a custom UserDetailsService as a bean.
InMemoryUserDetailsManager
@Configuration
public class UserConfig {
@Bean
public UserDetailsService userDetailsService(PasswordEncoder passwordEncoder) {
UserDetails userDetailFirst = User.builder()
.username("user1")
.password(passwordEncoder.encode("password"))
.roles("USER")
.build();
UserDetails userDetailSecond = User.builder()
.username("user2")
.password(passwordEncoder.encode("password"))
.roles("USER")
.build();
return new InMemoryUserDetailsManager(List.of(userDetailFirst, userDetailSecond));
}
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
Once we launch the application, our OIDC and OAuth2 setup with Spring Authorization Server should function correctly. However, you'll notice we've employed InMemoryUserDetailsManager
, which is fine for demos or prototyping. But for a production environment, it's not advisable because all data vanishes upon application restart.
JdbcUserDetailsManager in Spring Security
JdbcUserDetailsManager is a feature within Spring Security that uses JDBC to handle user credentials and roles by connecting to a relational database. It's ideal when your application can work with the standard schema for user tables that Spring Security expects.
The schema that is available from Spring security org/springframework/security/core/userdetails/jdbc/users.ddl
-- Create users and authorities tables for JdbcUserDetailsManager
CREATE TABLE users (
username VARCHAR(50) NOT NULL PRIMARY KEY,
password VARCHAR(500) NOT NULL,
enabled BOOLEAN NOT NULL
);
CREATE TABLE authorities (
username VARCHAR(50) NOT NULL,
authority VARCHAR(50) NOT NULL,
CONSTRAINT fk_authorities_users FOREIGN KEY (username) REFERENCES users (username)
);
CREATE UNIQUE INDEX ix_auth_username ON authorities (username, authority);
The sole adjustment needed to transition from InMemoryUserDetailsManager
to JdbcUserDetailsManager
@Bean
public UserDetailsService userDetailsService(DataSource dataSource) {
return new JdbcUserDetailsManager(dataSource);
}
This configuration is effective for applications that stick to Spring Security's standard table schema. But, if you need to customize (like using an email for login instead of a username), implementing a custom UserDetailsService offers the necessary adaptability.
Custom UserDetailsService with a Customer Entity
Lets add custom CustomUserDetailsService to the provider. In the AuthenticationProvider set the custom service using setUserDetailsService
@Configuration
public class UserConfig {
private final CustomUserDetailsService userDetailsService;
public UserConfig(CustomUserDetailsService userDetailsService) {
this.userDetailsService = userDetailsService;
}
@Bean
public AuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setUserDetailsService(userDetailsService);
provider.setPasswordEncoder(passwordEncoder());
return provider;
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
Custom service
@Service
public class UserDetailsService implements org.springframework.security.core.userdetails.UserDetailsService {
private final IUserRepository userRepository;
public UserDetailsService(IUserRepository userRepository) {
this.userRepository = userRepository;
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
var user = this.userRepository.findByUsername(username);
if (user.isEmpty())
throw new UsernameNotFoundException("User not found");
return user.get();
}
}
Repository
@Repository
public interface IUserRepository extends CrudRepository<UserProfile, UUID> {
Optional<UserProfile> findByUsername(String username);
}
Entity
@Entity
@Table(name = "userprofile")
public class UserProfile extends AuditTrail implements UserDetails, CredentialsContainer {
private String firstName;
private String lastName;
private String username;
private String email;
private String password;
@OneToMany(mappedBy = "userProfile", fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true)
private Collection<UserRole> userRoles;
@OneToOne
@JoinColumn(name = "tenant_id")
private Tenant tenant;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
// You can write custom logic here to return roles/permissions
return null;
}
@Override
public String getPassword() {
return this.password;
}
@Override
public String getUsername() {
return getEmail();
}
@Override
public boolean isAccountNonExpired() {
return UserDetails.super.isAccountNonExpired();
}
@Override
public boolean isAccountNonLocked() {
return UserDetails.super.isAccountNonLocked();
}
@Override
public boolean isCredentialsNonExpired() {
return UserDetails.super.isCredentialsNonExpired();
}
@Override
public boolean isEnabled() {
return UserDetails.super.isEnabled();
}
@Override
public void eraseCredentials() {
this.password = null;
}
}
In the security filter we have to tell spring security to use this service
.clientAuthentication(clientAuth -> clientAuth.authenticationProvider(authenticationProvider))
@Bean
@Order(1)
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
OAuth2AuthorizationServerConfigurer authorizationServerConfigurer = OAuth2AuthorizationServerConfigurer.authorizationServer();
http
.cors(Customizer.withDefaults())
.authorizeHttpRequests(authz -> authz
.requestMatchers(ALLOW_LIST).permitAll()
.requestMatchers("/**", "/oauth2/jwks/").hasAuthority("SCOPE_keys.write")
.anyRequest()
.authenticated())
.securityMatchers(matchers ->
matchers.requestMatchers(antMatcher("/oauth2/**"), authorizationServerConfigurer.getEndpointsMatcher()))
.with(authorizationServerConfigurer, (authorizationServer) ->
authorizationServer
.oidc(Customizer.withDefaults())
.clientAuthentication(clientAuth -> clientAuth.authenticationProvider(authenticationProvider))) // Enable OpenID Connect 1.0
.exceptionHandling((exceptions) -> exceptions
.defaultAuthenticationEntryPointFor(
new LoginUrlAuthenticationEntryPoint("/login"),
new MediaTypeRequestMatcher(MediaType.TEXT_HTML)
))
.oauth2ResourceServer((oauth2) -> oauth2.jwt(Customizer.withDefaults()));
return http.build();
}
Conclusion
Here, you have two robust choices for handling authentication:
- JdbcUserDetailsManager: A straightforward option if your application aligns with Spring's default schema.
- Custom UserDetailsService: Provides the flexibility to manage special fields and roles.
No matter if you choose JdbcUserDetailsManager or decide to implement a custom UserDetailsService, both will equip your application with a scalable, database-supported authentication system.
This content originally appeared on DEV Community and was authored by anand jaisy
anand jaisy | Sciencx (2025-01-23T01:36:11+00:00) Spring Authorization server + spring security with custom user details service for flexible data-driven authentication. Retrieved from https://www.scien.cx/2025/01/23/spring-authorization-server-spring-security-with-custom-user-details-service-for-flexible-data-driven-authentication-2/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.