Identity Access module in Java - Wiz-DevTech/prettygirllz GitHub Wiki

Here's how we can implement the core components of the Identity Access module in Java:

1. Core Entity Models

Let's start with the User entity that will be stored in PostgreSQL:

// src/main/java/com/wizdevtech/identityaccess/model/User.java
package com.wizdevtech.identityaccess.model;

import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;

import java.time.LocalDateTime;
import java.util.HashSet;
import java.util.Set;

@Entity
@Table(name = "users")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @Column(nullable = false, unique = true)
    private String email;
    
    @Column(name = "password_hash", nullable = false)
    private String passwordHash;
    
    @Column(name = "sensitive_data")
    private String sensitiveData;
    
    @ElementCollection(fetch = FetchType.EAGER)
    @CollectionTable(name = "user_roles", joinColumns = @JoinColumn(name = "user_id"))
    @Column(name = "role")
    private Set<String> roles = new HashSet<>();
    
    @CreationTimestamp
    @Column(name = "created_at")
    private LocalDateTime createdAt;
    
    @UpdateTimestamp
    @Column(name = "updated_at")
    private LocalDateTime updatedAt;
}

2. PostgreSQL Repository

Let's implement the repository for database operations:

// src/main/java/com/wizdevtech/identityaccess/repository/UserRepository.java
package com.wizdevtech.identityaccess.repository;

import com.wizdevtech.identityaccess.model.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import java.util.Optional;

@Repository
public interface UserRepository extends JpaRepository<User, Long> {
    
    Optional<User> findByEmail(String email);
    
    boolean existsByEmail(String email);
}

3. Encryption Service

Let's create a service for handling sensitive data encryption:

// src/main/java/com/wizdevtech/identityaccess/service/EncryptionService.java
package com.wizdevtech.identityaccess.service;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import java.util.Base64;

@Service
public class EncryptionService {

    private static final String ALGORITHM = "AES/GCM/NoPadding";
    private static final int GCM_TAG_LENGTH = 16;
    private static final int GCM_IV_LENGTH = 12;
    
    private final SecretKey secretKey;
    
    public EncryptionService(@Value("${encryption.key}") String encryptionKey) {
        // Convert key to bytes and create SecretKeySpec
        byte[] keyBytes = encryptionKey.getBytes(StandardCharsets.UTF_8);
        if (keyBytes.length != 32) {
            throw new IllegalArgumentException("Encryption key must be 32 bytes (256 bits)");
        }
        this.secretKey = new SecretKeySpec(keyBytes, "AES");
    }
    
    public String encrypt(String data) {
        if (data == null) {
            return null;
        }
        
        try {
            // Generate IV
            byte[] iv = new byte[GCM_IV_LENGTH];
            new SecureRandom().nextBytes(iv);
            
            // Initialize cipher
            Cipher cipher = Cipher.getInstance(ALGORITHM);
            GCMParameterSpec parameterSpec = new GCMParameterSpec(GCM_TAG_LENGTH * 8, iv);
            cipher.init(Cipher.ENCRYPT_MODE, secretKey, parameterSpec);
            
            // Encrypt
            byte[] encryptedData = cipher.doFinal(data.getBytes(StandardCharsets.UTF_8));
            
            // Combine IV and encrypted data
            ByteBuffer byteBuffer = ByteBuffer.allocate(iv.length + encryptedData.length);
            byteBuffer.put(iv);
            byteBuffer.put(encryptedData);
            
            // Base64 encode for storage
            return Base64.getEncoder().encodeToString(byteBuffer.array());
            
        } catch (Exception e) {
            throw new RuntimeException("Encryption failed", e);
        }
    }
    
    public String decrypt(String encryptedData) {
        if (encryptedData == null) {
            return null;
        }
        
        try {
            // Decode from Base64
            byte[] decodedData = Base64.getDecoder().decode(encryptedData);
            
            // Extract IV
            ByteBuffer byteBuffer = ByteBuffer.wrap(decodedData);
            byte[] iv = new byte[GCM_IV_LENGTH];
            byteBuffer.get(iv);
            
            // Extract encrypted data
            byte[] cipherText = new byte[byteBuffer.remaining()];
            byteBuffer.get(cipherText);
            
            // Initialize cipher for decryption
            Cipher cipher = Cipher.getInstance(ALGORITHM);
            GCMParameterSpec parameterSpec = new GCMParameterSpec(GCM_TAG_LENGTH * 8, iv);
            cipher.init(Cipher.DECRYPT_MODE, secretKey, parameterSpec);
            
            // Decrypt
            byte[] decryptedData = cipher.doFinal(cipherText);
            return new String(decryptedData, StandardCharsets.UTF_8);
            
        } catch (Exception e) {
            throw new RuntimeException("Decryption failed", e);
        }
    }
}

4. JWT Authentication Service

Now, let's implement JWT authentication:

// src/main/java/com/wizdevtech/identityaccess/service/JwtService.java
package com.wizdevtech.identityaccess.service;

import com.wizdevtech.identityaccess.model.User;
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Service;

import java.nio.charset.StandardCharsets;
import java.security.Key;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;

@Service
public class JwtService {

    @Value("${jwt.secret}")
    private String jwtSecret;
    
    @Value("${jwt.expiration}")
    private long jwtExpiration;
    
    @Value("${jwt.issuer}")
    private String issuer;
    
    public String generateToken(User user) {
        Map<String, Object> claims = new HashMap<>();
        claims.put("email", user.getEmail());
        claims.put("roles", user.getRoles());
        
        return Jwts.builder()
                .setClaims(claims)
                .setSubject(user.getId().toString())
                .setIssuedAt(new Date())
                .setExpiration(new Date(System.currentTimeMillis() + jwtExpiration))
                .setIssuer(issuer)
                .signWith(getSigningKey(), SignatureAlgorithm.HS256)
                .compact();
    }
    
    public boolean validateToken(String token, UserDetails userDetails) {
        final String username = extractUsername(token);
        return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
    }
    
    public String extractUsername(String token) {
        return extractClaim(token, Claims::getSubject);
    }
    
    public Date extractExpiration(String token) {
        return extractClaim(token, Claims::getExpiration);
    }
    
    public <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
        final Claims claims = extractAllClaims(token);
        return claimsResolver.apply(claims);
    }
    
    private Claims extractAllClaims(String token) {
        return Jwts.parserBuilder()
                .setSigningKey(getSigningKey())
                .build()
                .parseClaimsJws(token)
                .getBody();
    }
    
    private Key getSigningKey() {
        byte[] keyBytes = jwtSecret.getBytes(StandardCharsets.UTF_8);
        return Keys.hmacShaKeyFor(keyBytes);
    }
    
    private boolean isTokenExpired(String token) {
        return extractExpiration(token).before(new Date());
    }
}

5. Authentication Service

Now let's create the main authentication service:

// src/main/java/com/wizdevtech/identityaccess/service/AuthenticationService.java
package com.wizdevtech.identityaccess.service;

