Authentication
JWT-based stateless authentication with dual token system, 2FA, and fine-grained permissions.
Authentication
Neostra uses a stateless JWT-based authentication system with a dual token architecture. The platform supports multi-tenant login flows, two-factor authentication (TOTP), portal access for end users, and method-level authorization with fine-grained permissions.
Token Types
Core JWT
Standard authentication token with a 180-minute TTL. Issued after tenant selection and used for all authenticated API requests.
Temporary JWT
Short-lived token with a 30-minute TTL. Issued on initial sign-in before a tenant is selected. Only valid for the tenant selection endpoint.
Portal JWT
Scoped token for end-user portal access. Processed by a dedicated PortalTokenFilter on the /api/v1/portal/** filter chain.
Authentication Flow
The login process is a two-step flow. First, the user authenticates with credentials and receives a temporary token along with a list of tenants they belong to. Then, the user selects a tenant to receive a fully scoped Core JWT.
Sign In
Authenticate with credentials
Send email and password to the sign-in endpoint. On success, you receive a temporary JWT and a list of tenants the user belongs to.
Select a tenant
Use the temporary JWT to call the tenant selection endpoint with the desired tenant ID. This returns a fully scoped Core JWT.
Use the Core JWT
Include the Core JWT in the Authorization header for all subsequent API calls.
# Step 1: Sign in
curl -X POST https://api.neostra.io/v1/auth/signin \
-H "Content-Type: application/json" \
-d '{
"email": "user@example.com",
"password": "your-password"
}'
# Step 2: Select tenant
curl -X POST https://api.neostra.io/v1/auth/select-tenant \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <temporary_jwt>" \
-d '{
"tenantId": "tenant-abc-123"
}'
// Step 1: Sign in
const signinResponse = await fetch("https://api.neostra.io/v1/auth/signin", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
email: "user@example.com",
password: "your-password",
}),
});
const { data: signinData } = await signinResponse.json();
// signinData contains: { token, tenants: [{ id, name, logo }] }
// Step 2: Select tenant
const tenantResponse = await fetch("https://api.neostra.io/v1/auth/select-tenant", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${signinData.token}`,
},
body: JSON.stringify({ tenantId: signinData.tenants[0].id }),
});
const { data: session } = await tenantResponse.json();
// session contains: { token, user, tenant }
import requests
# Step 1: Sign in
signin_res = requests.post(
"https://api.neostra.io/v1/auth/signin",
json={"email": "user@example.com", "password": "your-password"},
)
signin_data = signin_res.json()["data"]
# Step 2: Select tenant
tenant_res = requests.post(
"https://api.neostra.io/v1/auth/select-tenant",
headers={"Authorization": f"Bearer {signin_data['token']}"},
json={"tenantId": signin_data["tenants"][0]["id"]},
)
session = tenant_res.json()["data"]
Sign-In Response
{
"success": true,
"message": "Authentication successful",
"data": {
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"tenants": [
{
"id": "tenant-abc-123",
"name": "Acme Corp",
"logo": "https://storage.neostra.io/logos/acme.png"
},
{
"id": "tenant-xyz-456",
"name": "Globex Inc",
"logo": null
}
]
}
}
User Registration
Registration is invite-based. A user receives an invitation token via email and uses it to complete their registration.
Retrieve invitation details
Call the signup endpoint with the token to get the invitation metadata (email, tenant, role).
Complete registration
Submit user details (name, password) to finalize the account.
# Get invitation info
curl -X GET https://api.neostra.io/v1/auth/signup/INVITE_TOKEN
# Complete registration
curl -X PATCH https://api.neostra.io/v1/auth/signup/INVITE_TOKEN \
-H "Content-Type: application/json" \
-d '{
"firstName": "Jane",
"lastName": "Doe",
"password": "SecureP@ssw0rd!"
}'
// Get invitation info
const inviteRes = await fetch(
`https://api.neostra.io/v1/auth/signup/${inviteToken}`
);
const inviteInfo = await inviteRes.json();
// Complete registration
const registerRes = await fetch(
`https://api.neostra.io/v1/auth/signup/${inviteToken}`,
{
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
firstName: "Jane",
lastName: "Doe",
password: "SecureP@ssw0rd!",
}),
}
);
Password Reset
Request password reset
Submit the user's email to initiate a reset. A token is sent via email.
Reset the password
Use the token from the email to set a new password.
# Request reset
curl -X POST https://api.neostra.io/v1/auth/reset-password \
-H "Content-Type: application/json" \
-d '{ "email": "user@example.com" }'
# Complete reset
curl -X PATCH https://api.neostra.io/v1/auth/reset-password/RESET_TOKEN \
-H "Content-Type: application/json" \
-d '{ "newPassword": "NewSecureP@ss!" }'
// Request reset
await fetch("https://api.neostra.io/v1/auth/reset-password", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email: "user@example.com" }),
});
// Complete reset with token from email
await fetch(`https://api.neostra.io/v1/auth/reset-password/${resetToken}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ newPassword: "NewSecureP@ss!" }),
});
Two-Factor Authentication (TOTP)
Neostra supports time-based one-time passwords (TOTP) for an additional layer of security.
Set up 2FA
Call the setup endpoint to receive a QR code and secret key. Scan the QR code with an authenticator app (Google Authenticator, Authy, etc.).
Verify and activate
Submit a TOTP code from the authenticator app to verify the setup. On success, you receive a set of backup codes.
Sign in with 2FA
After enabling 2FA, the sign-in flow will require a TOTP code as an additional parameter.
# Setup 2FA (returns QR code)
curl -X POST https://api.neostra.io/v1/auth/2fa/setup \
-H "Authorization: Bearer <core_jwt>"
# Verify with TOTP code from authenticator app
curl -X POST https://api.neostra.io/v1/auth/2fa/verify \
-H "Authorization: Bearer <core_jwt>" \
-H "Content-Type: application/json" \
-d '{ "code": "123456" }'
// Setup 2FA
const setupRes = await fetch("https://api.neostra.io/v1/auth/2fa/setup", {
method: "POST",
headers: { Authorization: `Bearer ${coreJwt}` },
});
const { data: setup } = await setupRes.json();
// setup contains: { qrCodeUrl, secret }
// Verify with TOTP code
const verifyRes = await fetch("https://api.neostra.io/v1/auth/2fa/verify", {
method: "POST",
headers: {
Authorization: `Bearer ${coreJwt}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ code: "123456" }),
});
const { data: verified } = await verifyRes.json();
// verified contains: { backupCodes: ["XXXX-XXXX", ...] }
Store backup codes in a secure location. They are only shown once and are required to regain access if you lose your authenticator device.
2FA Setup Response
{
"success": true,
"message": "2FA setup initiated",
"data": {
"qrCodeUrl": "data:image/png;base64,iVBORw0KGgo...",
"secret": "JBSWY3DPEHPK3PXP"
}
}
2FA Verify Response
{
"success": true,
"message": "2FA enabled successfully",
"data": {
"backupCodes": [
"A1B2-C3D4",
"E5F6-G7H8",
"I9J0-K1L2",
"M3N4-O5P6",
"Q7R8-S9T0"
]
}
}
Portal Authentication
The end-user portal uses a separate authentication filter chain with its own token processing.
Portal JWTs are scoped to specific subject request operations. They do not carry the full role and permission set of Core JWTs.
Security Configuration
The platform uses Spring Security with the following configuration:
Stateless Sessions
No server-side session storage. All auth state is encoded in the JWT. CSRF protection is disabled in favor of token-based auth.
Password Hashing
All passwords are hashed using BCryptPasswordEncoder before storage. Raw passwords are never persisted.
CORS
Cross-origin requests are configured via the cors.origins application property. Only explicitly allowed origins can call the API.
Dual Filter Chains
Two ordered filter chains process requests: Portal (Order 1) handles /api/v1/portal/**; Core (Order 2) handles everything else.
Authorization and Permissions
Authorization is enforced at the method level using @PreAuthorize annotations with fine-grained permissions.
Permission Format
Permissions follow a resource:action pattern:
| Permission | Description |
|---|---|
tenant:view | View tenant details |
tenant:edit | Modify tenant settings |
users:create | Invite new users |
users:edit | Update user profiles and roles |
assessments:create | Create new assessments |
assessments:view | View assessments |
subject-requests:create | Submit data subject requests |
subject-requests:view | View data subject requests |
consent:view | View consent records |
consent:manage | Manage consent configurations |
UserDetailsImpl
The authenticated user principal (UserDetailsImpl) carries the following information:
public class UserDetailsImpl {
String userId;
String email;
String tenantId;
List<String> roles;
List<String> permissions;
List<BrandAccess> brandAccess; // brands, processes, sub-processes
}
Example: Checking Permissions
@PreAuthorize("hasAuthority('assessments:create')")
@PostMapping("/api/v1/assessments")
public ResponseEntity<ServiceResponse<Assessment>> createAssessment(
@RequestBody AssessmentRequest request
) {
// Only users with assessments:create permission reach this method
}
Token Lifecycle
Tokens are not refreshable. When a Core JWT expires after 180 minutes, the user must re-authenticate through the full sign-in flow.
Last updated 1 week ago
Built with Documentation.AI