Spring Boot Security¶
Overview¶
This guide provides a comprehensive overview of Spring Security in Spring Boot applications. It covers the core concepts, configuration approaches, authentication and authorization mechanisms, and advanced topics including JWT-based authentication, OAuth2, and Keycloak integration. By the end of this guide, you'll understand how Spring Security works internally and be able to implement robust security solutions for your Spring Boot applications.
Prerequisites¶
- Basic knowledge of Java and Spring Boot
- Understanding of web security concepts (authentication, authorization)
- Familiarity with RESTful APIs
- Development environment with Spring Boot set up
Learning Objectives¶
- Understand Spring Security's architecture and core components
- Configure Spring Security in Spring Boot applications
- Implement various authentication mechanisms
- Secure RESTful APIs using Spring Security
- Apply proper authorization controls
- Implement JWT-based authentication
- Integrate with OAuth2 and OpenID Connect
- Configure Keycloak as an identity provider
- Test secured applications
- Apply security best practices
Table of Contents¶
- Spring Security Fundamentals
- Spring Security Architecture
- Authentication Mechanisms
- Authorization and Access Control
- Securing RESTful APIs
- CSRF, CORS, and XSS Protection
- JWT-Based Authentication
- OAuth2 and OpenID Connect
- Keycloak Integration
- Method Security
- Testing Secured Applications
- Security Best Practices
Spring Security Fundamentals¶
Spring Security is a powerful and highly customizable authentication and access control framework. It is the de-facto standard for securing Spring-based applications.
Core Concepts¶
- Authentication: The process of verifying the identity of a user, system, or entity.
- Authorization: The process of determining if an authenticated entity has permission to access a resource or perform an action.
- Principal: Currently authenticated user.
- Granted Authority: Permission granted to the principal.
- Role: A group of permissions/authorities.
Spring Security Features¶
- Comprehensive authentication support
- Protection against common attacks (CSRF, Session Fixation)
- Servlet API integration
- Optional integration with Spring Web MVC
- Support for multiple security contexts and authentication providers
- Extensible and customizable security architecture
Getting Started with Spring Security¶
To add Spring Security to a Spring Boot application, include the following dependency:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
This single dependency brings: - Authentication and authorization support - Default security configuration - Login/logout functionality - CSRF protection - Session management - Security header integration
Default Security Configuration¶
When you add the Spring Security starter dependency without any additional configuration, Spring Boot applies default security settings:
- Secures all HTTP endpoints with basic authentication
- Generates a default user with a random password (printed in the console)
- Enables CSRF protection, XSS protection, and secure HTTP headers
- Creates login/logout endpoints
To override these defaults, you need to provide custom configuration.
Spring Security Architecture¶
Understanding the internal architecture is crucial for effective customization of Spring Security. Let's explore the key components and their interactions.
Spring Security Architecture¶
Spring Security with keycloak Architecture¶
Security Filters Chain¶
Spring Security is primarily filter-based. It adds a chain of filters to the Servlet container's filter chain. These filters are executed in a specific order to implement security features:
- ChannelProcessingFilter: Ensures requests go through required channels (e.g., HTTPS)
- SecurityContextPersistenceFilter: Establishes SecurityContext for requests
- ConcurrentSessionFilter: Updates SessionRegistry and checks for expired sessions
- HeaderWriterFilter: Adds security headers to response
- CsrfFilter: Protects against CSRF attacks
- LogoutFilter: Processes logout requests
- UsernamePasswordAuthenticationFilter: Processes form-based authentication
- BasicAuthenticationFilter: Processes HTTP Basic authentication
- RequestCacheAwareFilter: Handles saved requests after authentication
- SecurityContextHolderAwareRequestFilter: Integrates with Servlet API
- AnonymousAuthenticationFilter: Creates anonymous users when no authentication exists
- SessionManagementFilter: Handles session fixation, session timeout, etc.
- ExceptionTranslationFilter: Catches security exceptions and redirects to appropriate handlers
- FilterSecurityInterceptor: Makes access control decisions based on configuration
This filter chain is dynamically configured based on your security requirements.
SecurityContextHolder¶
The SecurityContextHolder is where Spring Security stores details of the present security context, including:
- Current user's identity
- Authentication details
- Granted authorities
By default, it uses a ThreadLocal strategy to store this information, making it available throughout a single request thread.
// How to access current authentication in code
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication != null && authentication.isAuthenticated()) {
String username = authentication.getName();
Object principal = authentication.getPrincipal();
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
// Use authentication details
}
Authentication Manager¶
The AuthenticationManager is the core interface for authentication in Spring Security. Its primary method is:
Authentication authenticate(Authentication authentication) throws AuthenticationException;
This method: 1. Returns a fully populated Authentication object (including granted authorities) if successful 2. Throws an AuthenticationException if authentication fails 3. Returns null if it cannot decide
The most common implementation is ProviderManager
, which delegates to a chain of AuthenticationProvider instances.
Authentication Providers¶
Authentication providers perform specific types of authentication:
DaoAuthenticationProvider
: Username/password authentication using a UserDetailsServiceJwtAuthenticationProvider
: Authenticates JWT tokensRememberMeAuthenticationProvider
: Handles remember-me authenticationOAuthAuthenticationProvider
: Authenticates OAuth tokensLdapAuthenticationProvider
: LDAP authentication
These can be chained together to support multiple authentication mechanisms.
UserDetailsService¶
The UserDetailsService is a key interface that:
- Loads user details by username
- Returns a UserDetails object with username, password, authorities, and account status flags
public interface UserDetailsService {
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
Custom implementations typically connect to user stores like databases, LDAP, or external identity providers.
Access Decision Manager¶
The AccessDecisionManager makes decisions about whether access is granted to a secured resource. It uses:
- Authentication object (current user)
- Secured object being accessed
- List of security configuration attributes for the object
Spring Security offers several voting-based implementations, like:
- AffirmativeBased
: Grants access if any voter approves
- ConsensusBased
: Takes majority vote
- UnanimousBased
: Requires all voters to approve
Security Context Management¶
Spring Security manages the security context across requests through:
- SecurityContextPersistenceFilter: Loads and saves SecurityContext between requests
- SecurityContextRepository: Interface for storing contexts (default: HttpSessionSecurityContextRepository)
- SecurityContextHolderStrategy: Strategy for storing context (default: ThreadLocalSecurityContextHolderStrategy)
WebSecurityConfigurerAdapter (Legacy, pre-Spring Security 5.7)¶
In versions before 5.7, this adapter class was used to customize web security:
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/public/**").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login")
.permitAll();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth
.inMemoryAuthentication()
.withUser("user")
.password("{noop}password")
.roles("USER");
}
}
Component-Based Security Configuration (Spring Security 5.7+)¶
In modern Spring Security, the recommended approach uses component-based configuration:
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/public/**").permitAll()
.anyRequest().authenticated()
)
.formLogin(form -> form
.loginPage("/login")
.permitAll()
);
return http.build();
}
@Bean
public UserDetailsService userDetailsService() {
UserDetails user = User.withDefaultPasswordEncoder()
.username("user")
.password("password")
.roles("USER")
.build();
return new InMemoryUserDetailsManager(user);
}
}
RequestMatcher¶
Spring Security uses RequestMatcher to determine which security rules apply to which requests:
AntPathRequestMatcher
: Ant-style path patternsMvcRequestMatcher
: Spring MVC pattern matchingRegexRequestMatcher
: Regular expression-based matching
Filter Security Interceptor¶
FilterSecurityInterceptor makes the final access control decision. It:
- Uses SecurityMetadataSource to get attributes for a request
- Retrieves the Authentication from SecurityContext
- Delegates to AccessDecisionManager to make the authorization decision
Authentication Mechanisms¶
Spring Security supports multiple authentication mechanisms that can be configured based on your application's requirements.
Form-Based Authentication¶
The most common web authentication method using HTML forms:
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authorize -> authorize
.anyRequest().authenticated()
)
.formLogin(form -> form
.loginPage("/login") // Custom login page URL
.loginProcessingUrl("/perform-login") // URL to submit the credentials
.defaultSuccessUrl("/home") // Redirect after successful login
.failureUrl("/login?error=true") // Redirect after failed login
.usernameParameter("username") // Username parameter name in form
.passwordParameter("password") // Password parameter name in form
.permitAll() // Allow access to login page
);
return http.build();
}
HTTP Basic Authentication¶
Simple authentication scheme built into the HTTP protocol:
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authorize -> authorize
.anyRequest().authenticated()
)
.httpBasic(Customizer.withDefaults());
return http.build();
}
Remember-Me Authentication¶
Allows returning users to be remembered:
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authorize -> authorize
.anyRequest().authenticated()
)
.formLogin(Customizer.withDefaults())
.rememberMe(remember -> remember
.key("uniqueAndSecretKey")
.tokenValiditySeconds(86400) // 1 day
);
return http.build();
}
Custom Authentication Providers¶
For custom authentication logic, implement the AuthenticationProvider interface:
@Component
public class CustomAuthenticationProvider implements AuthenticationProvider {
@Override
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
String username = authentication.getName();
String password = authentication.getCredentials().toString();
// Custom authentication logic
if (shouldAuthenticateUser(username, password)) {
List<GrantedAuthority> authorities = new ArrayList<>();
authorities.add(new SimpleGrantedAuthority("ROLE_USER"));
return new UsernamePasswordAuthenticationToken(username, password, authorities);
} else {
throw new BadCredentialsException("Authentication failed");
}
}
@Override
public boolean supports(Class<?> authentication) {
return authentication.equals(UsernamePasswordAuthenticationToken.class);
}
private boolean shouldAuthenticateUser(String username, String password) {
// Implement custom validation logic
return true; // Placeholder
}
}
Then register it:
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authorize -> authorize
.anyRequest().authenticated()
)
.formLogin(Customizer.withDefaults());
return http.build();
}
@Bean
public AuthenticationManager authenticationManager(
AuthenticationConfiguration authConfig) throws Exception {
return authConfig.getAuthenticationManager();
}
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth,
CustomAuthenticationProvider customAuthProvider) {
auth.authenticationProvider(customAuthProvider);
}
LDAP Authentication¶
For organizations using LDAP directories:
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authorize -> authorize
.anyRequest().authenticated()
)
.formLogin(Customizer.withDefaults());
return http.build();
}
@Bean
public EmbeddedLdapServerContextSourceFactoryBean contextSourceFactoryBean() {
EmbeddedLdapServerContextSourceFactoryBean contextSourceFactoryBean =
EmbeddedLdapServerContextSourceFactoryBean.fromEmbeddedLdapServer();
contextSourceFactoryBean.setPort(0);
return contextSourceFactoryBean;
}
@Bean
public LdapAuthenticationProvider ldapAuthenticationProvider(
BaseLdapPathContextSource contextSource) {
BindAuthenticator authenticator = new BindAuthenticator(contextSource);
authenticator.setUserDnPatterns(new String[] { "uid={0},ou=people" });
LdapAuthoritiesPopulator authoritiesPopulator = new DefaultLdapAuthoritiesPopulator(
contextSource, "ou=groups");
authoritiesPopulator.setGroupRoleAttribute("cn");
return new LdapAuthenticationProvider(authenticator, authoritiesPopulator);
}
Database Authentication¶
Using a database to store user credentials:
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authorize -> authorize
.anyRequest().authenticated()
)
.formLogin(Customizer.withDefaults());
return http.build();
}
@Bean
public UserDetailsService userDetailsService(DataSource dataSource) {
JdbcUserDetailsManager userDetailsManager = new JdbcUserDetailsManager(dataSource);
// Optional: customize queries
userDetailsManager.setUsersByUsernameQuery(
"SELECT username, password, enabled FROM users WHERE username = ?");
userDetailsManager.setAuthoritiesByUsernameQuery(
"SELECT username, authority FROM authorities WHERE username = ?");
return userDetailsManager;
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
## JWT-Based Authentication
JSON Web Tokens (JWT) provide a stateless way to authenticate users, which is especially useful for APIs and microservice architectures.
### Understanding JWT Structure
A JWT consists of three parts separated by dots:
1. **Header**: Identifies the algorithm used for signing
2. **Payload**: Contains claims about the entity (user) and metadata
3. **Signature**: Ensures the token hasn't been altered
Example JWT:
### JWT Authentication Flow
1. User logs in with credentials
2. Server validates credentials and generates a JWT
3. Server returns the JWT to the client
4. Client stores the JWT (usually in localStorage or a cookie)
5. Client sends the JWT in the Authorization header for subsequent requests
6. Server validates the JWT signature and extracts user information
7. Server grants access based on the user's authorities
### Implementing JWT Authentication in Spring Boot
#### Dependencies
```xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
JWT Utility Class¶
@Component
public class JwtTokenUtil {
@Value("${jwt.secret}")
private String secret;
@Value("${jwt.expiration}")
private long expiration;
private Key key;
@PostConstruct
public void init() {
this.key = Keys.hmacShaKeyFor(Decoders.BASE64.decode(secret));
}
public String generateToken(UserDetails userDetails) {
Map<String, Object> claims = new HashMap<>();
Collection<? extends GrantedAuthority> authorities = userDetails.getAuthorities();
if (!authorities.isEmpty()) {
claims.put("authorities", authorities.stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.toList()));
}
return Jwts.builder()
.setClaims(claims)
.setSubject(userDetails.getUsername())
.setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + expiration * 1000))
.signWith(key, SignatureAlgorithm.HS512)
.compact();
}
public String getUsernameFromToken(String token) {
return Jwts.parserBuilder()
.setSigningKey(key)
.build()
.parseClaimsJws(token)
.getBody()
.getSubject();
}
public boolean validateToken(String token, UserDetails userDetails) {
final String username = getUsernameFromToken(token);
return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
}
private boolean isTokenExpired(String token) {
final Date expiration = getExpirationDateFromToken(token);
return expiration.before(new Date());
}
private Date getExpirationDateFromToken(String token) {
return Jwts.parserBuilder()
.setSigningKey(key)
.build()
.parseClaimsJws(token)
.getBody()
.getExpiration();
}
public List<SimpleGrantedAuthority> getAuthoritiesFromToken(String token) {
Claims claims = Jwts.parserBuilder()
.setSigningKey(key)
.build()
.parseClaimsJws(token)
.getBody();
List<String> authorities = claims.get("authorities", List.class);
if (authorities == null) {
return Collections.emptyList();
}
return authorities.stream()
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
}
}
JWT Request Filter¶
@Component
public class JwtRequestFilter extends OncePerRequestFilter {
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private JwtTokenUtil jwtTokenUtil;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain chain) throws ServletException, IOException {
final String requestTokenHeader = request.getHeader("Authorization");
String username = null;
String jwtToken = null;
// JWT Token is in the form "Bearer token". Remove Bearer word and get only the Token
if (requestTokenHeader != null && requestTokenHeader.startsWith("Bearer ")) {
jwtToken = requestTokenHeader.substring(7);
try {
username = jwtTokenUtil.getUsernameFromToken(jwtToken);
} catch (IllegalArgumentException e) {
logger.error("Unable to get JWT Token");
} catch (ExpiredJwtException e) {
logger.error("JWT Token has expired");
}
} else {
logger.warn("JWT Token does not begin with Bearer String");
}
// Once we get the token validate it.
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
// If token is valid configure Spring Security to manually set authentication
if (jwtTokenUtil.validateToken(jwtToken, userDetails)) {
List<SimpleGrantedAuthority> authorities = jwtTokenUtil.getAuthoritiesFromToken(jwtToken);
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(
userDetails, null, authorities);
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
// Set authentication in context
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
chain.doFilter(request, response);
}
}
Authentication Controller¶
@RestController
@RequestMapping("/api/auth")
public class AuthController {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private JwtTokenUtil jwtTokenUtil;
@PostMapping("/login")
public ResponseEntity<?> createAuthenticationToken(@RequestBody LoginRequest authenticationRequest) {
try {
authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
authenticationRequest.getUsername(),
authenticationRequest.getPassword())
);
} catch (BadCredentialsException e) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
.body(new MessageResponse("Invalid credentials"));
}
final UserDetails userDetails = userDetailsService
.loadUserByUsername(authenticationRequest.getUsername());
final String token = jwtTokenUtil.generateToken(userDetails);
return ResponseEntity.ok(new JwtResponse(token));
}
// Login request class
public static class LoginRequest {
private String username;
private String password;
// Getters and setters
}
// JWT response class
public static class JwtResponse {
private String token;
public JwtResponse(String token) {
this.token = token;
}
// Getter
}
// Message response class
public static class MessageResponse {
private String message;
public MessageResponse(String message) {
this.message = message;
}
// Getter
}
}
Security Configuration¶
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private JwtRequestFilter jwtRequestFilter;
@Autowired
private JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/api/auth/**").permitAll()
.anyRequest().authenticated()
)
.exceptionHandling(exception -> exception
.authenticationEntryPoint(jwtAuthenticationEntryPoint)
)
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
);
// Add JWT filter
http.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public AuthenticationManager authenticationManager(
AuthenticationConfiguration authConfig) throws Exception {
return authConfig.getAuthenticationManager();
}
}
@Component
class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized");
}
}
JWT Security Best Practices¶
- Use HTTPS: Always transmit JWTs over HTTPS to prevent token theft
- Set Proper Expiration: Short-lived tokens reduce risk if stolen
- Secure Token Storage: Don't store in localStorage (vulnerable to XSS); consider HttpOnly cookies
- Include Only Necessary Claims: Minimize sensitive data in tokens
- Use Strong Secret Keys: Long, random keys for HMAC algorithms
- Consider Using Asymmetric Algorithms: RSA or ECDSA for better security
- Implement Token Revocation: Blacklist or use refresh token pattern
- Validate All Inputs: Especially in token payloads
- Add Fingerprint Claims: Include IP or device info to prevent token reuse
- Monitor and Audit: Log authentication attempts and token usage
Keycloak Integration¶
Keycloak is an open-source Identity and Access Management solution that provides features like Single Sign-On (SSO), Identity Brokering, and Social Login.
Keycloak Architecture¶
Keycloak functions as an identity provider that manages: - User registration, authentication, and authorization - Client application registration - Identity brokering with external providers - User federation with existing LDAP or Active Directory servers - Social login
Setting Up Keycloak for Spring Boot¶
Add Dependencies¶
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
Configure Keycloak Properties¶
# application.properties
spring.security.oauth2.resourceserver.jwt.issuer-uri=http://localhost:8080/realms/your-realm
spring.security.oauth2.resourceserver.jwt.jwk-set-uri=http://localhost:8080/realms/your-realm/protocol/openid-connect/certs
Configure Security¶
@Configuration
@EnableWebSecurity
public class KeycloakSecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/api/public/**").permitAll()
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.requestMatchers("/api/user/**").hasRole("USER")
.anyRequest().authenticated()
)
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(jwt -> jwt.jwtAuthenticationConverter(jwtAuthenticationConverter()))
);
return http.build();
}
private Converter<Jwt, AbstractAuthenticationToken> jwtAuthenticationConverter() {
JwtAuthenticationConverter jwtConverter = new JwtAuthenticationConverter();
jwtConverter.setJwtGrantedAuthoritiesConverter(new KeycloakRoleConverter());
return jwtConverter;
}
public class KeycloakRoleConverter implements Converter<Jwt, Collection<GrantedAuthority>> {
@Override
public Collection<GrantedAuthority> convert(Jwt jwt) {
final Map<String, Object> realmAccess =
(Map<String, Object>) jwt.getClaims().get("realm_access");
if (realmAccess == null) {
return new ArrayList<>();
}
@SuppressWarnings("unchecked")
List<String> roles = (List<String>) realmAccess.get("roles");
return roles.stream()
.map(roleName -> "ROLE_" + roleName.toUpperCase())
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
}
}
}
JWT with Keycloak¶
Keycloak issues JWTs that Spring Security can validate. The key difference from custom JWT implementation is that Keycloak manages:
- Token issuance
- User management
- Token validation
- Role mappings
Keycloak as an OAuth2 Authorization Server¶
Keycloak can act as an OAuth2 Authorization Server, supporting these grant types:
- Authorization Code: For web applications
- Implicit: For SPA applications (deprecated in favor of PKCE)
- Resource Owner Password Credentials: For trusted applications
- Client Credentials: For service-to-service communication
- Refresh Token: For obtaining new access tokens
Client Registration in Keycloak¶
To use Keycloak, you need to register your application as a client:
- Create a new client in Keycloak admin console
- Set the client protocol to
openid-connect
- Configure Valid Redirect URIs
- Choose the appropriate Access Type (public, confidential, or bearer-only)
- Configure client scopes
- Set up client roles if needed
Full OAuth2 Login with Keycloak¶
For a complete login flow where users are redirected to Keycloak:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
# application.properties
spring.security.oauth2.client.registration.keycloak.client-id=your-client-id
spring.security.oauth2.client.registration.keycloak.client-secret=your-client-secret
spring.security.oauth2.client.registration.keycloak.scope=openid,profile,email,roles
spring.security.oauth2.client.registration.keycloak.authorization-grant-type=authorization_code
spring.security.oauth2.client.registration.keycloak.redirect-uri={baseUrl}/login/oauth2/code/{registrationId}
spring.security.oauth2.client.provider.keycloak.issuer-uri=http://localhost:8080/realms/your-realm
spring.security.oauth2.client.provider.keycloak.user-name-attribute=preferred_username
@Configuration
@EnableWebSecurity
public class OAuth2LoginSecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/", "/error", "/webjars/**").permitAll()
.requestMatchers("/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.oauth2Login(oauth2 -> oauth2
.userInfoEndpoint(userInfo -> userInfo
.userAuthoritiesMapper(this.userAuthoritiesMapper())
)
);
return http.build();
}
private GrantedAuthoritiesMapper userAuthoritiesMapper() {
return (authorities) -> {
Set<GrantedAuthority> mappedAuthorities = new HashSet<>();
authorities.forEach(authority -> {
if (authority instanceof OidcUserAuthority) {
OidcUserAuthority oidcUserAuthority = (OidcUserAuthority) authority;
OidcIdToken idToken = oidcUserAuthority.getIdToken();
Map<String, Object> claims = idToken.getClaims();
Map<String, Object> realmAccess = (Map<String, Object>) claims.get("realm_access");
if (realmAccess != null) {
@SuppressWarnings("unchecked")
List<String> roles = (List<String>) realmAccess.get("roles");
roles.forEach(role ->
mappedAuthorities.add(new SimpleGrantedAuthority("ROLE_" + role.toUpperCase()))
);
}
}
// Add the original authority
mappedAuthorities.add(authority);
});
return mappedAuthorities;
};
}
}
Obtaining Tokens Programmatically¶
For service-to-service authentication or testing:
@Service
public class KeycloakService {
@Value("${keycloak.auth-server-url}")
private String authServerUrl;
@Value("${keycloak.realm}")
private String realm;
@Value("${keycloak.resource}")
private String clientId;
@Value("${keycloak.credentials.secret}")
private String clientSecret;
public String getToken(String username, String password) {
String tokenUrl = authServerUrl + "/realms/" + realm + "/protocol/openid-connect/token";
MultiValueMap<String, String> map = new LinkedMultiValueMap<>();
map.add("grant_type", "password");
map.add("client_id", clientId);
map.add("client_secret", clientSecret);
map.add("username", username);
map.add("password", password);
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<>(map, headers);
RestTemplate restTemplate = new RestTemplate();
ResponseEntity<Map> response = restTemplate.postForEntity(
tokenUrl, request, Map.class);
if (response.getStatusCode() == HttpStatus.OK) {
return (String) response.getBody().get("access_token");
}
throw new RuntimeException("Could not get token");
}
public String getClientToken() {
String tokenUrl = authServerUrl + "/realms/" + realm + "/protocol/openid-connect/token";
MultiValueMap<String, String> map = new LinkedMultiValueMap<>();
map.add("grant_type", "client_credentials");
map.add("client_id", clientId);
map.add("client_secret", clientSecret);
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<>(map, headers);
RestTemplate restTemplate = new RestTemplate();
ResponseEntity<Map> response = restTemplate.postForEntity(
tokenUrl, request, Map.class);
if (response.getStatusCode() == HttpStatus.OK) {
return (String) response.getBody().get("access_token");
}
throw new RuntimeException("Could not get token");
}
}
User Management with Keycloak¶
For managing users through the Keycloak Admin REST API:
@Service
public class KeycloakAdminService {
@Value("${keycloak.auth-server-url}")
private String authServerUrl;
@Value("${keycloak.realm}")
private String realm;
@Autowired
private KeycloakService keycloakService;
public void createUser(String username, String email, String firstName, String lastName, String password) {
String adminToken = keycloakService.getClientToken();
String userUrl = authServerUrl + "/admin/realms/" + realm + "/users";
Map<String, Object> user = new HashMap<>();
user.put("username", username);
user.put("email", email);
user.put("firstName", firstName);
user.put("lastName", lastName);
user.put("enabled", true);
Map<String, Object> credentials = new HashMap<>();
credentials.put("type", "password");
credentials.put("value", password);
credentials.put("temporary", false);
user.put("credentials", List.of(credentials));
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.setBearerAuth(adminToken);
HttpEntity<Map<String, Object>> request = new HttpEntity<>(user, headers);
RestTemplate restTemplate = new RestTemplate();
ResponseEntity<Void> response = restTemplate.exchange(
userUrl, HttpMethod.POST, request, Void.class);
if (response.getStatusCode() != HttpStatus.CREATED) {
throw new RuntimeException("Could not create user: " + response.getStatusCode());
}
}
// More user management methods can be added here
}
Keycloak Best Practices¶
- Use HTTPS: Secure all communications with Keycloak
- Proper Client Configuration: Use confidential clients with client secrets for backend applications
- Implement Proper Scopes: Only request necessary scopes
- Use Authorization Code Flow with PKCE: For web and mobile applications
- Keep Keycloak Updated: Regularly apply security updates
- Configure Session Timeouts: Set appropriate token lifetimes
- Use Proper SSL Certificates: Avoid certificate validation issues
- Implement User Consent: For sensitive operations
- Enable Brute Force Protection: Protect against login attacks
- Audit Regularly: Review authentication logs periodically