import com.wizdevtech.identityaccess.dto.AuthenticationRequest;
import com.wizdevtech.identityaccess.dto.AuthenticationResponse;
import com.wizdevtech.identityaccess.model.User;
import com.wizdevtech.identityaccess.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class AuthenticationService {

    private final UserRepository userRepository;
    private final JwtService jwtService;
    private final PasswordEncoder passwordEncoder;
    private final EncryptionService encryptionService;
    
    public AuthenticationResponse authenticate(AuthenticationRequest request) {
        // Find user by email
        User user = userRepository.findByEmail(request.getEmail())
                .orElseThrow(() -> new UsernameNotFoundException("User not found with email: " + request.getEmail()));
                
        // Verify password
        if (!passwordEncoder.matches(request.getPassword(), user.getPasswordHash())) {
            throw new BadCredentialsException("Invalid credentials");
        }
        
        // Generate JWT token
        String token = jwtService.generateToken(user);
        
        return AuthenticationResponse.builder()
                .token(token)
                .id(user.getId())
                .email(user.getEmail())
                .roles(user.getRoles())
                .build();
    }
    
    public User createUser(User user, String rawPassword) {
        // Check if user exists
        if (userRepository.existsByEmail(user.getEmail())) {
            throw new IllegalArgumentException("Email already in use");
        }
        
        // Hash password
        user.setPasswordHash(passwordEncoder.encode(rawPassword));
        
        // Encrypt sensitive data if present
        if (user.getSensitiveData() != null) {
            user.setSensitiveData(encryptionService.encrypt(user.getSensitiveData()));
        }
        
        // Save user
        return userRepository.save(user);
    }
}

6. REST API Controllers

Let's implement the REST endpoints:

// src/main/java/com/wizdevtech/identityaccess/controller/AuthController.java
package com.wizdevtech.identityaccess.controller;

import com.wizdevtech.identityaccess.dto.AuthenticationRequest;
import com.wizdevtech.identityaccess.dto.AuthenticationResponse;
import com.wizdevtech.identityaccess.dto.RegistrationRequest;
import com.wizdevtech.identityaccess.model.User;
import com.wizdevtech.identityaccess.service.AuthenticationService;
import com.wizdevtech.identityaccess.service.JwtService;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api/auth")
@RequiredArgsConstructor
public class AuthController {

    private final AuthenticationService authService;
    private final JwtService jwtService;
    
    @PostMapping("/login")
    public ResponseEntity<AuthenticationResponse> login(@Valid @RequestBody AuthenticationRequest request) {
        return ResponseEntity.ok(authService.authenticate(request));
    }
    
    @PostMapping("/register")
    public ResponseEntity<AuthenticationResponse> register(@Valid @RequestBody RegistrationRequest request) {
        // Create user entity from request
        User user = new User();
        user.setEmail(request.getEmail());
        user.setRoles(request.getRoles());
        
        // Create user in database
        User createdUser = authService.createUser(user, request.getPassword());
        
        // Generate token for auto login
        String token = jwtService.generateToken(createdUser);
        
        return ResponseEntity.ok(AuthenticationResponse.builder()
                .token(token)
                .id(createdUser.getId())
                .email(createdUser.getEmail())
                .roles(createdUser.getRoles())
                .build());
    }
    
    @PostMapping("/validate")
    public ResponseEntity<Boolean> validateToken(@RequestParam String token) {
        // Simple token validation without user lookup
        try {
            jwtService.extractAllClaims(token);
            return ResponseEntity.ok(true);
        } catch (Exception e) {
            return ResponseEntity.ok(false);
        }
    }
}

7. gRPC Service

Let's implement the gRPC service. First, the proto file:

// src/main/proto/auth.proto
syntax = "proto3";

package com.wizdevtech.identityaccess.grpc;

option java_multiple_files = true;
option java_package = "com.wizdevtech.identityaccess.grpc";

service AuthService {
  rpc Login (LoginRequest) returns (LoginResponse);
  rpc ValidateToken (ValidateTokenRequest) returns (ValidateTokenResponse);
}

message LoginRequest {
  string email = 1;
  string password = 2;
}

message LoginResponse {
  bool success = 1;
  string message = 2;
  string token = 3;
  User user = 4;
}

message User {
  int64 id = 1;
  string email = 2;
  repeated string roles = 3;
}

message ValidateTokenRequest {
  string token = 1;
}

message ValidateTokenResponse {
  bool is_valid = 1;
  User user = 2;
}

And the gRPC service implementation:

// src/main/java/com/wizdevtech/identityaccess/grpc/AuthServiceImpl.java
package com.wizdevtech.identityaccess.grpc;

