Purpose
The Chenile security auth framework provides reusable building blocks for an auth server, gateway, and resource-server integration. It is designed so the framework owns protocol flow and token generation, while each application owns its tenant, user, client, provider, MFA, and authorization data.
Framework path:
/ajapro/chenile-security/auth-framework
Reference implementation path:
/ajapro/chenile-samples/security-auth-sample
Module Model
The framework is separated from the existing Chenile security implementation so users can adopt it without forcing a migration of legacy security code.
core: shared contracts such asTenantRegistry,ExternalProviderService,MfaPolicyService,MfaChallengeService, andMfaProvider.auth-server: login APIs, OAuth-style token issuing, Google callback handling, MFA challenge/verify flow, and/api/service/me.gateway: gateway validation and relay behavior for routing secured requests to backend services.starters: Spring Boot starter modules that assemble framework beans with sensible defaults.
Applications should depend on the framework artifacts and provide implementation beans. They should not fork framework code for tenant-specific rules.
Responsibility Split
The framework owns:
- login orchestration
- token creation and verification
- public API shape for auth server flows
- optional MFA challenge hand-off
- gateway token validation and request relay
- resource-server integration contracts
The application owns:
- tenant and realm storage
- user identity storage
- client registrations
- provider registrations and secrets
- MFA policy storage
- MFA challenge persistence
- external MFA provider integration
- service-level authorization rules
This split keeps framework code reusable and lets applications use JDBC, JPA, LDAP, remote IAM, third-party MFA, or a mixed model behind the same contracts.
Core Authentication Flow
The browser or API client first identifies the user by email:
POST /api/login/identify
Content-Type: application/json
{"email":"alice@tenant-alpha.example"}
The auth server returns the tenant and available providers for that user. If a password or OTP provider is selected, the client submits:
POST /api/login/authenticate
Content-Type: application/json
{
"email": "alice@tenant-alpha.example",
"providerId": 1,
"credential": "password1!"
}
If no second factor is required, the response contains tokens. If the tenant policy requires MFA, the response contains nextStep: mfa, a challengeId, display metadata, and expiry information. A token is not issued until the challenge is verified.
Tenant-Level MFA Flow
MFA is optional and contract driven. The framework checks for MfaPolicyService and MfaChallengeService beans. If they are absent, login remains password/provider-only.
When present, the framework executes this sequence:
- Primary provider authentication succeeds.
MfaPolicyService.evaluate(...)decides whether the tenant/client/provider combination needs a second factor.- If MFA is not required, the framework issues a token with
mfa=false. - If MFA is required,
MfaChallengeService.start(...)creates a challenge and returns challenge metadata to the client. - The client calls
/api/login/mfa/verifywithchallengeIdandcode. MfaChallengeService.verify(...)validates the code and returns the original primary provider context.- The framework issues the final token with MFA claims.
Verification endpoint:
POST /api/login/mfa/verify
Content-Type: application/json
{
"challengeId": "generated-challenge-id",
"code": "246810"
}
MFA Contracts
MfaPolicyService is the policy decision point. A typical implementation reads tenant policy from a database and returns either MfaPolicy.notRequired() or a required policy with provider metadata.
Important policy fields:
required: whether a second factor is mandatory.providerKey: application/provider identifier such asemail-otp,sms-otp, ormock-external-mfa.providerType: provider category such asOTP,EMAIL_OTP,SMS_OTP,PUSH, orEXTERNAL.displayName: user-facing label.destinationHint: safe hint for UI display.challengeTtl: expiry duration for a challenge.allowedProviderTypes: types that the policy allows.
MfaChallengeService owns durable challenge lifecycle. In production it should persist challenge state, expiry, attempt count, and verified/failed status.
MfaProvider is optional and useful for third-party or non-database verification. If an application registers an MfaProvider, the challenge service can delegate code verification to that provider instead of checking a locally stored secret.
Token Claims
The framework adds authentication-method claims so gateways and services can make authorization decisions based on MFA strength.
auth_provider: provider key used for the primary login.auth_provider_type: primary provider type.mfa:trueonly when a second factor was verified.amr: authentication methods used, for example["pwd"],["pwd","otp"], or["google","otp"].mfa_provider: provider key used for MFA.mfa_provider_type: provider type used for MFA.
Resource services should not re-run MFA. They should validate the JWT and inspect these claims when an endpoint requires a stronger authentication method.
External Providers
Google login remains a first-factor provider. A successful Google callback can still be followed by tenant-level MFA. If MFA is required after Google login, the auth server redirects the UI with a hash containing next_step=mfa and challenge_id.
The same extension model can support third-party MFA:
- Add a tenant policy row pointing to an external provider key.
- Register a Spring bean implementing
MfaProvider. - Persist the challenge through
MfaChallengeService. - Delegate verification to the provider.
The framework does not prescribe vendor APIs, SMS providers, email providers, or push notification providers.
Production Guidance
Use durable storage for challenges. In-memory challenges are not safe when the auth server runs more than one pod or restarts during login.
Expire challenges aggressively and enforce attempt limits. The sample uses pending, verified, and failed status values with a retry cap.
Keep tenant policy separate from user provider registration. A tenant policy decides whether MFA is required; a user/provider registration or external MFA provider decides whether the user can satisfy that factor.
Do not put sensitive OTP values in logs, UI hints, or token claims. Sample seed data uses known values only to make local development and tests deterministic.
For high-risk services, check both mfa=true and amr contents instead of trusting only that a token exists.
Do not package environment-specific application.yml or application.yaml files inside framework or application jars. Framework modules should provide code, contracts, auto-configuration, and defaults inside typed configuration classes only. Applications should provide runtime values through environment variables, mounted config files, ConfigMaps, Secrets, or another deployment-time configuration source.
Recommended production configuration pattern:
- Put non-secret runtime config in deployment-managed files, for example gateway routes, issuer URLs, JWK URLs, service ports, and audience maps.
- Put secrets in a secret manager or Kubernetes Secret, for example datasource passwords, OAuth client secrets, signing keys, and third-party MFA credentials.
- Load mounted config with
SPRING_CONFIG_ADDITIONAL_LOCATION. - Keep tests deterministic with
@SpringBootTest(properties = ...),@DynamicPropertySource, orsrc/test/resources/application.yaml; test resources are not production runtime defaults.
This keeps the framework reusable and prevents one sample application’s local defaults from becoming hidden behavior for every adopter.
Testing Strategy
Framework tests should cover the contract behavior without tying to one database schema:
- password login without MFA returns a token with
mfa=false - password login with MFA returns a challenge and no token
- MFA verify returns a token with
mfa=trueand the expectedamr - invalid MFA code returns
401 - non-password provider types preserve the correct primary
amr
Application tests should cover the real persistence and migration path:
- Liquibase creates policy and challenge tables
- tenant A requires MFA and tenant B does not
- expired or failed challenges cannot be reused
- services can read MFA claims from
/api/service/meor validated JWTs