google-site-verification: google61fe8ba583a51912.html
Centralized Access Control: Implementing SSO in Java with OpenUnison


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?

  1. Unified Authentication: One login for all applications and services
  2. Enterprise Integration: Leverage existing Active Directory or LDAP
  3. Kubernetes-Native: Designed for cloud-native Java deployments
  4. Reduced Development Overhead: No need to implement auth in every service
  5. 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

  1. Centralized Configuration: Store OpenUnison settings in ConfigMaps or external configuration
  2. Proper Session Management: Configure session timeouts and secure cookies
  3. Role-Based Access Control: Leverage group memberships from OpenUnison
  4. TLS Everywhere: Ensure all communication is encrypted
  5. 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.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper