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:
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;
}
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);
}
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);
}
}
}
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());
}
}
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);
}
}
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);
}
}
}
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());
}
}
}
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();
}
}
// 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);
}
}
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
// 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:
- PostgreSQL persistence with encryption
- JWT authentication
- REST and gRPC interfaces
- 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!