import com.wizdevtech.identityaccess.dto.AuthenticationRequest;
import com.wizdevtech.identityaccess.dto.AuthenticationResponse;
import com.wizdevtech.identityaccess.service.AuthenticationService;
import com.wizdevtech.identityaccess.service.JwtService;
import io.grpc.Status;
import io.grpc.stub.StreamObserver;
import lombok.RequiredArgsConstructor;
import org.lognet.springboot.grpc.GRpcService;
import org.springframework.security.authentication.BadCredentialsException;

import java.util.stream.Collectors;

@GRpcService
@RequiredArgsConstructor
public class AuthServiceImpl extends AuthServiceGrpc.AuthServiceImplBase {

    private final AuthenticationService authService;
    private final JwtService jwtService;
    
    @Override
    public void login(LoginRequest request, StreamObserver<LoginResponse> responseObserver) {
        try {
            // Convert gRPC request to DTO
            AuthenticationRequest authRequest = new AuthenticationRequest(
                    request.getEmail(),
                    request.getPassword()
            );
            
            // Authenticate
            AuthenticationResponse authResponse = authService.authenticate(authRequest);
            
            // Convert to gRPC response
            User user = User.newBuilder()
                    .setId(authResponse.getId())
                    .setEmail(authResponse.getEmail())
                    .addAllRoles(authResponse.getRoles())
                    .build();
                    
            LoginResponse response = LoginResponse.newBuilder()
                    .setSuccess(true)
                    .setToken(authResponse.getToken())
                    .setUser(user)
                    .build();
                    
            responseObserver.onNext(response);
            responseObserver.onCompleted();
            
        } catch (BadCredentialsException e) {
            LoginResponse response = LoginResponse.newBuilder()
                    .setSuccess(false)
                    .setMessage("Invalid credentials")
                    .build();
                    
            responseObserver.onNext(response);
            responseObserver.onCompleted();
            
        } catch (Exception e) {
            responseObserver.onError(Status.INTERNAL
                    .withDescription("Internal server error")
                    .withCause(e)
                    .asRuntimeException());
        }
    }
    
    @Override
    public void validateToken(ValidateTokenRequest request, StreamObserver<ValidateTokenResponse> responseObserver) {
        try {
            String token = request.getToken();
            
            // Verify token
            boolean isValid = false;
            User user = null;
            
            try {
                // Extract user info from token
                var claims = jwtService.extractAllClaims(token);
                
                if (!jwtService.isTokenExpired(token)) {
                    isValid = true;
                    
                    // Build user from token claims
                    long userId = Long.parseLong(jwtService.extractUsername(token));
                    String email = claims.get("email", String.class);
                    
                    @SuppressWarnings("unchecked")
                    List<String> roles = (List<String>) claims.get("roles", List.class);
                    
                    user = User.newBuilder()
                            .setId(userId)
                            .setEmail(email)
                            .addAllRoles(roles)
                            .build();
                }
            } catch (Exception e) {
                // Token is invalid
                isValid = false;
            }
            
            ValidateTokenResponse response = ValidateTokenResponse.newBuilder()
                    .setIsValid(isValid)
                    .setUser(user != null ? user : User.getDefaultInstance())
                    .build();
                    
            responseObserver.onNext(response);
            responseObserver.onCompleted();
            
        } catch (Exception e) {
            responseObserver.onError(Status.INTERNAL
                    .withDescription("Internal server error")
                    .withCause(e)
                    .asRuntimeException());
        }
    }
}

8. Spring Security Configuration

Let's set up the Spring Security configuration:

// src/main/java/com/wizdevtech/identityaccess/config/SecurityConfig.java
package com.wizdevtech.identityaccess.config;

import com.wizdevtech.identityaccess.security.JwtAuthenticationFilter;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
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.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {

    private final JwtAuthenticationFilter jwtAuthFilter;
    
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .csrf().disable()
            .authorizeHttpRequests()
            .requestMatchers("/api/auth/**").permitAll()
            .requestMatchers("/health").permitAll()
            .anyRequest().authenticated()
            .and()
            .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            .and()
            .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);
            
        return http.build();
    }
    
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
    
    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
        return config.getAuthenticationManager();
    }
}

9. JWT Authentication Filter

// src/main/java/com/wizdevtech/identityaccess/security/JwtAuthenticationFilter.java
package com.wizdevtech.identityaccess.security;

import com.wizdevtech.identityaccess.service.JwtService;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.lang.NonNull;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    private final JwtService jwtService;
    
    @Override
    protected void doFilterInternal(
            @NonNull HttpServletRequest request,
            @NonNull HttpServletResponse response,
            @NonNull FilterChain filterChain
    ) throws ServletException, IOException {
        final String authHeader = request.getHeader("Authorization");
        
        if (authHeader == null || !authHeader.startsWith("Bearer ")) {
            filterChain.doFilter(request, response);
            return;
        }
        
        try {
            // Extract JWT
            final String jwt = authHeader.substring(7);
            
            // Validate JWT
            if (SecurityContextHolder.getContext().getAuthentication() == null) {
                // Extract all claims
                Map<String, Object> claims = jwtService.extractAllClaims(jwt);
                
                if (!jwtService.isTokenExpired(jwt)) {
                    // Get user ID
                    String userId = jwtService.extractUsername(jwt);
                    
                    // Get roles
                    @SuppressWarnings("unchecked")
                    List<String> roles = (List<String>) claims.get("roles");
                    
                    // Create authorities
                    var authorities = roles.stream()
                            .map(role -> new SimpleGrantedAuthority("ROLE_" + role))
                            .collect(Collectors.toList());
                    
                    // Create authentication token
                    var authToken = new UsernamePasswordAuthenticationToken(
                            userId,
                            null,
                            authorities
                    );
                    
                    authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                    SecurityContextHolder.getContext().setAuthentication(authToken);
                }
            }
            
        } catch (Exception ignored) {
            // JWT is invalid, continue without authentication
        }
        
        filterChain.doFilter(request, response);
    }
}

10. Application Configuration

Lastly, let's set up the application properties:

# src/main/resources/application.yml
spring:
  datasource:
    url: ${POSTGRES_URI:jdbc:postgresql://localhost:5432/identityaccess}
    username: ${POSTGRES_USER:postgres}
    password: ${POSTGRES_PASSWORD:postgres}
    driver-class-name: org.postgresql.Driver
  jpa:
    hibernate:
      ddl-auto: update
    properties:
      hibernate:
        dialect: org.hibernate.dialect.PostgreSQLDialect
        format_sql: true
    show-sql: false

server:
  port: ${HTTP_PORT:8080}
  
grpc:
  port: ${GRPC_PORT:9090}
  
jwt:
  secret: ${JWT_SECRET:your_secure_jwt_secret_key_at_least_32_chars}
  expiration: ${JWT_EXPIRATION:3600000}
  issuer: identity-access
  
encryption:
  key: ${ENCRYPTION_KEY:your_32_char_encryption_key_for_data}

logging:
  level:
    org.springframework.security: INFO
    com.wizdevtech: DEBUG

11. Main Application Class

// src/main/java/com/wizdevtech/identityaccess/IdentityAccessApplication.java
package com.wizdevtech.identityaccess;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;

@SpringBootApplication
@EnableJpaAuditing
public class IdentityAccessApplication {

    public static void main(String[] args) {
        SpringApplication.run(IdentityAccessApplication.class, args);
    }
}

This Java-based implementation will be fully compatible with your existing Java application. It provides the same core features as the original Node.js implementation:

  1. PostgreSQL persistence with encryption
  2. JWT authentication
  3. REST and gRPC interfaces
  4. Field-level encryption for sensitive data

The module is designed to be easily integrated into your existing Java project and can be imported using Maven. Let me know if you need any adjustments or clarification on any part of the implementation!

⚠️ **GitHub.com Fallback** ⚠️