Purpose
security-auth-sample shows how an application team should consume the Chenile auth framework without modifying framework code.
Module path:
/ajapro/chenile-samples/security-auth-sample
Framework path:
/ajapro/chenile-security/auth-framework
Framework guide:
Chenile Security Auth Framework
Module Layout
auth-implementation: application-owned JPA and Liquibase implementation of Chenile auth contracts.auth-server-app: runnable Spring Boot auth server assembled fromchenile-security-auth-server.gateway-app: runnable Spring Boot gateway assembled fromchenile-security-gateway.service-a: protected resource service that validates JWTs and calls service B.service-b: protected resource service with tenant/ACL checks.demo-ui: React UI for login, token acquisition, and service calls.runtime: Docker Compose, gateway route config, and image build assets.
Dependency Pattern
Applications should depend on Chenile artifacts and keep their own persistence and business rules outside the framework.
<dependency>
<groupId>org.chenile</groupId>
<artifactId>chenile-security-auth-server</artifactId>
</dependency>
<dependency>
<groupId>org.chenile</groupId>
<artifactId>chenile-security-gateway</artifactId>
</dependency>
<dependency>
<groupId>org.chenile</groupId>
<artifactId>chenile-security-starter-resource-server</artifactId>
</dependency>
Implementation Layers
- Framework contracts live in
chenile-security-auth-core. - Token and login flows live in
chenile-security-auth-server. - Gateway route, relay header, and token validation behavior live in
chenile-security-gateway. - Application tenant, user, client, provider, schema, and seed data live in
auth-implementation. - Service authorization rules remain local to each resource service.
Auth Implementation Module
auth-implementation is the most important module for application developers. It demonstrates how to keep business-owned identity state outside the framework.
It provides:
- JPA entities for realms, users, clients, auth providers, tenant MFA policy, and MFA challenges.
- Spring Data repositories for provider lookup, active user lookup, tenant policy lookup, and challenge lifecycle.
- Liquibase changelogs for schema and seed data.
- Implementations of framework contracts such as tenant registry, provider config lookup, MFA policy, and MFA challenge handling.
- A mock external MFA provider that shows how a third-party provider can be plugged in without changing framework code.
The production rule is simple: copy the contract implementation pattern, not the sample seed data.
Database Model
The sample uses Postgres and Liquibase. The MFA change set is:
auth-implementation/src/main/resources/db/changelog/003-mfa.sql
It adds two tables:
tenant_mfa_policy: tenant-level decision table that says whether a realm requires MFA and which provider should be used.mfa_challenge: durable challenge table that stores challenge id, user, primary provider, client id, MFA provider, status, attempts, creation time, and expiry.
The seeded behavior is:
tenant-alpha: MFA enabled usingemail-otp; sample code is246810.tenant-beta: MFA disabled; password login returns tokens immediately.platform: MFA enabled for admin-style flows usingadmin-otp.
In a real deployment, do not seed deterministic OTP codes. Use a provider that generates, sends, and verifies one-time codes or delegates to a trusted MFA service.
Tenant MFA Flow In The Sample
For tenant-alpha, login is a two-step flow.
First authenticate the primary credential:
curl -s http://localhost:9000/api/login/authenticate \
-H 'content-type: application/json' \
-d '{"email":"alice@tenant-alpha.example","providerId":1,"credential":"password1!"}'
The response contains nextStep: "mfa" and a challengeId. Then verify the challenge:
curl -s http://localhost:9000/api/login/mfa/verify \
-H 'content-type: application/json' \
-d '{"challengeId":"<challenge-id>","code":"246810"}'
The verified response contains access and ID tokens. The token claims include mfa=true, amr, mfa_provider, and mfa_provider_type.
For tenant-beta, the same primary authentication returns tokens directly because the tenant policy disables MFA.
UI Flow
demo-ui now understands both token responses and MFA challenge responses.
The UI flow is:
- Identify the user by email.
- Select or auto-select an auth provider.
- Submit the primary credential or start Google login.
- If the response contains
nextStep=mfa, show the second-factor form. - Submit
challengeIdand MFA code to/api/login/mfa/verify. - Store the final token only after successful MFA verification.
- Use the token to call gateway and service endpoints.
Google login can also require MFA. In that case the auth server redirects back to the UI with hash values containing next_step=mfa and challenge_id.
Gateway And Service Validation
The gateway validates the token before routing requests. Resource services still own endpoint-level authorization.
The sample services demonstrate:
- unauthenticated request rejection
- tenant header mismatch rejection
- ACL or scope based rejection
- valid tenant token accepted by the correct service endpoint
If a real service requires MFA, check the token claims after JWT validation. For example, require mfa=true and confirm amr contains the expected second factor such as otp.
Extending MFA
There are two common extension paths.
Use database-backed OTP when the application owns the provider secret:
- Add or provision a user auth provider row.
- Enable
tenant_mfa_policyfor the tenant. - Set
provider_keyto the user provider key. - Verify the submitted code against the stored provider secret or generated challenge secret.
Use third-party MFA when another system owns the factor:
- Implement
org.chenile.security.auth.framework.contract.MfaProvider. - Return a stable
providerKey()andproviderType(). - Generate a safe destination hint.
- Delegate
verify(...)to the third-party provider. - Point
tenant_mfa_policy.provider_keyandprovider_typeto that provider.
The sample includes MockExternalMfaProvider only as a local reference. Replace it with a real email, SMS, push, or enterprise MFA integration in production.
Configuration Namespaces
Framework-owned properties use:
chenile:
security:
auth-server:
gateway:
jwt:
Sample-owned application properties use:
sample:
security:
This separation is intentional. Teams should not add application-specific properties under chenile.security.* unless they are extending the framework contract.
External Runtime Configuration
The sample deliberately does not package src/main/resources/application.yml or src/main/resources/application.yaml in runnable modules. This is the production pattern: application jars should not carry local ports, database URLs, issuer URLs, demo hints, or environment-specific route definitions.
Runtime configuration lives under:
security-auth-sample/runtime/config/
The files are:
auth-server-config.yml: auth-server port, issuer, JPA/Liquibase, token audiences, and demo metadata.gateway-config.yml: gateway port, issuer/JWK URLs, relay headers, and backend routes.service-a-config.yml: service A port, service B URL, issuer, and JWK endpoint.service-b-config.yml: service B port, issuer, and JWK endpoint.
Docker Compose mounts these files and sets:
SPRING_CONFIG_ADDITIONAL_LOCATION=file:/config/<module-config>.yml
For Kubernetes, use the same model with ConfigMaps for non-secret config and Secrets for database passwords, OAuth client secrets, signing material, and third-party MFA credentials.
For local command-line runs, pass the external config explicitly:
java -jar auth-server-app/target/*.jar \
--spring.config.additional-location=file:runtime/config/auth-server-config.yml
Run Locally
Install the framework and run the sample:
cd /ajapro/chenile-samples/security-auth-sample
./run.sh
The script builds local chenile-security artifacts, packages the sample modules, and starts Docker Compose.
The Compose stack also mounts the external config files. If a service starts on the wrong port, first verify the module has no packaged application.yaml and that the correct SPRING_CONFIG_ADDITIONAL_LOCATION value is present.
Local endpoints:
- Auth server:
http://localhost:9000 - Gateway:
http://localhost:8080 - Service A:
http://localhost:8081 - Service B:
http://localhost:8082 - Postgres:
localhost:15432
Validate Locally
Backend tests:
cd /ajapro/chenile-samples/security-auth-sample
mvn test
Targeted auth-server integration tests:
mvn -pl auth-server-app -am test
UI build:
cd /ajapro/chenile-samples/security-auth-sample/demo-ui
npm install
npm run build
The auth-server integration test starts Postgres through Testcontainers and applies the real Liquibase changelogs. That validates the tenant MFA policy and challenge tables against a real database.
UI
The UI is in:
security-auth-sample/demo-ui
Run it with:
npm install
npm run dev
The UI is intentionally separate from backend packaging so teams can replace it with their own frontend while preserving the same auth server and gateway APIs.
What To Copy Into A Real Application
Copy the pattern, not the framework internals:
- create an app-owned implementation module for
TenantRegistryand provider lookup - implement
MfaPolicyServiceandMfaChallengeServiceif tenant-level MFA is required - implement
MfaProviderfor external email, SMS, push, or enterprise MFA integrations - create a small auth-server boot app that imports the implementation module
- create a small gateway boot app with route config
- keep resource-service JWT validation and authorization rules inside each service
- seed local/dev data through Liquibase or your production migration tool
Do not copy or modify chenile-security/auth-framework into an application repository.
Production Checklist
- Store MFA challenges in a durable database so auth-server pods can restart or scale horizontally.
- Enforce challenge expiry and retry limits.
- Do not expose OTP secrets through logs, UI hints, or token claims.
- Keep tenant policy changes auditable.
- Use different provider keys for different MFA mechanisms.
- Validate the
mfaandamrclaims in services that need step-up authentication. - Keep sample deterministic credentials out of production data.