Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ batch-app/markers/*
/.idea/
.DS_Store
**/.DS_Store
**/target/
86 changes: 86 additions & 0 deletions services/carddemo-auth/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# CardDemo Auth Service

Identity & Access Management microservice for the CardDemo system, migrated from legacy COBOL programs to Spring Boot 3.x.

## Legacy COBOL Mapping

| COBOL Program | Function | Java Equivalent |
|---|---|---|
| `COSGN00C` | User sign-on (authentication) | `POST /auth/login` — validates credentials, returns JWT |
| `COUSR00C` | List users | `GET /users` — returns all users (admin) or own profile (user) |
| `COUSR01C` | Add user | `POST /users` — creates a new user (admin only) |
| `COUSR02C` | Update user | `PUT /users/{id}` — updates user fields |
| `COUSR03C` | Delete user | `DELETE /users/{id}` — removes a user (admin only) |

### Data Model Mapping (Copybook `CSUSR01Y`)

| COBOL Field | PIC | Java Field | Type | Notes |
|---|---|---|---|---|
| `SEC-USR-ID` | `X(08)` | `userId` | `String(8)` | Primary key |
| `SEC-USR-FNAME` | `X(20)` | `firstName` | `String(20)` | |
| `SEC-USR-LNAME` | `X(20)` | `lastName` | `String(20)` | |
| `SEC-USR-PWD` | `X(08)` | `password` | `String` | BCrypt hash (replaces plaintext COBOL storage) |
| `SEC-USR-TYPE` | `X(01)` | `userType` | `Enum(ADMIN,USER)` | 'A' → ADMIN, 'U' → USER |
| `SEC-USR-FILLER` | `X(23)` | — | — | Not migrated (padding) |

## Architecture

- **Framework**: Spring Boot 3.2.x
- **Security**: Spring Security + JWT (stateless)
- **Persistence**: Spring Data JPA with PostgreSQL (H2 for tests)
- **Password Storage**: BCrypt (replacing COBOL plaintext `PIC X(08)`)

## API Endpoints

### Authentication
```
POST /auth/login
Body: { "userId": "ADMIN01", "password": "secret" }
Response: { "token": "eyJ...", "userId": "ADMIN01", "userType": "ADMIN" }
```

### User Management (requires Bearer token)
```
GET /users — List users (admin: all, user: own profile only)
POST /users — Create user (admin only)
PUT /users/{id} — Update user (admin: any, user: own profile only)
DELETE /users/{id} — Delete user (admin only)
```

## Role-Based Access Control

| Role | GET /users | POST /users | PUT /users/{id} | DELETE /users/{id} |
|---|---|---|---|---|
| ADMIN | All users | Yes | Any user | Yes |
| USER | Own profile only | No | Own profile only (cannot change role) | No |

## Running Locally

### Prerequisites
- Java 17+
- Maven 3.8+
- PostgreSQL 14+ (or use the H2 test profile)

### Build & Test
```bash
cd services/carddemo-auth
mvn clean package
```

### Run
```bash
# Set environment variables for PostgreSQL connection
export DB_USERNAME=carddemo
export DB_PASSWORD=carddemo
export JWT_SECRET=your-256-bit-secret-key-here

mvn spring-boot:run
```

## Security Improvements Over Legacy

1. **Password Hashing**: BCrypt replaces plaintext COBOL `PIC X(08)` passwords
2. **Stateless Auth**: JWT tokens replace CICS session-based security
3. **Role Enforcement**: Programmatic RBAC replaces RACF group checks
4. **Input Validation**: Bean Validation replaces manual COBOL field checks
5. **Audit-Ready**: Structured JSON responses replace BMS screen output
98 changes: 98 additions & 0 deletions services/carddemo-auth/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.5</version>
<relativePath/>
</parent>

<groupId>com.cardemo</groupId>
<artifactId>carddemo-auth</artifactId>
<version>1.0.0-SNAPSHOT</version>
<name>CardDemo Auth Service</name>
<description>Identity and Access Management service for CardDemo - migrated from COBOL COSGN00C/COUSR* programs</description>

<properties>
<java.version>17</java.version>
<jjwt.version>0.12.5</jjwt.version>
</properties>

<dependencies>
<!-- Spring Boot Starters -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>

<!-- JWT -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>${jjwt.version}</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>${jjwt.version}</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>${jjwt.version}</version>
<scope>runtime</scope>
</dependency>

<!-- PostgreSQL Driver -->
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>

<!-- H2 for testing -->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>test</scope>
</dependency>

<!-- Test dependencies -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.cardemo.auth;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class CardDemoAuthApplication {

public static void main(String[] args) {
SpringApplication.run(CardDemoAuthApplication.class, args);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package com.cardemo.auth.config;

import com.cardemo.auth.security.JwtAuthenticationFilter;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
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
@EnableMethodSecurity
public class SecurityConfig {

private final JwtAuthenticationFilter jwtAuthenticationFilter;

public SecurityConfig(JwtAuthenticationFilter jwtAuthenticationFilter) {
this.jwtAuthenticationFilter = jwtAuthenticationFilter;
}

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.exceptionHandling(ex -> ex
.authenticationEntryPoint((request, response, authException) ->
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized"))
)
.authorizeHttpRequests(auth -> auth
.requestMatchers(HttpMethod.POST, "/auth/login").permitAll()

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚩 No brute-force / rate-limiting protection on the login endpoint

The POST /auth/login endpoint is permitAll() (line 38 of SecurityConfig) with no rate limiting or account lockout mechanism. While this isn't strictly a code bug (it's a missing feature), it's worth flagging for a security-focused auth service: an attacker can make unlimited login attempts. The original COBOL COSGN00C program typically had lockout after N failed attempts. Consider adding Spring's rate limiting, a lockout counter, or delegating to an API gateway rate limiter.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Acknowledged — rate limiting / account lockout is a valid hardening concern. The original COBOL COSGN00C did enforce lockout after failed attempts. This can be added in a follow-up (e.g., a failed-attempt counter in the users table or an API gateway rate limiter like Bucket4j or Spring Cloud Gateway filters). Keeping this PR scoped to the core auth functionality for now.

.anyRequest().authenticated()
)
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);

return http.build();
}

@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.cardemo.auth.controller;

import com.cardemo.auth.dto.LoginRequest;
import com.cardemo.auth.dto.LoginResponse;
import com.cardemo.auth.service.AuthService;
import jakarta.validation.Valid;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

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

private final AuthService authService;

public AuthController(AuthService authService) {
this.authService = authService;
}

@PostMapping("/login")
public ResponseEntity<LoginResponse> login(@Valid @RequestBody LoginRequest request) {
LoginResponse response = authService.authenticate(request);
return ResponseEntity.ok(response);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package com.cardemo.auth.controller;

import com.cardemo.auth.service.AuthService.AuthenticationException;
import com.cardemo.auth.service.UserService.UserAlreadyExistsException;
import com.cardemo.auth.service.UserService.UserNotFoundException;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import java.util.Map;

@RestControllerAdvice
public class GlobalExceptionHandler {

@ExceptionHandler(AuthenticationException.class)
public ResponseEntity<Map<String, String>> handleAuthenticationException(AuthenticationException ex) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
.body(Map.of("error", ex.getMessage()));
}

@ExceptionHandler(UserNotFoundException.class)
public ResponseEntity<Map<String, String>> handleUserNotFound(UserNotFoundException ex) {
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(Map.of("error", ex.getMessage()));
}

@ExceptionHandler(UserAlreadyExistsException.class)
public ResponseEntity<Map<String, String>> handleUserAlreadyExists(UserAlreadyExistsException ex) {
return ResponseEntity.status(HttpStatus.CONFLICT)
.body(Map.of("error", ex.getMessage()));
}

@ExceptionHandler(DataIntegrityViolationException.class)
public ResponseEntity<Map<String, String>> handleDataIntegrity(DataIntegrityViolationException ex) {
return ResponseEntity.status(HttpStatus.CONFLICT)
.body(Map.of("error", "Resource conflict"));
}

@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Map<String, String>> handleValidation(MethodArgumentNotValidException ex) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(Map.of("error", "Validation failed: " + (ex.getFieldError() != null ? ex.getFieldError().getDefaultMessage() : ex.getMessage())));
}
}
Loading