Keycloak Java Adapter
Library
Keycloak Java Adapter
Overview
Keycloak Java Adapter is an authentication and authorization library for Java applications, providing integration with Keycloak through OpenID Connect and SAML protocols.
Details
Keycloak Java Adapter enables easy implementation of Keycloak authentication in Java applications (Spring Boot, Jakarta EE, Servlet, etc.). However, an important change is that Java Adapters are now deprecated, and migration to standard OpenID Connect support is recommended. Major frameworks like Spring Security and Jakarta EE already have built-in OpenID Connect support, making Keycloak-specific adapters unnecessary. Supporting Java 8, 11, and 17, it provides authentication flows, token management, user session management, and role-based access control. It supports various integration patterns including Spring Boot Starter, Servlet Filter, and JAX-RS integration. The Keycloak-quickstarts repository provides abundant implementation examples for learning best practices in modern Java application authentication.
Pros and Cons
Pros
- Standards Compliance: Implementation compliant with OpenID Connect/SAML standards
- Framework Integration: Natural integration with Spring, Jakarta EE, etc.
- Multiple Auth Flows: Support for Authorization Code, Implicit, Hybrid Flow
- Role-Based Authorization: Fine-grained permission control and authorization
- Enterprise Ready: Proven track record in large enterprise systems
- Rich Examples: Detailed implementation samples in quickstarts
Cons
- Deprecated: Java Adapter is already EOL (End of Life)
- Migration Recommended: Need to migrate to framework-standard OIDC implementations
- Complexity: High functionality comes with initial setup complexity
- Learning Curve: Deep understanding of OpenID Connect, OAuth 2.0 required
- Java Only: Cannot be used with non-Java languages
Main Links
- GitHub - Keycloak
- Keycloak Official Site
- Keycloak Quickstarts
- Keycloak Documentation
- OpenID Connect
- Spring Security OAuth2
Code Examples
Spring Boot + OAuth2 Keycloak Integration
// pom.xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
<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>
// application.yml
spring:
security:
oauth2:
client:
registration:
keycloak:
client-id: my-app
client-secret: ${KEYCLOAK_CLIENT_SECRET}
scope: openid,profile,email
authorization-grant-type: authorization_code
redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}"
provider:
keycloak:
authorization-uri: http://localhost:8080/realms/myrealm/protocol/openid-connect/auth
token-uri: http://localhost:8080/realms/myrealm/protocol/openid-connect/token
user-info-uri: http://localhost:8080/realms/myrealm/protocol/openid-connect/userinfo
jwk-set-uri: http://localhost:8080/realms/myrealm/protocol/openid-connect/certs
user-name-attribute: preferred_username
resourceserver:
jwt:
jwk-set-uri: http://localhost:8080/realms/myrealm/protocol/openid-connect/certs
// Security configuration
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authz -> authz
.requestMatchers("/public/**").permitAll()
.requestMatchers("/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.oauth2Login(oauth2 -> oauth2
.loginPage("/login")
.defaultSuccessUrl("/dashboard")
)
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(jwt -> jwt
.jwtAuthenticationConverter(jwtAuthenticationConverter())
)
)
.logout(logout -> logout
.logoutSuccessUrl("http://localhost:8080/realms/myrealm/protocol/openid-connect/logout")
);
return http.build();
}
@Bean
public JwtAuthenticationConverter jwtAuthenticationConverter() {
JwtGrantedAuthoritiesConverter authoritiesConverter = new JwtGrantedAuthoritiesConverter();
authoritiesConverter.setAuthorityPrefix("ROLE_");
authoritiesConverter.setAuthoritiesClaimName("realm_access.roles");
JwtAuthenticationConverter converter = new JwtAuthenticationConverter();
converter.setJwtGrantedAuthoritiesConverter(authoritiesConverter);
return converter;
}
}
JAX-RS + Keycloak Integration
// Keycloak JAX-RS integration configuration
@ApplicationPath("/api")
public class RestApplication extends Application {
@Override
public Set<Class<?>> getClasses() {
Set<Class<?>> classes = new HashSet<>();
classes.add(UserResource.class);
classes.add(AdminResource.class);
classes.add(SecurityExceptionMapper.class);
return classes;
}
}
// User resource class
@Path("/users")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class UserResource {
@Context
private SecurityContext securityContext;
@GET
@Path("/profile")
@RolesAllowed("user")
public Response getUserProfile() {
String username = securityContext.getUserPrincipal().getName();
// Get user information from Keycloak token
if (securityContext.getUserPrincipal() instanceof KeycloakPrincipal) {
KeycloakPrincipal<?> principal = (KeycloakPrincipal<?>) securityContext.getUserPrincipal();
AccessToken token = principal.getKeycloakSecurityContext().getToken();
UserProfile profile = UserProfile.builder()
.username(token.getPreferredUsername())
.email(token.getEmail())
.firstName(token.getGivenName())
.lastName(token.getFamilyName())
.roles(token.getRealmAccess().getRoles())
.build();
return Response.ok(profile).build();
}
return Response.status(Response.Status.UNAUTHORIZED).build();
}
@GET
@Path("/admin")
@RolesAllowed("admin")
public Response getAdminInfo() {
return Response.ok("Admin access granted").build();
}
}
// User profile model
public class UserProfile {
private String username;
private String email;
private String firstName;
private String lastName;
private Set<String> roles;
// Constructor, getters, setters, builder
public static Builder builder() {
return new Builder();
}
public static class Builder {
private String username;
private String email;
private String firstName;
private String lastName;
private Set<String> roles;
public Builder username(String username) {
this.username = username;
return this;
}
public Builder email(String email) {
this.email = email;
return this;
}
public Builder firstName(String firstName) {
this.firstName = firstName;
return this;
}
public Builder lastName(String lastName) {
this.lastName = lastName;
return this;
}
public Builder roles(Set<String> roles) {
this.roles = roles;
return this;
}
public UserProfile build() {
UserProfile profile = new UserProfile();
profile.username = this.username;
profile.email = this.email;
profile.firstName = this.firstName;
profile.lastName = this.lastName;
profile.roles = this.roles;
return profile;
}
}
}
JWT Validation and Custom Authentication
// JWT validation service
@Service
public class KeycloakJwtService {
private final JwtDecoder jwtDecoder;
private final Logger logger = LoggerFactory.getLogger(KeycloakJwtService.class);
public KeycloakJwtService(@Value("${spring.security.oauth2.resourceserver.jwt.jwk-set-uri}") String jwkSetUri) {
this.jwtDecoder = NimbusJwtDecoder.withJwkSetUri(jwkSetUri).build();
}
public Optional<UserInfo> validateAndExtractUserInfo(String token) {
try {
Jwt jwt = jwtDecoder.decode(token);
return Optional.of(extractUserInfo(jwt));
} catch (JwtException e) {
logger.warn("Invalid JWT token: {}", e.getMessage());
return Optional.empty();
}
}
private UserInfo extractUserInfo(Jwt jwt) {
return UserInfo.builder()
.subject(jwt.getSubject())
.username(jwt.getClaimAsString("preferred_username"))
.email(jwt.getClaimAsString("email"))
.firstName(jwt.getClaimAsString("given_name"))
.lastName(jwt.getClaimAsString("family_name"))
.roles(extractRoles(jwt))
.issuedAt(jwt.getIssuedAt())
.expiresAt(jwt.getExpiresAt())
.build();
}
@SuppressWarnings("unchecked")
private Set<String> extractRoles(Jwt jwt) {
Map<String, Object> realmAccess = jwt.getClaimAsMap("realm_access");
if (realmAccess != null && realmAccess.containsKey("roles")) {
List<String> roles = (List<String>) realmAccess.get("roles");
return new HashSet<>(roles);
}
return Collections.emptySet();
}
}
// User information model
public class UserInfo {
private String subject;
private String username;
private String email;
private String firstName;
private String lastName;
private Set<String> roles;
private Instant issuedAt;
private Instant expiresAt;
public boolean hasRole(String role) {
return roles.contains(role);
}
public boolean isExpired() {
return expiresAt != null && Instant.now().isAfter(expiresAt);
}
// Builder pattern implementation
public static Builder builder() {
return new Builder();
}
public static class Builder {
// Builder implementation omitted
}
}
REST API Authentication Filter
// JWT authentication filter
@Component
public class JwtAuthenticationFilter implements Filter {
private final KeycloakJwtService jwtService;
private final Logger logger = LoggerFactory.getLogger(JwtAuthenticationFilter.class);
public JwtAuthenticationFilter(KeycloakJwtService jwtService) {
this.jwtService = jwtService;
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
// Skip public paths
if (isPublicPath(httpRequest.getRequestURI())) {
chain.doFilter(request, response);
return;
}
String token = extractToken(httpRequest);
if (token == null) {
sendUnauthorized(httpResponse, "Missing Authorization header");
return;
}
Optional<UserInfo> userInfo = jwtService.validateAndExtractUserInfo(token);
if (userInfo.isEmpty()) {
sendUnauthorized(httpResponse, "Invalid token");
return;
}
if (userInfo.get().isExpired()) {
sendUnauthorized(httpResponse, "Token expired");
return;
}
// Set user information in request
httpRequest.setAttribute("userInfo", userInfo.get());
chain.doFilter(request, response);
}
private String extractToken(HttpServletRequest request) {
String authHeader = request.getHeader("Authorization");
if (authHeader != null && authHeader.startsWith("Bearer ")) {
return authHeader.substring(7);
}
return null;
}
private boolean isPublicPath(String path) {
List<String> publicPaths = Arrays.asList("/public", "/health", "/actuator");
return publicPaths.stream().anyMatch(path::startsWith);
}
private void sendUnauthorized(HttpServletResponse response, String message) throws IOException {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType("application/json");
response.getWriter().write("{\"error\": \"" + message + "\"}");
}
}
// Role authentication annotation
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface RequireRole {
String[] value();
}
// Role authentication interceptor
@Component
public class RoleAuthorizationInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
if (!(handler instanceof HandlerMethod)) {
return true;
}
HandlerMethod handlerMethod = (HandlerMethod) handler;
RequireRole requireRole = handlerMethod.getMethodAnnotation(RequireRole.class);
if (requireRole == null) {
requireRole = handlerMethod.getBeanType().getAnnotation(RequireRole.class);
}
if (requireRole == null) {
return true;
}
UserInfo userInfo = (UserInfo) request.getAttribute("userInfo");
if (userInfo == null) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
return false;
}
String[] requiredRoles = requireRole.value();
boolean hasRequiredRole = Arrays.stream(requiredRoles)
.anyMatch(userInfo::hasRole);
if (!hasRequiredRole) {
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
return false;
}
return true;
}
}
Keycloak Admin API Integration
// Keycloak admin client
@Service
public class KeycloakAdminService {
private final Keycloak keycloakAdmin;
public KeycloakAdminService(
@Value("${keycloak.admin.server-url}") String serverUrl,
@Value("${keycloak.admin.realm}") String realm,
@Value("${keycloak.admin.client-id}") String clientId,
@Value("${keycloak.admin.client-secret}") String clientSecret) {
this.keycloakAdmin = KeycloakBuilder.builder()
.serverUrl(serverUrl)
.realm(realm)
.clientId(clientId)
.clientSecret(clientSecret)
.grantType(OAuth2Constants.CLIENT_CREDENTIALS)
.build();
}
public List<UserRepresentation> getAllUsers() {
return keycloakAdmin.realm("myrealm")
.users()
.list();
}
public Optional<UserRepresentation> getUserByUsername(String username) {
List<UserRepresentation> users = keycloakAdmin.realm("myrealm")
.users()
.search(username, true);
return users.stream().findFirst();
}
public String createUser(CreateUserRequest request) {
UserRepresentation user = new UserRepresentation();
user.setUsername(request.getUsername());
user.setEmail(request.getEmail());
user.setFirstName(request.getFirstName());
user.setLastName(request.getLastName());
user.setEnabled(true);
user.setEmailVerified(false);
Response response = keycloakAdmin.realm("myrealm")
.users()
.create(user);
if (response.getStatus() == 201) {
String locationHeader = response.getHeaderString("Location");
return locationHeader.substring(locationHeader.lastIndexOf('/') + 1);
} else {
throw new RuntimeException("Failed to create user: " + response.getStatusInfo());
}
}
public void updateUserRoles(String userId, List<String> roleNames) {
UserResource userResource = keycloakAdmin.realm("myrealm")
.users()
.get(userId);
RolesResource rolesResource = keycloakAdmin.realm("myrealm").roles();
List<RoleRepresentation> roles = roleNames.stream()
.map(roleName -> rolesResource.get(roleName).toRepresentation())
.collect(Collectors.toList());
userResource.roles().realmLevel().add(roles);
}
public void deleteUser(String userId) {
keycloakAdmin.realm("myrealm")
.users()
.get(userId)
.remove();
}
}
// User creation request model
public class CreateUserRequest {
private String username;
private String email;
private String firstName;
private String lastName;
// Constructor, getters, setters
}
// Admin API controller
@RestController
@RequestMapping("/admin/users")
@RequireRole("admin")
public class UserAdminController {
private final KeycloakAdminService adminService;
public UserAdminController(KeycloakAdminService adminService) {
this.adminService = adminService;
}
@GetMapping
public ResponseEntity<List<UserRepresentation>> getAllUsers() {
List<UserRepresentation> users = adminService.getAllUsers();
return ResponseEntity.ok(users);
}
@GetMapping("/{username}")
public ResponseEntity<UserRepresentation> getUserByUsername(@PathVariable String username) {
return adminService.getUserByUsername(username)
.map(user -> ResponseEntity.ok(user))
.orElse(ResponseEntity.notFound().build());
}
@PostMapping
public ResponseEntity<Map<String, String>> createUser(@RequestBody CreateUserRequest request) {
try {
String userId = adminService.createUser(request);
return ResponseEntity.status(HttpStatus.CREATED)
.body(Map.of("userId", userId));
} catch (Exception e) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(Map.of("error", e.getMessage()));
}
}
@PutMapping("/{userId}/roles")
public ResponseEntity<Void> updateUserRoles(
@PathVariable String userId,
@RequestBody List<String> roleNames) {
try {
adminService.updateUserRoles(userId, roleNames);
return ResponseEntity.ok().build();
} catch (Exception e) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).build();
}
}
@DeleteMapping("/{userId}")
public ResponseEntity<Void> deleteUser(@PathVariable String userId) {
try {
adminService.deleteUser(userId);
return ResponseEntity.ok().build();
} catch (Exception e) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).build();
}
}
}
Integration Testing
// Keycloak integration test
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@Testcontainers
public class KeycloakIntegrationTest {
@Container
static KeycloakContainer keycloak = new KeycloakContainer("quay.io/keycloak/keycloak:latest")
.withRealmImportFile("test-realm.json");
@Autowired
private TestRestTemplate restTemplate;
@LocalServerPort
private int port;
private String accessToken;
@BeforeEach
void setUp() {
// Get test token
accessToken = getAccessToken("test-user", "password");
}
@Test
void testProtectedEndpointWithValidToken() {
HttpHeaders headers = new HttpHeaders();
headers.setBearerAuth(accessToken);
HttpEntity<String> entity = new HttpEntity<>(headers);
ResponseEntity<String> response = restTemplate.exchange(
"http://localhost:" + port + "/api/users/profile",
HttpMethod.GET,
entity,
String.class
);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
}
@Test
void testProtectedEndpointWithoutToken() {
ResponseEntity<String> response = restTemplate.getForEntity(
"http://localhost:" + port + "/api/users/profile",
String.class
);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED);
}
@Test
void testAdminEndpointWithUserRole() {
HttpHeaders headers = new HttpHeaders();
headers.setBearerAuth(accessToken);
HttpEntity<String> entity = new HttpEntity<>(headers);
ResponseEntity<String> response = restTemplate.exchange(
"http://localhost:" + port + "/admin/users",
HttpMethod.GET,
entity,
String.class
);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN);
}
private String getAccessToken(String username, String password) {
// Get token from OAuth2 Token Endpoint
String tokenEndpoint = keycloak.getAuthServerUrl() + "/realms/test/protocol/openid-connect/token";
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
MultiValueMap<String, String> body = new LinkedMultiValueMap<>();
body.add("grant_type", "password");
body.add("client_id", "test-client");
body.add("client_secret", "test-secret");
body.add("username", username);
body.add("password", password);
HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<>(body, headers);
ResponseEntity<Map> response = restTemplate.postForEntity(tokenEndpoint, request, Map.class);
@SuppressWarnings("unchecked")
Map<String, Object> responseBody = response.getBody();
return (String) responseBody.get("access_token");
}
}