A Spring Boot + Groovy microservice that handles authentication and JWT-based authorization for the RespondlyAI platform.
- Overview
- Tech Stack
- Project Structure
- API Endpoints
- Data Model
- JWT Token
- Error Handling
- Configuration
- Local Development Setup
- Database Migrations
- Running Tests
The Auth Service is responsible for:
- User registration (signup) — restricted to users with the
OWNERrole; email must be a valid@gmail.comaddress. - User login — authenticates via email/password and returns a signed JWT access token.
- Stateless JWT authentication — every protected request must supply a
Bearer <token>in theAuthorizationheader. - Role-based access control — supports three roles:
OWNER,ADMIN, andEMPLOYEE. - Interactive API docs — Swagger UI is available at
http://localhost:8080/swagger-ui/.
| Layer | Technology |
|---|---|
| Language | Groovy 4 (Apache Groovy) |
| Framework | Spring Boot 4.0.3 |
| Security | Spring Security + JJWT 0.12.5 |
| Persistence | Spring Data JPA + PostgreSQL 17 |
| Schema migrations | Flyway |
| Validation | Jakarta Bean Validation |
| API docs | SpringDoc OpenAPI (Swagger UI) |
| Build tool | Gradle (Groovy DSL) |
| Java version | Java 25 |
| Containerisation | Docker Compose (local dev DB) |
src/
└── main/
│ ├── groovy/in/respondlyai/auth/
│ │ ├── AuthApplication.groovy # Spring Boot entry point
│ │ ├── config/
│ │ │ ├── SecurityConfig.groovy # Spring Security configuration (JWT filter, BCrypt, CSRF off, stateless)
│ │ │ └── SwaggerConfig.groovy # OpenAPI / Swagger UI configuration
│ │ ├── controller/
│ │ │ └── AuthController.groovy # REST endpoints: POST /api/auth/signup, POST /api/auth/login
│ │ ├── dto/
│ │ │ ├── LoginRequest.groovy # Login payload (email, password)
│ │ │ ├── SignupRequest.groovy # Signup payload (name, email, password, role)
│ │ │ └── AuthResponse.groovy # Response DTO (token, userId, email, role)
│ │ ├── entity/
│ │ │ ├── User.groovy # JPA entity mapped to `users` table
│ │ │ └── Role.groovy # Enum: OWNER | ADMIN | EMPLOYEE
│ │ ├── exception/
│ │ │ ├── ApiException.groovy # Custom runtime exception with factory helpers
│ │ │ ├── ApiErrorResponse.groovy # Structured JSON error body
│ │ │ ├── ErrorType.groovy # Enum of error categories
│ │ │ └── GlobalExceptionHandler.groovy # @ControllerAdvice — centralised error mapping
│ │ ├── repository/
│ │ │ └── UserRepository.groovy # JpaRepository<User, UUID> with custom finders
│ │ ├── security/jwt/
│ │ │ ├── JwtService.groovy # Token generation, validation, claim extraction
│ │ │ └── JwtAuthenticationFilter.groovy # OncePerRequestFilter — validates Bearer tokens
│ │ └── service/
│ │ ├── AuthService.groovy # Core signup / login business logic
│ │ └── AppUserDetailsService.groovy # UserDetailsService bridge for Spring Security
│ └── resources/
│ ├── application.properties # Base configuration (port, JPA, Flyway)
│ ├── application-local.example.properties # Template for local dev secrets
│ └── db/migration/
│ ├── V1__create_users_table.sql # Creates the `users` table
│ └── V2__rename_organization_column.sql # Renames `organization` → `organization_id`
└── test/
└── groovy/in/respondlyai/auth/
└── AuthApplicationTests.groovy # Spring context load test
Base URL: http://localhost:8080
Registers a new user. Only OWNER role is permitted.
Request body
{
"name": "Jane Doe",
"email": "jane@gmail.com",
"password": "secret123",
"role": "OWNER"
}Responses
| Status | Meaning |
|---|---|
201 Created |
User created; JWT returned in body and Authorization header |
400 Bad Request |
Validation failure (e.g. missing name, weak password) |
403 Forbidden |
Role is not OWNER |
409 Conflict |
Email already registered |
500 Internal Server Error |
Unexpected server error |
Authenticates an existing user.
Request body
{
"email": "jane@gmail.com",
"password": "secret123"
}Responses
| Status | Meaning |
|---|---|
200 OK |
Login successful; JWT returned in body and Authorization header |
400 Bad Request |
Missing/invalid credentials |
401 Unauthorized |
Wrong email or password |
{
"token": "<JWT>",
"userId": "550e8400-e29b-41d4-a716-446655440000",
"email": "jane@gmail.com",
"role": "OWNER"
}The JWT is also returned as a Bearer token in the Authorization response header.
| Column | Type | Notes |
|---|---|---|
uuid |
UUID | Primary key, auto-generated |
user_id |
VARCHAR | Application-level unique identifier |
name |
VARCHAR | Full name, required |
email |
VARCHAR | Unique, required |
password |
VARCHAR | BCrypt-hashed |
is_verified |
BOOLEAN | Default false |
role |
VARCHAR | OWNER / ADMIN / MEMBER |
organization_id |
VARCHAR | Nullable |
created_at |
TIMESTAMP | Set on insert |
updated_at |
TIMESTAMP | Updated on every save |
Tokens are signed with HMAC-SHA256 using a secret key that must be at least 32 bytes (256 bits).
Custom claims embedded in the token:
| Claim | Value |
|---|---|
sub |
User's email address |
userId |
Application user ID |
role |
User role string |
organizationId |
Organization ID (omitted if null) |
Default expiry: 24 hours (86400000 ms).
Refresh-token expiry: 7 days (604800000 ms) — configurable, not yet implemented as a separate endpoint.
All errors follow a consistent JSON structure:
{
"success": false,
"message": "Human-readable error description",
"type": "VALIDATION_ERROR",
"timestamp": "2026-03-01T10:00:00.000Z"
}Error types: VALIDATION_ERROR, AUTH_ERROR, FORBIDDEN, CONFLICT, BAD_REQUEST, INTERNAL_SERVER_ERROR.
spring.application.name=auth-service
server.port=8080
spring.profiles.active=${SPRING_PROFILES_ACTIVE:local}
spring.jpa.hibernate.ddl-auto=validate
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true
spring.jpa.open-in-view=false
spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect
spring.flyway.enabled=true
spring.flyway.locations=classpath:db/migrationspring.datasource.url=jdbc:postgresql://localhost:5432/auth_db
spring.datasource.username=postgres
spring.datasource.password=password
application.security.jwt.secret-key=<at-least-32-char-secret>
application.security.jwt.expiration=86400000
application.security.jwt.refresh-token.expiration=604800000- Java 25+
- Docker & Docker Compose
- Gradle (or use the included
./gradlewwrapper)
-
Clone the repository
git clone https://github.com/RespondlyAI/auth-service-backend-server.git cd auth-service-backend-server -
Start the local PostgreSQL database
docker compose up -d
This starts a PostgreSQL 17 container on port
5432with databaseauth_db. -
Create your local properties file
cp src/main/resources/application-local.example.properties \ src/main/resources/application-local.properties
Edit
application-local.propertiesand fill in your database credentials and a strong JWT secret (at least 32 characters).Generate a secure secret:
openssl rand -base64 32
-
Run the application
./gradlew bootRun
The server starts on
http://localhost:8080. -
Explore the API
Open
http://localhost:8080/swagger-ui/in your browser for interactive API documentation.
Flyway runs automatically on startup. Migration scripts live in src/main/resources/db/migration/:
| File | Description |
|---|---|
V1__create_users_table.sql |
Creates the users table with all columns and constraints |
V2__rename_organization_column.sql |
Renames column organization → organization_id |
./gradlew testThe test suite requires no running database — the context load test excludes DataSourceAutoConfiguration.
.