Article
In enterprise Java environments, managing authentication across multiple applications—Kubernetes dashboards, Jenkins, GitLab, and custom Spring Boot services—creates significant operational overhead. OpenUnison is an open-source identity and access management solution that provides a unified Single Sign-On (SSO) portal for your entire infrastructure. For Java development teams, it simplifies security by centralizing authentication against existing identity providers like Active Directory, LDAP, or OIDC providers.
What is OpenUnison?
OpenUnison is a cloud-native identity and access management proxy that provides:
- Single Sign-On (SSO): Unified login for all your applications
- Multi-protocol Support: SAML, OAuth 2.0, OpenID Connect, and LDAP
- Kubernetes Integration: Seamless access to Kubernetes clusters
- Identity Federation: Connect to existing identity providers
- Access Request Workflows: Self-service access management
Why OpenUnison for Java Applications?
- Unified Authentication: One login for all applications and services
- Enterprise Integration: Leverage existing Active Directory or LDAP
- Kubernetes-Native: Designed for cloud-native Java deployments
- Reduced Development Overhead: No need to implement auth in every service
- Compliance Ready: Built-in auditing and access controls
OpenUnison Architecture for Java Ecosystems
OpenUnison acts as an identity gateway between your users and applications:
User → OpenUnison Portal → Identity Provider (AD/LDAP/OIDC) → Java Applications ↓ Kubernetes Dashboard ↓ Jenkins, GitLab, etc.
Deploying OpenUnison in Kubernetes
1. Deploy OpenUnison Operator:
# openunison-operator.yaml apiVersion: apps/v1 kind: Deployment metadata: name: openunison-operator namespace: openunison spec: replicas: 1 selector: matchLabels: name: openunison-operator template: metadata: labels: name: openunison-operator spec: serviceAccountName: openunison-operator containers: - name: openunison-operator image: docker.io/tremolosecurity/openunison-operator:latest imagePullPolicy: Always env: - name: WATCH_NAMESPACE value: "" - name: POD_NAME valueFrom: fieldRef: fieldPath: metadata.name - name: OPERATOR_NAME value: "openunison-operator"
2. Create OpenUnison Custom Resource:
# openunison-cr.yaml apiVersion: openunison.tremolo.io/v1 kind: OpenUnison metadata: name: openunison namespace: openunison spec: enable_activemq: false network: openunison_host: sso.mycompany.com dashboard_host: k8s.mycompany.com api_server_host: k8s-api.mycompany.com session_inactivity_timeout_seconds: 900 certificates: use_k8s_cm: true trust_certs: - name: my-company-ca pem_b64: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0t... keyvalue: - name: K8S_DB_NAMESPACE value: openunison-database - name: K8S_DB_SECRET value: orchestra-db deployments: - name: openunison replicas: 2 image: docker.io/tremolosecurity/openunison:latest - name: orchestra replicas: 2 image: docker.io/tremolosecurity/orchestra:latest
Integrating Java Applications with OpenUnison
Approach 1: SAML Integration with Spring Security
1. Add SAML Dependencies:
<!-- pom.xml --> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-saml2-service-provider</artifactId> <version>6.1.0</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency>
2. SAML Security Configuration:
@Configuration
@EnableWebSecurity
public class SamlSecurityConfig {
@Value("${openunison.entity-id}")
private String openUnisonEntityId;
@Value("${openunison.metadata-url}")
private String metadataUrl;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authorize -> authorize
.anyRequest().authenticated()
)
.saml2Login(saml2 -> saml2
.loginProcessingUrl("/login/saml2/sso")
.authenticationManager(authenticationManager())
)
.saml2Logout(saml2 -> saml2
.logoutRequest(logout -> logout
.logoutUrl("/logout/saml2/slo")
)
.logoutResponse(logout -> logout
.logoutUrl("/logout/saml2/slo")
)
);
return http.build();
}
@Bean
public RelyingPartyRegistrationRepository relyingPartyRegistrations() {
RelyingPartyRegistration registration = RelyingPartyRegistrations
.fromMetadataLocation(metadataUrl)
.registrationId("openunison")
.entityId(openUnisonEntityId)
.build();
return new InMemoryRelyingPartyRegistrationRepository(registration);
}
@Bean
public AuthenticationManager authenticationManager() {
return new ProviderManager(samlAuthenticationProvider());
}
@Bean
public Saml2AuthenticationProvider samlAuthenticationProvider() {
OpenSaml4AuthenticationProvider provider = new OpenSaml4AuthenticationProvider();
provider.setResponseAuthenticationConverter(responseToken -> {
Saml2Authentication authentication =
OpenSaml4AuthenticationProvider
.createDefaultResponseAuthenticationConverter()
.convert(responseToken);
// Extract attributes from SAML response
Saml2AuthenticatedPrincipal principal =
(Saml2AuthenticatedPrincipal) authentication.getPrincipal();
String username = principal.getName();
List<String> groups = principal.getAttribute("groups");
String email = principal.getAttribute("email");
// Create custom authentication with extracted attributes
return new Saml2Authentication(
new CustomSaml2AuthenticatedPrincipal(principal, groups, email),
authentication.getSaml2Response(),
authorities(groups)
);
});
return provider;
}
private Collection<? extends GrantedAuthority> authorities(List<String> groups) {
return groups.stream()
.map(group -> new SimpleGrantedAuthority("ROLE_" + group.toUpperCase()))
.collect(Collectors.toList());
}
}
3. Custom SAML Principal:
public class CustomSaml2AuthenticatedPrincipal implements Saml2AuthenticatedPrincipal {
private final Saml2AuthenticatedPrincipal delegate;
private final List<String> groups;
private final String email;
public CustomSaml2AuthenticatedPrincipal(Saml2AuthenticatedPrincipal delegate,
List<String> groups, String email) {
this.delegate = delegate;
this.groups = groups != null ? groups : Collections.emptyList();
this.email = email;
}
@Override
public String getName() {
return delegate.getName();
}
@Override
public Map<String, List<Object>> getAttributes() {
return delegate.getAttributes();
}
@Override
public Collection<String> getSessionIndexes() {
return delegate.getSessionIndexes();
}
@Override
public String getRelyingPartyRegistrationId() {
return delegate.getRelyingPartyRegistrationId();
}
public List<String> getGroups() {
return groups;
}
public String getEmail() {
return email;
}
}
Approach 2: OpenID Connect Integration
1. OIDC Security Configuration:
@Configuration
@EnableWebSecurity
public class OidcSecurityConfig {
@Value("${openunison.issuer-uri}")
private String issuerUri;
@Value("${openunison.client-id}")
private String clientId;
@Value("${openunison.client-secret}")
private String clientSecret;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/public/**").permitAll()
.anyRequest().authenticated()
)
.oauth2Login(oauth2 -> oauth2
.loginPage("/oauth2/authorization/openunison")
.defaultSuccessUrl("/dashboard", true)
.failureUrl("/login?error=true")
)
.oauth2Client(oauth2 -> oauth2
.authorizationCodeGrant(codeGrant -> codeGrant
.authorizationRequestRepository(new HttpSessionOAuth2AuthorizationRequestRepository())
)
)
.logout(logout -> logout
.logoutSuccessUrl("/")
.invalidateHttpSession(true)
.clearAuthentication(true)
.deleteCookies("JSESSIONID")
);
return http.build();
}
@Bean
public ClientRegistrationRepository clientRegistrationRepository() {
ClientRegistration openUnisonRegistration = ClientRegistration
.withRegistrationId("openunison")
.clientId(clientId)
.clientSecret(clientSecret)
.scope("openid", "profile", "email", "groups")
.authorizationUri(issuerUri + "/authorize")
.tokenUri(issuerUri + "/token")
.userInfoUri(issuerUri + "/userinfo")
.jwkSetUri(issuerUri + "/protocol/openid-connect/certs")
.clientName("OpenUnison")
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.redirectUri("{baseUrl}/login/oauth2/code/{registrationId}")
.build();
return new InMemoryClientRegistrationRepository(openUnisonRegistration);
}
@Bean
public OAuth2UserService<OidcUserRequest, OidcUser> oidcUserService() {
final OidcUserService delegate = new OidcUserService();
return userRequest -> {
OidcUser oidcUser = delegate.loadUser(userRequest);
// Extract groups and custom attributes
Map<String, Object> claims = oidcUser.getClaims();
List<String> groups = (List<String>) claims.get("groups");
String email = (String) claims.get("email");
return new CustomOidcUser(oidcUser, groups, email);
};
}
}
2. Custom OIDC User:
public class CustomOidcUser extends DefaultOidcUser {
private final List<String> groups;
private final String email;
public CustomOidcUser(OidcUser oidcUser, List<String> groups, String email) {
super(oidcUser.getAuthorities(), oidcUser.getIdToken(), oidcUser.getUserInfo());
this.groups = groups != null ? groups : Collections.emptyList();
this.email = email;
}
public List<String> getGroups() {
return groups;
}
public String getEmail() {
return email;
}
@Override
public String getName() {
return getPreferredUsername();
}
public String getPreferredUsername() {
return (String) getAttributes().get("preferred_username");
}
}
Application Configuration
application.yml:
# OpenUnison Configuration
openunison:
entity-id: https://sso.mycompany.com/auth/metadata
metadata-url: https://sso.mycompany.com/auth/metadata
issuer-uri: https://sso.mycompany.com/auth/realms/mycompany
client-id: my-java-app
client-secret: ${OIDC_CLIENT_SECRET}
# Spring Security
spring:
security:
oauth2:
client:
provider:
openunison:
issuer-uri: https://sso.mycompany.com/auth/realms/mycompany
registration:
openunison:
client-id: my-java-app
client-secret: ${OIDC_CLIENT_SECRET}
scope: openid,profile,email,groups
# Application
server:
port: 8080
servlet:
session:
cookie:
name: JSESSIONID
secure: true
Secure Controller Implementation
@RestController
@RequestMapping("/api")
public class SecureController {
@GetMapping("/user/profile")
public ResponseEntity<UserProfile> getUserProfile(@AuthenticationPrincipal CustomOidcUser user) {
UserProfile profile = UserProfile.builder()
.username(user.getPreferredUsername())
.email(user.getEmail())
.groups(user.getGroups())
.fullName(user.getFullName())
.build();
return ResponseEntity.ok(profile);
}
@PreAuthorize("hasRole('DEVELOPERS')")
@GetMapping("/admin/dashboard")
public ResponseEntity<Map<String, String>> adminDashboard() {
return ResponseEntity.ok(Collections.singletonMap("message", "Admin access granted"));
}
@GetMapping("/public/info")
public ResponseEntity<Map<String, String>> publicInfo() {
return ResponseEntity.ok(Collections.singletonMap("message", "Public information"));
}
}
@Data
@Builder
class UserProfile {
private String username;
private String email;
private List<String> groups;
private String fullName;
}
Kubernetes Deployment with OpenUnison Integration
# deployment.yaml apiVersion: apps/v1 kind: Deployment metadata: name: java-app namespace: my-java-apps labels: app: java-app spec: replicas: 3 selector: matchLabels: app: java-app template: metadata: labels: app: java-app spec: containers: - name: app image: myregistry/java-app:latest ports: - containerPort: 8080 env: - name: OIDC_CLIENT_SECRET valueFrom: secretKeyRef: name: java-app-secrets key: oidc-client-secret - name: SPRING_PROFILES_ACTIVE value: "prod" resources: requests: memory: "512Mi" cpu: "250m" limits: memory: "1Gi" cpu: "500m" livenessProbe: httpGet: path: /actuator/health port: 8080 initialDelaySeconds: 30 periodSeconds: 10 readinessProbe: httpGet: path: /actuator/health/readiness port: 8080 initialDelaySeconds: 15 periodSeconds: 5 --- apiVersion: v1 kind: Service metadata: name: java-app namespace: my-java-apps spec: selector: app: java-app ports: - port: 80 targetPort: 8080 type: ClusterIP --- apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: java-app namespace: my-java-apps annotations: nginx.ingress.kubernetes.io/auth-url: "https://sso.mycompany.com/auth/url" nginx.ingress.kubernetes.io/auth-signin: "https://sso.mycompany.com/login" spec: rules: - host: app.mycompany.com http: paths: - path: / pathType: Prefix backend: service: name: java-app port: number: 80 tls: - hosts: - app.mycompany.com secretName: java-app-tls
OpenUnison Configuration for Multiple Java Apps
1. Application Registration in OpenUnison:
# app-registration.yaml apiVersion: openunison.tremolo.io/v1 kind: AppPortal metadata: name: java-app-portal namespace: openunison spec: label: Java Applications logo: /static/java.png groups: - "cn=developers,ou=groups,dc=mycompany,dc=com" links: - name: "Order Service" url: "https://orders.mycompany.com" groups: - "cn=orders-users,ou=groups,dc=mycompany,dc=com" - name: "User Service" url: "https://users.mycompany.com" groups: - "cn=user-admins,ou=groups,dc=mycompany,dc=com" - name: "Reporting Dashboard" url: "https://reports.mycompany.com" groups: - "cn=report-viewers,ou=groups,dc=mycompany,dc=com"
Monitoring and Health Checks
@Component
public class OpenUnisonHealthIndicator implements HealthIndicator {
private final RestTemplate restTemplate;
private final String metadataUrl;
public OpenUnisonHealthIndicator(@Value("${openunison.metadata-url}") String metadataUrl) {
this.metadataUrl = metadataUrl;
this.restTemplate = new RestTemplate();
}
@Override
public Health health() {
try {
ResponseEntity<String> response = restTemplate.getForEntity(metadataUrl, String.class);
if (response.getStatusCode().is2xxSuccessful()) {
return Health.up()
.withDetail("service", "OpenUnison")
.withDetail("status", "Available")
.build();
} else {
return Health.down()
.withDetail("service", "OpenUnison")
.withDetail("status", "Unavailable")
.withDetail("httpStatus", response.getStatusCodeValue())
.build();
}
} catch (Exception e) {
return Health.down(e)
.withDetail("service", "OpenUnison")
.build();
}
}
}
Best Practices for Java Teams
- Centralized Configuration: Store OpenUnison settings in ConfigMaps or external configuration
- Proper Session Management: Configure session timeouts and secure cookies
- Role-Based Access Control: Leverage group memberships from OpenUnison
- TLS Everywhere: Ensure all communication is encrypted
- Regular Auditing: Monitor authentication events and access patterns
@Aspect
@Component
public class SecurityAuditAspect {
private static final Logger logger = LoggerFactory.getLogger(SecurityAuditAspect.class);
@AfterReturning("execution(* com.example..*Controller.*(..)) && @annotation(org.springframework.security.access.prepost.PreAuthorize)")
public void auditAuthorizedAccess(JoinPoint joinPoint) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication != null && authentication.getPrincipal() instanceof CustomOidcUser) {
CustomOidcUser user = (CustomOidcUser) authentication.getPrincipal();
logger.info("User {} accessed {} with roles {}",
user.getPreferredUsername(),
joinPoint.getSignature().getName(),
user.getGroups());
}
}
}
Conclusion
OpenUnison provides a powerful, enterprise-ready SSO solution for Java applications in Kubernetes environments. By centralizing authentication and authorization, it eliminates the need for each Java service to implement its own security layer while providing seamless integration with existing identity providers.
The combination of OpenUnison with Spring Security's SAML and OIDC support creates a robust security foundation that scales across multiple applications and environments. This approach not only enhances security but also significantly reduces development and operational overhead, allowing Java teams to focus on delivering business value rather than managing authentication complexity.