Keycloak Java Adapter

AuthenticationJavaKeycloakOpenID ConnectSAMLSSO

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

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");
    }
}