Introduction
Ory Kratos is a cloud-native identity and user management system that provides secure authentication, registration, profile management, and recovery flows. For Java applications, integrating with Kratos enables robust, standards-compliant identity management without building authentication systems from scratch. This guide explores how to seamlessly integrate Ory Kratos with Java applications using Spring Boot.
Article: Implementing Ory Kratos Identity Management in Java
Ory Kratos provides API-first identity management that can be integrated into any Java application. It handles complex flows like login, registration, password reset, MFA, and social login while maintaining security best practices.
1. Ory Kratos Architecture Overview
Key Components:
- Kratos Public API - For browser-based flows (UI integration)
- Kratos Admin API - For backend operations (user management)
- Identity Schema - JSON Schema for user identity data
- Sessions - JWT-based session management
- Flows - Login, registration, recovery, verification, settings
Integration Patterns:
- Frontend Integration - Java backend + JavaScript frontend
- Backend Integration - Direct API calls from Java application
- Hybrid Approach - Combined frontend and backend integration
2. Maven Dependencies
pom.xml:
<properties>
<spring.boot.version>3.1.0</spring.boot.version>
<ory.kratos.version>0.13.0</ory.kratos.version>
<jackson.version>2.15.2</jackson.version>
</properties>
<dependencies>
<!-- Spring Boot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>${spring.boot.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<version>${spring.boot.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
<version>${spring.boot.version}</version>
</dependency>
<!-- Ory Kratos Client -->
<dependency>
<groupId>sh.ory.kratos</groupId>
<artifactId>kratos-client</artifactId>
<version>${ory.kratos.version}</version>
</dependency>
<!-- Jackson for JSON processing -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>${jackson.version}</version>
</dependency>
<!-- JWT for session validation -->
<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>
</dependencies>
3. Ory Kratos Configuration
Application Properties:
# application.yml ory: kratos: public-url: http://localhost:4433 admin-url: http://kratos:4434 browser-url: http://localhost:4455 session: cookie-name: ory_kratos_session endpoints: login: /self-service/login registration: /self-service/registration logout: /self-service/logout verification: /self-service/verification recovery: /self-service/recovery settings: /self-service/settings whoami: /sessions/whoami app: security: allowed-origins: "http://localhost:3000,http://localhost:8080"
Kratos Client Configuration:
package com.myapp.kratos.config;
import sh.ory.kratos.ApiClient;
import sh.ory.kratos.api.V0alpha2Api;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class KratosConfig {
@Value("${ory.kratos.public-url}")
private String kratosPublicUrl;
@Value("${ory.kratos.admin-url}")
private String kratosAdminUrl;
@Bean
public ApiClient kratosPublicClient() {
ApiClient client = new ApiClient();
client.setBasePath(kratosPublicUrl);
return client;
}
@Bean
public ApiClient kratosAdminClient() {
ApiClient client = new ApiClient();
client.setBasePath(kratosAdminUrl);
return client;
}
@Bean
public V0alpha2Api kratosPublicApi(ApiClient kratosPublicClient) {
return new V0alpha2Api(kratosPublicClient);
}
@Bean
public V0alpha2Api kratosAdminApi(ApiClient kratosAdminClient) {
return new V0alpha2Api(kratosAdminClient);
}
}
4. Kratos Service Layer
Kratos Service Interface:
package com.myapp.kratos.service;
import sh.ory.kratos.model.*;
import javax.servlet.http.HttpServletRequest;
import java.util.Optional;
public interface KratosService {
// Flow management
LoginFlow initializeLoginFlow();
RegistrationFlow initializeRegistrationFlow();
SettingsFlow initializeSettingsFlow(String sessionToken);
RecoveryFlow initializeRecoveryFlow();
VerificationFlow initializeVerificationFlow();
// Flow execution
SuccessfulNativeLogin executeLoginFlow(String flowId, Map<String, Object> traits);
SuccessfulNativeRegistration executeRegistrationFlow(String flowId, Map<String, Object> traits);
void executeLogoutFlow(String sessionToken);
// Session management
Optional<Session> getSession(String sessionToken);
Session whoami(String sessionToken);
void deleteSession(String sessionToken);
// Identity management
Identity getIdentity(String identityId);
Identity updateIdentity(String identityId, Map<String, Object> traits);
void deleteIdentity(String identityId);
// Request helpers
Optional<String> extractSessionToken(HttpServletRequest request);
}
Kratos Service Implementation:
package com.myapp.kratos.service;
import sh.ory.kratos.ApiClient;
import sh.ory.kratos.ApiException;
import sh.ory.kratos.api.V0alpha2Api;
import sh.ory.kratos.model.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import javax.servlet.http.HttpServletRequest;
import java.util.Map;
import java.util.Optional;
@Service
public class KratosServiceImpl implements KratosService {
private static final Logger logger = LoggerFactory.getLogger(KratosServiceImpl.class);
private final V0alpha2Api kratosPublicApi;
private final V0alpha2Api kratosAdminApi;
@Value("${ory.kratos.session.cookie-name:ory_kratos_session}")
private String sessionCookieName;
public KratosServiceImpl(
@Qualifier("kratosPublicApi") V0alpha2Api kratosPublicApi,
@Qualifier("kratosAdminApi") V0alpha2Api kratosAdminApi) {
this.kratosPublicApi = kratosPublicApi;
this.kratosAdminApi = kratosAdminApi;
}
@Override
public LoginFlow initializeLoginFlow() {
try {
return kratosPublicApi.initializeSelfServiceLoginFlowForNativeApps();
} catch (ApiException e) {
logger.error("Failed to initialize login flow", e);
throw new KratosException("Failed to initialize login flow", e);
}
}
@Override
public RegistrationFlow initializeRegistrationFlow() {
try {
return kratosPublicApi.initializeSelfServiceRegistrationFlowForNativeApps();
} catch (ApiException e) {
logger.error("Failed to initialize registration flow", e);
throw new KratosException("Failed to initialize registration flow", e);
}
}
@Override
public SettingsFlow initializeSettingsFlow(String sessionToken) {
try {
return kratosPublicApi.initializeSelfServiceSettingsFlowForNativeApps(sessionToken);
} catch (ApiException e) {
logger.error("Failed to initialize settings flow", e);
throw new KratosException("Failed to initialize settings flow", e);
}
}
@Override
public RecoveryFlow initializeRecoveryFlow() {
try {
return kratosPublicApi.initializeSelfServiceRecoveryFlowForNativeApps();
} catch (ApiException e) {
logger.error("Failed to initialize recovery flow", e);
throw new KratosException("Failed to initialize recovery flow", e);
}
}
@Override
public VerificationFlow initializeVerificationFlow() {
try {
return kratosPublicApi.initializeSelfServiceVerificationFlowForNativeApps();
} catch (ApiException e) {
logger.error("Failed to initialize verification flow", e);
throw new KratosException("Failed to initialize verification flow", e);
}
}
@Override
public SuccessfulNativeLogin executeLoginFlow(String flowId, Map<String, Object> traits) {
try {
SubmitSelfServiceLoginFlowBody body = new SubmitSelfServiceLoginFlowBody();
body.setMethod("password");
body.setCsrfToken(extractCsrfToken(traits));
// Set traits based on your identity schema
if (traits.containsKey("email")) {
body.setPassword(traits.get("password").toString());
body.setIdentifier(traits.get("email").toString());
}
return kratosPublicApi.submitSelfServiceLoginFlow(flowId, body);
} catch (ApiException e) {
logger.error("Failed to execute login flow: {}", flowId, e);
throw new KratosException("Login flow execution failed", e);
}
}
@Override
public SuccessfulNativeRegistration executeRegistrationFlow(String flowId, Map<String, Object> traits) {
try {
SubmitSelfServiceRegistrationFlowBody body = new SubmitSelfServiceRegistrationFlowBody();
body.setMethod("password");
body.setCsrfToken(extractCsrfToken(traits));
// Set traits based on your identity schema
if (traits.containsKey("traits")) {
@SuppressWarnings("unchecked")
Map<String, Object> identityTraits = (Map<String, Object>) traits.get("traits");
body.setTraits(identityTraits);
}
if (traits.containsKey("password")) {
body.setPassword(traits.get("password").toString());
}
return kratosPublicApi.submitSelfServiceRegistrationFlow(flowId, body);
} catch (ApiException e) {
logger.error("Failed to execute registration flow: {}", flowId, e);
throw new KratosException("Registration flow execution failed", e);
}
}
@Override
public void executeLogoutFlow(String sessionToken) {
try {
kratosPublicApi.submitSelfServiceLogoutFlow(sessionToken);
} catch (ApiException e) {
logger.error("Failed to execute logout flow", e);
throw new KratosException("Logout failed", e);
}
}
@Override
public Optional<Session> getSession(String sessionToken) {
try {
Session session = kratosPublicApi.toSession(sessionToken);
return Optional.of(session);
} catch (ApiException e) {
if (e.getCode() == 401) {
return Optional.empty();
}
logger.error("Failed to get session", e);
throw new KratosException("Failed to get session", e);
}
}
@Override
public Session whoami(String sessionToken) {
try {
return kratosPublicApi.toSession(sessionToken);
} catch (ApiException e) {
logger.error("Failed to get session info", e);
throw new KratosException("Failed to get session info", e);
}
}
@Override
public void deleteSession(String sessionToken) {
try {
kratosAdminApi.adminDeleteIdentitySessions(sessionToken);
} catch (ApiException e) {
logger.error("Failed to delete session", e);
throw new KratosException("Failed to delete session", e);
}
}
@Override
public Identity getIdentity(String identityId) {
try {
return kratosAdminApi.adminGetIdentity(identityId);
} catch (ApiException e) {
logger.error("Failed to get identity: {}", identityId, e);
throw new KratosException("Failed to get identity", e);
}
}
@Override
public Identity updateIdentity(String identityId, Map<String, Object> traits) {
try {
AdminUpdateIdentityBody body = new AdminUpdateIdentityBody();
body.setTraits(traits);
body.setSchemaId("default");
return kratosAdminApi.adminUpdateIdentity(identityId, body);
} catch (ApiException e) {
logger.error("Failed to update identity: {}", identityId, e);
throw new KratosException("Failed to update identity", e);
}
}
@Override
public void deleteIdentity(String identityId) {
try {
kratosAdminApi.adminDeleteIdentity(identityId);
} catch (ApiException e) {
logger.error("Failed to delete identity: {}", identityId, e);
throw new KratosException("Failed to delete identity", e);
}
}
@Override
public Optional<String> extractSessionToken(HttpServletRequest request) {
// Check cookie first
javax.servlet.http.Cookie[] cookies = request.getCookies();
if (cookies != null) {
for (javax.servlet.http.Cookie cookie : cookies) {
if (sessionCookieName.equals(cookie.getName())) {
return Optional.of(cookie.getValue());
}
}
}
// Check Authorization header
String authHeader = request.getHeader("Authorization");
if (authHeader != null && authHeader.startsWith("Bearer ")) {
return Optional.of(authHeader.substring(7));
}
return Optional.empty();
}
private String extractCsrfToken(Map<String, Object> traits) {
return traits.getOrDefault("csrf_token", "").toString();
}
}
class KratosException extends RuntimeException {
public KratosException(String message) {
super(message);
}
public KratosException(String message, Throwable cause) {
super(message, cause);
}
}
5. Spring Security Integration
Kratos Authentication Filter:
package com.myapp.security;
import com.myapp.kratos.service.KratosService;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.List;
import java.util.stream.Collectors;
public class KratosAuthenticationFilter extends OncePerRequestFilter {
private final KratosService kratosService;
public KratosAuthenticationFilter(KratosService kratosService) {
this.kratosService = kratosService;
}
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
try {
// Extract session token from request
String sessionToken = kratosService.extractSessionToken(request).orElse(null);
if (sessionToken != null) {
// Validate session with Kratos
kratosService.getSession(sessionToken).ifPresent(session -> {
// Create Spring Security authentication
KratosAuthentication authentication = createAuthentication(session, sessionToken);
SecurityContextHolder.getContext().setAuthentication(authentication);
});
}
} catch (Exception e) {
logger.error("Kratos authentication failed", e);
// Continue without authentication
}
filterChain.doFilter(request, response);
}
private KratosAuthentication createAuthentication(sh.ory.kratos.model.Session session, String sessionToken) {
String identityId = session.getIdentity().getId();
List<SimpleGrantedAuthority> authorities = extractAuthorities(session);
return new KratosAuthentication(identityId, sessionToken, authorities, session);
}
private List<SimpleGrantedAuthority> extractAuthorities(sh.ory.kratos.model.Session session) {
// Extract roles and permissions from identity traits
// This depends on your identity schema configuration
return List.of(new SimpleGrantedAuthority("ROLE_USER"));
}
}
Custom Authentication Object:
package com.myapp.security;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import java.util.Collection;
public class KratosAuthentication extends AbstractAuthenticationToken {
private final String identityId;
private final String sessionToken;
private final sh.ory.kratos.model.Session session;
public KratosAuthentication(String identityId, String sessionToken,
Collection<? extends GrantedAuthority> authorities,
sh.ory.kratos.model.Session session) {
super(authorities);
this.identityId = identityId;
this.sessionToken = sessionToken;
this.session = session;
setAuthenticated(true);
}
@Override
public Object getCredentials() {
return sessionToken;
}
@Override
public Object getPrincipal() {
return identityId;
}
public sh.ory.kratos.model.Session getSession() {
return session;
}
public String getIdentityId() {
return identityId;
}
}
Security Configuration:
package com.myapp.config;
import com.myapp.kratos.service.KratosService;
import com.myapp.security.KratosAuthenticationFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import java.util.List;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
private final KratosService kratosService;
public SecurityConfig(KratosService kratosService) {
this.kratosService = kratosService;
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
.csrf(csrf -> csrf.disable())
.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(authz -> authz
.requestMatchers("/api/auth/**", "/public/**", "/error").permitAll()
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.addFilterBefore(
new KratosAuthenticationFilter(kratosService),
UsernamePasswordAuthenticationFilter.class
);
return http.build();
}
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(List.of("http://localhost:3000", "http://localhost:8080"));
configuration.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS"));
configuration.setAllowedHeaders(List.of("*"));
configuration.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
}
6. REST Controllers
Authentication Controller:
package com.myapp.controller;
import com.myapp.kratos.service.KratosService;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.HashMap;
import java.util.Map;
@RestController
@RequestMapping("/api/auth")
public class AuthController {
private final KratosService kratosService;
public AuthController(KratosService kratosService) {
this.kratosService = kratosService;
}
@GetMapping("/login")
public ResponseEntity<Map<String, Object>> initializeLogin() {
try {
var flow = kratosService.initializeLoginFlow();
Map<String, Object> response = new HashMap<>();
response.put("flowId", flow.getId());
response.put("ui", flow.getUi());
return ResponseEntity.ok(response);
} catch (Exception e) {
return ResponseEntity.badRequest().body(Map.of("error", e.getMessage()));
}
}
@PostMapping("/login")
public ResponseEntity<Map<String, Object>> login(
@RequestParam String flowId,
@RequestBody Map<String, Object> loginRequest) {
try {
var result = kratosService.executeLoginFlow(flowId, loginRequest);
Map<String, Object> response = new HashMap<>();
response.put("sessionToken", result.getSessionToken());
response.put("session", result.getSession());
return ResponseEntity.ok(response);
} catch (Exception e) {
return ResponseEntity.badRequest().body(Map.of("error", e.getMessage()));
}
}
@GetMapping("/register")
public ResponseEntity<Map<String, Object>> initializeRegistration() {
try {
var flow = kratosService.initializeRegistrationFlow();
Map<String, Object> response = new HashMap<>();
response.put("flowId", flow.getId());
response.put("ui", flow.getUi());
return ResponseEntity.ok(response);
} catch (Exception e) {
return ResponseEntity.badRequest().body(Map.of("error", e.getMessage()));
}
}
@PostMapping("/register")
public ResponseEntity<Map<String, Object>> register(
@RequestParam String flowId,
@RequestBody Map<String, Object> registrationRequest) {
try {
var result = kratosService.executeRegistrationFlow(flowId, registrationRequest);
Map<String, Object> response = new HashMap<>();
response.put("sessionToken", result.getSessionToken());
response.put("session", result.getSession());
return ResponseEntity.ok(response);
} catch (Exception e) {
return ResponseEntity.badRequest().body(Map.of("error", e.getMessage()));
}
}
@PostMapping("/logout")
public ResponseEntity<Map<String, Object>> logout(HttpServletRequest request) {
try {
var sessionToken = kratosService.extractSessionToken(request).orElse(null);
if (sessionToken != null) {
kratosService.executeLogoutFlow(sessionToken);
}
return ResponseEntity.ok(Map.of("message", "Logged out successfully"));
} catch (Exception e) {
return ResponseEntity.badRequest().body(Map.of("error", e.getMessage()));
}
}
@GetMapping("/session")
public ResponseEntity<Map<String, Object>> getSession(HttpServletRequest request) {
try {
var sessionToken = kratosService.extractSessionToken(request).orElse(null);
if (sessionToken == null) {
return ResponseEntity.status(401).body(Map.of("error", "No session found"));
}
var session = kratosService.whoami(sessionToken);
return ResponseEntity.ok(Map.of("session", session));
} catch (Exception e) {
return ResponseEntity.status(401).body(Map.of("error", "Invalid session"));
}
}
}
User Profile Controller:
package com.myapp.controller;
import com.myapp.kratos.service.KratosService;
import com.myapp.security.KratosAuthentication;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
@RestController
@RequestMapping("/api/user")
public class UserController {
private final KratosService kratosService;
public UserController(KratosService kratosService) {
this.kratosService = kratosService;
}
@GetMapping("/profile")
public ResponseEntity<Map<String, Object>> getProfile(@AuthenticationPrincipal KratosAuthentication authentication) {
try {
var identity = kratosService.getIdentity(authentication.getIdentityId());
Map<String, Object> profile = Map.of(
"id", identity.getId(),
"traits", identity.getTraits(),
"createdAt", identity.getCreatedAt(),
"updatedAt", identity.getUpdatedAt()
);
return ResponseEntity.ok(profile);
} catch (Exception e) {
return ResponseEntity.badRequest().body(Map.of("error", e.getMessage()));
}
}
@PutMapping("/profile")
public ResponseEntity<Map<String, Object>> updateProfile(
@AuthenticationPrincipal KratosAuthentication authentication,
@RequestBody Map<String, Object> updates) {
try {
@SuppressWarnings("unchecked")
Map<String, Object> traits = (Map<String, Object>) updates.get("traits");
var updatedIdentity = kratosService.updateIdentity(authentication.getIdentityId(), traits);
return ResponseEntity.ok(Map.of(
"message", "Profile updated successfully",
"identity", updatedIdentity
));
} catch (Exception e) {
return ResponseEntity.badRequest().body(Map.of("error", e.getMessage()));
}
}
@GetMapping("/settings")
public ResponseEntity<Map<String, Object>> initializeSettings(@AuthenticationPrincipal KratosAuthentication authentication) {
try {
var flow = kratosService.initializeSettingsFlow(authentication.getCredentials().toString());
return ResponseEntity.ok(Map.of(
"flowId", flow.getId(),
"ui", flow.getUi()
));
} catch (Exception e) {
return ResponseEntity.badRequest().body(Map.of("error", e.getMessage()));
}
}
}
7. Frontend Integration Helper
Kratos UI Helper:
package com.myapp.kratos.ui;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.stereotype.Component;
import java.util.Map;
@Component
public class KratosUIHelper {
private final ObjectMapper objectMapper;
public KratosUIHelper(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
}
public Map<String, Object> extractFormFields(sh.ory.kratos.model.UiContainer ui) {
// Extract form fields from Kratos UI for frontend rendering
return Map.of(
"action", ui.getAction(),
"method", ui.getMethod(),
"nodes", ui.getNodes()
);
}
public Map<String, Object> createLoginResponse(sh.ory.kratos.model.LoginFlow flow) {
return Map.of(
"flowId", flow.getId(),
"ui", extractFormFields(flow.getUi()),
"type", flow.getType()
);
}
public Map<String, Object> createRegistrationResponse(sh.ory.kratos.model.RegistrationFlow flow) {
return Map.of(
"flowId", flow.getId(),
"ui", extractFormFields(flow.getUi()),
"type", flow.getType()
);
}
}
8. Error Handling
Global Exception Handler:
package com.myapp.exception;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.util.Map;
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(KratosException.class)
public ResponseEntity<Map<String, Object>> handleKratosException(KratosException e) {
return ResponseEntity.badRequest().body(Map.of(
"error", "Kratos operation failed",
"message", e.getMessage()
));
}
@ExceptionHandler(sh.ory.kratos.ApiException.class)
public ResponseEntity<Map<String, Object>> handleApiException(sh.ory.kratos.ApiException e) {
return ResponseEntity.status(e.getCode()).body(Map.of(
"error", "Kratos API error",
"message", e.getMessage(),
"code", e.getCode()
));
}
}
9. Application Configuration
Kratos Identity Schema (identity.schema.json):
{
"$id": "https://example.com/identity.schema.json",
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "User",
"type": "object",
"properties": {
"traits": {
"type": "object",
"properties": {
"email": {
"type": "string",
"format": "email",
"title": "E-Mail",
"minLength": 3,
"ory.sh/kratos": {
"credentials": {
"password": {
"identifier": true
}
},
"verification": {
"via": "email"
},
"recovery": {
"via": "email"
}
}
},
"name": {
"type": "object",
"properties": {
"first": {
"type": "string",
"title": "First Name"
},
"last": {
"type": "string",
"title": "Last Name"
}
}
},
"roles": {
"type": "array",
"items": {
"type": "string"
},
"default": ["user"]
}
},
"required": ["email"],
"additionalProperties": false
}
}
}
10. Docker Compose for Development
docker-compose.yml:
version: '3.8' services: kratos: image: oryd/kratos:v0.13.0 ports: - "4433:4433" # Public API - "4434:4434" # Admin API - "4455:4455" # UI environment: - DSN=memory - LOG_LEVEL=debug volumes: - ./kratos:/etc/config/kratos - ./identity.schema.json:/etc/config/kratos/identity.schema.json command: serve -c /etc/config/kratos/kratos.yml --dev kratos-migrate: image: oryd/kratos:v0.13.0 environment: - DSN=memory volumes: - ./kratos:/etc/config/kratos command: migrate sql -e --yes -c /etc/config/kratos/kratos.yml depends_on: - kratos java-app: build: . ports: - "8080:8080" environment: - ORY_KRATOS_PUBLIC_URL=http://kratos:4433 - ORY_KRATOS_ADMIN_URL=http://kratos:4434 - SPRING_PROFILES_ACTIVE=dev depends_on: - kratos
11. Testing
Integration Test:
package com.myapp.test;
import com.myapp.kratos.service.KratosService;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;
@SpringBootTest
public class KratosIntegrationTest {
@MockBean
private KratosService kratosService;
@Test
void testLoginFlow() {
// Mock Kratos responses
when(kratosService.initializeLoginFlow()).thenReturn(createMockLoginFlow());
// Test login flow initialization
// ... implementation
}
private sh.ory.kratos.model.LoginFlow createMockLoginFlow() {
// Create mock login flow
return new sh.ory.kratos.model.LoginFlow();
}
}
Benefits of Ory Kratos Integration
- Production-Ready Authentication - Battle-tested identity flows
- Security Best Practices - Built-in protection against common attacks
- Extensible - Custom identity schemas and flows
- Standards Compliant - OAuth2, OpenID Connect, and more
- Cloud Native - Designed for microservices and containers
- Reduced Development Time - No need to build auth from scratch
Conclusion
Integrating Ory Kratos with Java applications provides a robust, secure, and scalable identity management solution. By leveraging Kratos' powerful API and combining it with Spring Security, you can implement comprehensive authentication and authorization flows with minimal custom code.
The key to successful integration is:
- Proper session management across frontend and backend
- Secure API communication with Kratos services
- Comprehensive error handling for various flow states
- Flexible identity schemas that match your business needs
- Thorough testing of all authentication scenarios
Start with basic login/registration flows and gradually add more advanced features like MFA, social login, and passwordless authentication as your application evolves.
Call to Action: Begin by setting up a local Kratos instance and implementing basic login/registration in your Java application. Test the integration thoroughly, then gradually add more advanced features like profile management and password recovery. Monitor the authentication flows in production to ensure security and reliability.