π§βπ» Olara User Service
Welcome to the Olara User Service repository! This service is a core part of the Olara platform, responsible for user, role, and permission management, authentication, and integration with Keycloak for identity management.
π Recent Updates
- Social Login: OAuth 2.0 authentication with Google, Facebook, Instagram, Twitter, and Apple via Keycloak identity brokering
- Admin User Management: New administrative endpoints for cross-tenant user management
- Enhanced User Filtering: Improved user querying capabilities with tenant-based filtering
- Passkey Authentication: Complete WebAuthn/FIDO2 passkey authentication system with pagination support
- Security Enhancements: Role-based access control for administrative operations
π¦ Table of Contents
- Features
- Architecture
- Tech Stack
- Getting Started
- Prerequisites
- Configuration
- Running Locally
- Docker Compose
- Database & Migrations
- Key Modules
- Entities
- DTOs
- Repositories
- Services
- Keycloak Integration
- Social Login Integration
- Mobile Firebase Social Login
- Referral System
- Admin User Management
- Eureka Service Discovery
- Monitoring & Observability
- Testing
- Biometric Device Authentication Flow
- Passkey Authentication Flow
- Biometric Device API Field Reference
- Multi-Language Support
- Complete API Endpoints Reference
- Visual Flow: Biometric Authentication
- Example Usage Scenarios
- Troubleshooting
- License
π Features
- User Management: CRUD operations for users, including registration, authentication, and password reset.
- Admin User Management: Administrative operations for user management across tenants with elevated privileges.
- Social Login: OAuth 2.0 authentication with Google, Facebook, Instagram, Twitter, and Apple via Keycloak identity brokering.
- Role & Permission Management: Fine-grained access control with roles and permissions, including dynamic role assignment.
- Passkey Authentication: WebAuthn/FIDO2 passkey authentication for passwordless login with paginated passkey management.
- Biometric Device Authentication: Advanced biometric authentication with device management.
- Referral System: Complete referral tracking with referral codes, referral relationships, and paginated referral queries.
- Multi-Tenancy: Tenant-aware data isolation.
- Keycloak Integration: Centralized identity and access management with role synchronization.
- Eureka Discovery: Service registration and discovery for microservices.
- Flyway Migrations: Automated database schema management.
- Observability: Health checks, metrics, and distributed tracing.
- Dockerized: Ready for containerized deployment.
ποΈ Architecture
graph TD
A[API Gateway] -->|REST| B(User Service)
B -->|JPA| C[(PostgreSQL)]
B -->|Keycloak Admin Client| D[Keycloak]
B -->|Eureka Client| E[Eureka Server]
B -->|Redis| F[Redis]
B -->|RabbitMQ| G[RabbitMQ]
B -->|Loki| H[Loki]
B -->|Elastic APM| I[ElasticSearch]
π οΈ Tech Stack
- Java 17
- Spring Boot 3
- Spring Data JPA
- Spring Cloud (Eureka)
- Keycloak Admin Client
- YubiKey WebAuthn (Passkey/FIDO2 Authentication)
- PostgreSQL
- Flyway
- Redis
- RabbitMQ
- Docker & Docker Compose
- Loki, Elastic APM (Observability)
π Getting Started
Prerequisites
- Java 17+
- Docker & Docker Compose
- PostgreSQL (if running locally)
- Keycloak (if running locally)
Configuration
Configuration is managed via:
- application.yml (local/dev)
- application-uat.yml (UAT)
- Spring Cloud Config Server (for distributed config)
Key settings:
- Database: spring.datasource.url, spring.datasource.username, spring.datasource.password
- Keycloak: keycloak.auth-server-url, keycloak.realm, keycloak.resource, keycloak.credentials.secret
- Eureka: eureka.client.serviceUrl.defaultZone
Running Locally
./gradlew clean build
java -jar build/libs/user-service-java-0.0.1-SNAPSHOT.jar
Docker Compose
Spin up the full stack (Postgres, Keycloak, Eureka, Redis, RabbitMQ, Loki, ElasticSearch):
docker-compose -f docker-compose-uat.yml up --build
π Keycloak Integration
- Uses
keycloak-admin-clientfor programmatic user and realm management. - Configuration in
KeycloakConfig.java. - Service logic in
KeycloakService.java. - Supports user creation, authentication, and password reset via Keycloak.
JWT Token Structure
When users authenticate, they receive JWT tokens that contain their roles and permissions. The token structure includes:
{
"sub": "user-uuid",
"preferred_username": "user@example.com",
"email": "user@example.com",
"realm_access": {
"roles": [
"user",
"manager",
"PERMISSION_VIEW_USER",
"PERMISSION_EDIT_USER"
]
},
"resource_access": {
"tenant-client": {
"roles": [
"tenant-role-1",
"tenant-role-2"
]
}
},
"tenantId": "tenant-uuid",
"userId": "user-uuid",
"exp": 1719500000,
"iat": 1719496400
}
Role Representation in Tokens:
- Roles and Permissions are represented as strings in the realm_access.roles array
- Role Assignment/Removal automatically updates what appears in subsequent tokens
- Permissions are added as composite roles, so they appear alongside regular roles
- Client-specific roles appear under resource_access.{client-id}.roles
Role and Permission Synchronization
When roles are assigned or removed via the API: 1. Local Database is updated immediately 2. Keycloak role assignments are synchronized 3. New tokens issued after the change will reflect the updated roles 4. Existing tokens remain valid until expiration with their original role set
π Social Login Integration
The user service supports OAuth 2.0 social login through Keycloak's identity brokering capabilities, allowing users to authenticate using popular social media platforms.
Supported Providers
- Google - OAuth 2.0 with Google Sign-In
- Facebook - Facebook Login API
- Instagram - Instagram Basic Display API
- Twitter - Twitter OAuth 2.0
- Apple - Sign in with Apple
How Social Login Works
- Client Request: Frontend requests social login URL for a specific provider
- Keycloak Redirect: User is redirected to the social provider's OAuth page
- Provider Authentication: User authenticates with the social provider
- Callback: Social provider redirects back to Keycloak with authorization code
- Token Exchange: Keycloak exchanges code for user info and creates/updates user
- JWT Issuance: Keycloak issues JWT token with user information
- User Creation: If new user, local database record is created
API Endpoints
Get Social Login URL
POST /api/v1/auth/social-login/url
Request:
{
"provider": "google"
}
Response:
{
"timestamp": "2025-09-07T10:00:00.000Z",
"success": true,
"message": "Operation successful",
"data": {
"authorizationUrl": "http://localhost:8090/realms/olara-realm/protocol/openid-connect/auth?client_id=user-service&response_type=code&scope=openid profile email&redirect_uri=http://localhost:8080/auth/social/callback&kc_idp_hint=google"
},
"metadata": {
"processingTime": "15ms",
"serverId": "user-service-01",
"version": "1.0.0"
}
}
Complete Social Login
POST /api/v1/auth/social-login
Request:
{
"provider": "google",
"code": "authorization_code_from_provider",
"redirectUri": "http://localhost:8080/auth/social/callback",
"state": "optional_state_parameter"
}
Response:
{
"timestamp": "2025-09-07T10:00:00.000Z",
"success": true,
"message": "Social login successful",
"data": {
"user": {
"id": "user-uuid",
"tenantId": "tenant-code",
"username": "user@gmail.com",
"email": "user@gmail.com",
"phone": null,
"firstName": "John",
"lastName": "Doe",
"middleName": null,
"imageUrl": "https://lh3.googleusercontent.com/photo.jpg",
"dateOfBirth": null,
"address": null,
"emailVerified": true,
"phoneVerified": false,
"status": "ACTIVE",
"roles": [],
"lastLoginAt": null,
"created": "2025-09-07T10:00:00.000Z",
"updated": "2025-09-07T10:00:00.000Z",
"createdBy": "social-login",
"updatedBy": "social-login",
"kycStatus": null,
"failedLoginAttempts": 0,
"referralCode": "ABC123",
"referredBy": null
},
"accessToken": "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUI...",
"refreshToken": "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUI...",
"expiresIn": 3600,
"refreshExpiresIn": 2592000
},
"metadata": {
"processingTime": "120ms",
"serverId": "user-service-01",
"version": "1.0.0"
}
}
Configuration
Social login providers are configured in application-local.yml:
social:
providers:
google:
client-id: ${GOOGLE_CLIENT_ID:your-google-client-id}
client-secret: ${GOOGLE_CLIENT_SECRET:your-google-client-secret}
redirect-uri: ${GOOGLE_REDIRECT_URI:http://localhost:8080/auth/social/callback}
authorization-uri: https://accounts.google.com/o/oauth2/auth
token-uri: https://oauth2.googleapis.com/token
user-info-uri: https://www.googleapis.com/oauth2/v2/userinfo
scope: openid,profile,email
facebook:
client-id: ${FACEBOOK_CLIENT_ID:your-facebook-client-id}
client-secret: ${FACEBOOK_CLIENT_SECRET:your-facebook-client-secret}
redirect-uri: ${FACEBOOK_REDIRECT_URI:http://localhost:8080/auth/social/callback}
authorization-uri: https://www.facebook.com/v12.0/dialog/oauth
token-uri: https://graph.facebook.com/v12.0/oauth/access_token
user-info-uri: https://graph.facebook.com/me?fields=id,name,email,first_name,last_name,picture
scope: email,public_profile
# ... other providers
Keycloak Identity Provider Setup
To enable social login, configure identity providers in Keycloak:
- Access Keycloak Admin Console
- Navigate to your realm (e.g.,
olara-realm) - Go to Identity Providers section
- Add Identity Provider for each social platform:
- Google: Use OpenID Connect v1.0
- Facebook: Use Facebook
- Twitter: Use Twitter
- Instagram: Use Instagram
-
Apple: Use Apple
-
Configure each provider with:
- Client ID and Client Secret
- Redirect URI:
http://localhost:8090/realms/olara-realm/broker/{provider}/endpoint - Scopes: Appropriate scopes for each provider
User Experience Flow
sequenceDiagram
participant Client
participant UserService
participant Keycloak
participant SocialProvider
Client->>UserService: POST /auth/social-login/url {provider: "google"}
UserService-->>Client: authorizationUrl
Client->>Keycloak: Redirect to authorizationUrl
Keycloak->>SocialProvider: Redirect to Google OAuth
SocialProvider-->>User: Google authentication page
User-->>SocialProvider: Authenticate & authorize
SocialProvider-->>Keycloak: Redirect with authorization code
Keycloak->>SocialProvider: Exchange code for access token
SocialProvider-->>Keycloak: Access token + user info
Keycloak->>Keycloak: Create/update user account
Client->>UserService: POST /auth/social-login {code, provider}
UserService->>Keycloak: Exchange code for JWT
Keycloak-->>UserService: JWT token
UserService-->>Client: Login successful + user info
Security Considerations
- State Parameter: Always include a state parameter to prevent CSRF attacks
- PKCE: Use Proof Key for Code Exchange for enhanced security
- HTTPS: Ensure all redirects use HTTPS in production
- Token Storage: Store tokens securely (HttpOnly cookies recommended)
- User Consent: Clearly communicate what data is being shared
Error Handling
Social login may fail due to:
- Invalid authorization code
- Expired tokens
- Provider API rate limits
- User denied consent
- Invalid provider configuration
All errors are handled gracefully with appropriate HTTP status codes and error messages.
π± Mobile Firebase Social Login
This service supports mobile Firebase social login for Flutter, React Native, and native iOS/Android apps. This implementation provides a mobile-friendly endpoint that works directly with Firebase SDK authentication, bypassing the web redirect flow complexity.
Overview
Mobile Firebase social login allows mobile apps to handle OAuth flow directly using Firebase SDK, then send the authentication result to the backend for user creation and token generation. This approach is optimized for mobile user experience and eliminates the need for web browser redirects.
Supported Providers
- Google - Google Sign-In via Firebase
- Apple - Sign in with Apple via Firebase
- Facebook - Facebook Login via Firebase
- Twitter - Twitter OAuth via Firebase
- GitHub - GitHub OAuth via Firebase
Endpoint
POST /api/v1/auth/mobile/social-login
Request
{
"provider": "google",
"idToken": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"accessToken": "ya29.a0ATi6K2s...",
"additionalUserInfo": {
"isNewUser": false,
"profile": {
"uid": "111339873715121048538",
"email": "user@example.com",
"emailVerified": true,
"displayName": "John Doe",
"givenName": "John",
"familyName": "Doe",
"photoUrl": "https://lh3.googleusercontent.com/photo.jpg",
"phoneNumber": "+1234567890",
"providerId": "google.com"
}
},
"deviceId": "device-uuid-optional",
"ipAddress": "192.168.1.1-optional"
}
Parameters
| Field | Type | Required | Description |
|---|---|---|---|
provider |
String | β | One of: google, apple, facebook, twitter, github |
idToken |
String | β | JWT ID token from Firebase |
accessToken |
String | β | OAuth access token from provider (stored for future use) |
additionalUserInfo |
Object | β | User profile information from Firebase |
additionalUserInfo.isNewUser |
Boolean | β | Whether this is a new user for Firebase (deprecated - check user.created == user.updated) |
additionalUserInfo.profile |
Object | β | User profile data |
deviceId |
String | β | Device identifier for tracking |
ipAddress |
String | β | Client IP address for logging |
Response
{
"timestamp": "2025-09-07T10:00:00.000Z",
"success": true,
"message": "Mobile social login successful",
"data": {
"user": {
"id": "user-uuid",
"tenantId": "tenant-code",
"username": "user@gmail.com",
"email": "user@gmail.com",
"phone": null,
"firstName": "John",
"lastName": "Doe",
"middleName": null,
"imageUrl": "https://lh3.googleusercontent.com/photo.jpg",
"dateOfBirth": null,
"address": null,
"emailVerified": true,
"phoneVerified": false,
"status": "ACTIVE",
"roles": [],
"lastLoginAt": null,
"created": "2025-09-07T10:00:00.000Z",
"updated": "2025-09-07T10:00:00.000Z",
"createdBy": "firebase-social-login",
"updatedBy": "firebase-social-login",
"kycStatus": null,
"failedLoginAttempts": 0,
"referralCode": "ABC123",
"referredBy": null
},
"accessToken": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"expiresIn": 3600,
"refreshExpiresIn": 2592000
},
"metadata": {
"processingTime": "120ms",
"serverId": "user-service-01",
"version": "1.0.0"
}
}
Flutter Implementation Example
1. Add Dependencies
dependencies:
firebase_auth: ^4.0.0
google_sign_in: ^6.0.0
sign_in_with_apple: ^4.0.0
http: ^1.1.0
2. Firebase Configuration
First, configure Firebase in your Flutter app:
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_auth/firebase_auth.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
runApp(MyApp());
}
3. Google Sign In with Firebase
import 'package:firebase_auth/firebase_auth.dart';
import 'package:google_sign_in/google_sign_in.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';
final FirebaseAuth _auth = FirebaseAuth.instance;
final GoogleSignIn _googleSignIn = GoogleSignIn();
Future<void> signInWithGoogle() async {
try {
// Trigger Google Sign In
final GoogleSignInAccount? googleUser = await _googleSignIn.signIn();
if (googleUser == null) return; // User cancelled
// Get authentication credentials
final GoogleSignInAuthentication googleAuth = await googleUser.authentication;
// Create Firebase credential
final OAuthCredential credential = GoogleAuthProvider.credential(
accessToken: googleAuth.accessToken,
idToken: googleAuth.idToken,
);
// Sign in to Firebase
final UserCredential userCredential = await _auth.signInWithCredential(credential);
final User? firebaseUser = userCredential.user;
if (firebaseUser == null) throw Exception('Firebase authentication failed');
// Get additional user info
final additionalUserInfo = userCredential.additionalUserInfo;
// Prepare request data
final requestData = {
'provider': 'google',
'idToken': await firebaseUser.getIdToken(),
'accessToken': googleAuth.accessToken,
'additionalUserInfo': {
'isNewUser': additionalUserInfo?.isNewUser ?? false,
'profile': {
'uid': firebaseUser.uid,
'email': firebaseUser.email,
'emailVerified': firebaseUser.emailVerified,
'displayName': firebaseUser.displayName,
'givenName': additionalUserInfo?.profile?['given_name'],
'familyName': additionalUserInfo?.profile?['family_name'],
'photoUrl': firebaseUser.photoURL,
'phoneNumber': firebaseUser.phoneNumber,
'providerId': firebaseUser.providerData.first.providerId,
}
},
'deviceId': await getDeviceId(),
'ipAddress': await getIpAddress(),
};
// Send to your backend
final response = await http.post(
Uri.parse('https://your-api.com/api/v1/auth/mobile/social-login'),
headers: {
'Content-Type': 'application/json',
'X-Tenant-ID': 'your-tenant-id',
},
body: jsonEncode(requestData),
);
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
final accessToken = data['data']['accessToken'];
final refreshToken = data['data']['refreshToken'];
final user = data['data']['user'];
final isNewUser = user['created'] == user['updated'];
// Save tokens securely
await saveTokens(accessToken, refreshToken);
if (isNewUser) {
// Show profile completion flow
navigateToProfileCompletion();
} else {
// Navigate to main app
navigateToHome();
}
} else {
throw Exception('Login failed: ${response.body}');
}
} catch (e) {
print('Error signing in with Google: $e');
showErrorDialog(e.toString());
}
}
4. Apple Sign In with Firebase
import 'package:firebase_auth/firebase_auth.dart';
import 'package:sign_in_with_apple/sign_in_with_apple.dart';
Future<void> signInWithApple() async {
try {
// Request Apple credentials
final appleCredential = await SignInWithApple.getAppleIDCredential(
scopes: [
AppleIDAuthorizationScopes.email,
AppleIDAuthorizationScopes.fullName,
],
);
// Create Firebase OAuth credential
final OAuthProvider oAuthProvider = OAuthProvider('apple.com');
final AuthCredential credential = oAuthProvider.credential(
idToken: appleCredential.identityToken,
accessToken: appleCredential.authorizationCode,
);
// Sign in to Firebase
final UserCredential userCredential = await _auth.signInWithCredential(credential);
final User? firebaseUser = userCredential.user;
if (firebaseUser == null) throw Exception('Firebase authentication failed');
// Get additional user info
final additionalUserInfo = userCredential.additionalUserInfo;
// Prepare request data
final requestData = {
'provider': 'apple',
'idToken': await firebaseUser.getIdToken(),
'accessToken': appleCredential.authorizationCode,
'additionalUserInfo': {
'isNewUser': additionalUserInfo?.isNewUser ?? false,
'profile': {
'uid': firebaseUser.uid,
'email': firebaseUser.email,
'emailVerified': firebaseUser.emailVerified,
'displayName': firebaseUser.displayName,
'givenName': additionalUserInfo?.profile?['given_name'],
'familyName': additionalUserInfo?.profile?['family_name'],
'photoUrl': firebaseUser.photoURL,
'phoneNumber': firebaseUser.phoneNumber,
'providerId': firebaseUser.providerData.first.providerId,
}
},
'deviceId': await getDeviceId(),
};
// Send to backend
final response = await http.post(
Uri.parse('https://your-api.com/api/v1/auth/mobile/social-login'),
headers: {
'Content-Type': 'application/json',
'X-Tenant-ID': 'your-tenant-id',
},
body: jsonEncode(requestData),
);
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
await saveTokens(data['data']['accessToken'], data['data']['refreshToken']);
}
} catch (e) {
print('Error signing in with Apple: $e');
}
}
How It Works
Flow Diagram
sequenceDiagram
participant MobileApp
participant Firebase
participant UserService
participant Keycloak
MobileApp->>Firebase: Sign in with provider (Google/Apple/etc.)
Firebase->>MobileApp: Firebase UserCredential (ID token + user info)
MobileApp->>UserService: POST /mobile/social-login + Firebase data
UserService->>UserService: Verify ID token + extract user info
UserService->>UserService: Check if user exists (by provider ID or email)
alt New User
UserService->>UserService: Create user in local DB
UserService->>Keycloak: Create user in Keycloak
UserService->>Keycloak: Generate temp password + authenticate
else Existing User
UserService->>UserService: Update user profile if needed
end
Keycloak-->>UserService: JWT tokens
UserService-->>MobileApp: Login successful + tokens + user info
Security Considerations
- ID Token Verification: The backend verifies the Firebase ID token to ensure authenticity
- Secure Token Storage: Use platform-specific secure storage (Keychain, KeyStore)
- HTTPS Only: Always use HTTPS for API communications
- Tenant Isolation: Include tenant ID in request headers for multi-tenant support
- Device Tracking: Optional device ID and IP logging for security monitoring
Registration Behavior
Automatic User Registration
When a user signs in with Firebase social login for the first time:
- New user is automatically registered - No separate registration endpoint needed
- Email is auto-verified β - Firebase/social providers handle verification
- User profile is auto-created from Firebase data:
- Email, name, profile picture
- Phone number (if available)
- Provider-specific unique ID
- Social connection is recorded - Links provider account to user
- Access & refresh tokens are returned - User is immediately logged in
- New user status can be determined - Check if
user.created == user.updatedto identify new users
Auto-Verified Fields
| Field | Auto-Verified | Reason |
|---|---|---|
| β YES | OAuth provider verified before issuing tokens | |
| Phone | β NO | Not always provided or verified by OAuth provider |
| Account | β YES | User authenticated via Firebase + OAuth provider |
Error Handling
Common error responses:
| Status | Error | Meaning |
|---|---|---|
| 400 | Provider is required | provider field missing |
| 400 | ID token is required | idToken field missing |
| 400 | Unsupported provider | Provider not in allowed list |
| 400 | Tenant information required | X-Tenant-ID header missing |
| 401 | Invalid ID token format | JWT parsing failed |
| 401 | Mobile social login failed | General authentication failure |
| 404 | Tenant not found | Tenant ID doesn't exist |
| 500 | Failed to generate tokens | Keycloak communication error |
Testing
Using cURL
curl -X POST https://your-api.com/api/v1/auth/mobile/social-login \
-H "Content-Type: application/json" \
-H "X-Tenant-ID: tenant-uuid" \
-d '{
"provider": "google",
"idToken": "eyJhbGciOiJSUzI1NiIsImtpZCI6...",
"accessToken": "ya29.a0ATi6K2s...",
"additionalUserInfo": {
"isNewUser": false,
"profile": {
"uid": "123456789",
"email": "user@example.com",
"emailVerified": true,
"displayName": "John Doe"
}
},
"deviceId": "device-123"
}'
Configuration
No additional configuration needed beyond standard Keycloak setup. The endpoint automatically:
- Verifies Firebase ID tokens
- Creates users in local database and Keycloak
- Generates JWT access/refresh tokens
- Stores social connection data
- Handles device tracking
Future Enhancements
- Firebase Admin SDK: Enhanced token verification with Firebase Admin SDK
- Device Biometric Binding: Link social login to device biometrics
- Token Refresh: Automatic token refresh for long-lived sessions
- Account Linking: Allow linking multiple social providers to one account
- Advanced Security: Risk-based authentication and fraud detection
π€ Referral System
The user service includes a comprehensive referral system that tracks user referrals and supports reward management.
How It Works
- User Registration with Referral Code:
- Users can provide a referral code during registration
- The system validates the referral code and links the new user to the referrer
-
A
Referralrecord is created to track the relationship -
Referral Code Generation:
- Each user gets a unique referral code upon registration
- Codes are generated using a secure random algorithm
-
Collision detection with retry mechanism ensures uniqueness
-
Referral Tracking:
- All referrals are stored in the
referralstable - Includes referrer, referred user, status, and reward information
- Supports pagination and efficient querying through projections
Database Schema
Users Table:
- referral_code: Unique code for this user
- referred_by: UUID reference to the user who referred them
Referrals Table:
- referrer_id: User who made the referral
- referred_id: User who was referred
- status: Current status (PENDING, COMPLETED, etc.)
- reward_issued: Whether reward has been given
- reward_type & reward_value: Reward details
API Endpoints
GET /api/v1/users/{userId}/referrals- Get paginated list of user's referrals- Registration endpoints automatically handle referral code processing
- Notification system sends emails for successful referrals
π Admin User Management
The user service provides comprehensive administrative capabilities for managing users across the entire system. These features are designed for system administrators who need to perform cross-tenant operations and user lifecycle management.
Key Features
- Cross-Tenant User Management: Administrators can view and manage users across all tenants
- Bulk User Operations: Create, deactivate, and unlock users system-wide
- Tenant-Specific Queries: Filter users by specific tenant for targeted management
- Security Controls: All admin endpoints are protected with role-based access control
- Audit Trail: All administrative actions are logged for compliance
Security & Permissions
Admin endpoints require specific permissions:
- CREATE_USER: Required for user creation
- VIEW_USERS: Required for viewing user lists
- MANAGE_USERS: Required for user status changes
Use Cases
- System Administration:
- Create users for new tenants
- Manage user access across the platform
-
Handle account lockouts and security issues
-
Tenant Management:
- View all users within a specific tenant
- Perform bulk operations on tenant users
-
Monitor user activity and status
-
Security Operations:
- Unlock accounts after failed login attempts
- Deactivate compromised accounts
- (Reactivation is not available via API)
API Endpoints
POST /api/v1/admin/users- Create new user (cross-tenant)GET /api/v1/admin/users- Get all users across tenantsPOST /api/v1/admin/users/{userId}/deactivate- Deactivate user
POST /api/v1/admin/users/{userId}/unlock- Unlock user account
π Eureka Service Discovery
- Registers with Eureka for service discovery.
- Configurable via
eureka.client.serviceUrl.defaultZoneand related properties. - See
application.ymland Docker Compose for details.
π Monitoring & Observability
- Actuator: Health, info, metrics endpoints (
/actuator/*) - Loki: Centralized logging
- Elastic APM: Distributed tracing
- Healthcheck: Configured in Docker Compose
π§ͺ Testing
- Unit and integration tests in
src/test/java - Test configuration in
src/test/resources/application.yml - Use
./gradlew testto run tests
π Passkey Authentication Flow
This service supports WebAuthn/FIDO2 passkey authentication for passwordless login. Passkeys provide a secure, phishing-resistant alternative to traditional passwords.
How to Login with Passkey
Passkey login eliminates passwords - here's how it works:
- Enter your username on the login page
- Click "Login with Passkey"
- Choose your passkey from the browser popup (if multiple)
- Authenticate using fingerprint, face scan, PIN, or security key
- You're logged in! - No password needed
What Makes Passkey Login Secure
- Phishing Resistant: Keys only work on the correct domain
- No Password Storage: Nothing to steal or forget
- Multi-Device Support: Use passkeys across your devices
- Biometric Ready: Works with fingerprint, face, or PIN
- Hardware Security: Keys stored in secure hardware when possible
Passkey Management Features
- Multiple Passkeys: Users can register multiple passkeys for enhanced security and convenience
- Passkey Inventory: View all registered passkeys with detailed information
- Paginated Lists: Efficiently browse through passkeys with pagination support
- Passkey Deletion: Remove unused or compromised passkeys
- Usage Tracking: Monitor when passkeys were last used and signature counts
Using Pagination with Passkeys
The passkey management API supports pagination for efficient data retrieval. The response format follows the standard BaseController pattern used throughout the Olara User Service API.
// Get first page with 10 passkeys per page, sorted by creation date
const response = await fetch('/api/v1/passkeys?page=0&size=10&sort=createdAt,desc');
const result = await response.json();
// Access passkeys directly from data array
console.log('Passkeys:', result.data);
console.log('Total pages:', result.totalPages);
console.log('Current page:', result.page);
console.log('Page size:', result.size);
console.log('Total elements:', result.totalElements);
// Check if there are more pages
if (result.page < result.totalPages - 1) {
// Get next page
const nextPage = await fetch(`/api/v1/passkeys?page=${result.page + 1}&size=10&sort=createdAt,desc`);
const nextResult = await nextPage.json();
console.log('Next page passkeys:', nextResult.data);
}
// Get passkeys sorted by last used date
const recentPasskeys = await fetch('/api/v1/passkeys?page=0&size=5&sort=lastUsedAt,desc');
const recentResult = await recentPasskeys.json();
console.log('Recently used passkeys:', recentResult.data);
Note: The pagination response includes metadata such as processing time and server information, which is automatically added by the BaseController.
Quick Start: Login with Passkey
// Frontend login code
async function loginWithPasskey(username) {
// 1. Start authentication
const response = await fetch('/api/v1/passkeys/authenticate/start', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username })
});
const options = await response.json();
// 2. Get credential from browser
const credential = await navigator.credentials.get({
publicKey: options.data
});
// 3. Complete authentication
const authResponse = await fetch('/api/v1/passkeys/authenticate/finish', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
credentialId: btoa(String.fromCharCode(...new Uint8Array(credential.rawId))),
authenticatorData: btoa(String.fromCharCode(...new Uint8Array(credential.response.authenticatorData))),
clientDataJSON: btoa(String.fromCharCode(...new Uint8Array(credential.response.clientDataJSON))),
signature: btoa(String.fromCharCode(...new Uint8Array(credential.response.signature))),
userHandle: credential.response.userHandle ?
btoa(String.fromCharCode(...new Uint8Array(credential.response.userHandle))) : null
})
});
const result = await authResponse.json();
if (result.success) {
// Store tokens and redirect
localStorage.setItem('accessToken', result.data.accessToken);
window.location.href = '/dashboard';
}
}
Overview
Passkey authentication uses public-key cryptography where: - Registration: Creates a cryptographic key pair stored securely on the user's device - Authentication: Uses the private key to sign challenges without exposing sensitive data - Security: Keys never leave the device, providing strong protection against phishing
Passkey Registration Flow
1. Start Registration
Endpoint: POST /api/v1/passkeys/register/start
Request:
{
"username": "john.doe"
}
Response:
{
"success": true,
"data": {
"rp": {
"name": "Olara User Service",
"id": "olara.com"
},
"user": {
"name": "john.doe",
"displayName": "John Doe",
"id": "dXNlcmlk"
},
"challenge": "challenge-string",
"pubKeyCredParams": [
{
"type": "public-key",
"alg": -7
}
],
"timeout": 60000,
"excludeCredentials": [],
"authenticatorSelection": {
"authenticatorAttachment": "cross-platform",
"requireResidentKey": false,
"userVerification": "preferred"
},
"attestation": "direct"
}
}
2. Complete Registration
Endpoint: POST /api/v1/passkeys/register/finish
Request:
{
"credentialId": "base64-encoded-credential-id",
"publicKey": "base64-encoded-public-key",
"attestationObject": "base64-encoded-attestation",
"clientDataJSON": "base64-encoded-client-data",
"transports": ["usb", "nfc", "ble"]
}
Response:
{
"success": true,
"data": "Passkey registered successfully"
}
Passkey Authentication Flow
1. Start Authentication
Endpoint: POST /api/v1/passkeys/authenticate/start
Request:
{
"username": "john.doe"
}
Response:
{
"success": true,
"data": {
"challenge": "challenge-string",
"timeout": 60000,
"rpId": "olara.com",
"allowCredentials": [
{
"type": "public-key",
"id": "base64-encoded-credential-id",
"transports": ["usb", "nfc"]
}
],
"userVerification": "preferred"
}
}
2. Complete Authentication
Endpoint: POST /api/v1/passkeys/authenticate/finish
Request:
{
"credentialId": "base64-encoded-credential-id",
"authenticatorData": "base64-encoded-authenticator-data",
"clientDataJSON": "base64-encoded-client-data",
"signature": "base64-encoded-signature",
"userHandle": "base64-encoded-user-handle"
}
Response:
{
"success": true,
"data": {
"id": "user-id",
"username": "john.doe",
"email": "john.doe@olara.com",
"firstName": "John",
"lastName": "Doe"
}
}
Passkey Management
List User's Passkeys
Endpoint: GET /api/v1/passkeys
Query Parameters:
- page (optional): Page number (0-based, default: 0)
- size (optional): Number of items per page (default: 20)
- sort (optional): Sort criteria (e.g., createdAt,desc)
Example Requests:
GET /api/v1/passkeys # Default: page=0, size=20
GET /api/v1/passkeys?page=0&size=10&sort=createdAt,desc # Custom page size and sorting
GET /api/v1/passkeys?page=1&size=5&sort=lastUsedAt,desc # Second page with different sorting
Response:
{
"timestamp": "2025-09-07T12:00:00.000Z",
"success": true,
"message": "Operation successful",
"data": [
{
"id": "passkey-id",
"credentialId": "base64-encoded-credential-id",
"createdAt": "2025-01-15T10:30:00Z",
"lastUsedAt": "2025-01-15T14:20:00Z",
"signatureCount": 5,
"transports": ["usb", "nfc"],
"backupEligible": true,
"backupState": false
}
],
"totalElements": 1,
"totalPages": 1,
"page": 0,
"size": 20,
"metadata": {
"processingTime": "15ms",
"serverId": "user-service-01",
"version": "1.0.0"
}
}
Delete Passkey
Endpoint: DELETE /api/v1/passkeys/{credentialId}
Response:
{
"success": true,
"data": "Passkey deleted successfully"
}
Update Passkey
Endpoint: PUT /api/v1/passkeys/{credentialId}
Request:
{
"name": "My Security Key"
}
Response:
{
"success": true,
"data": "Passkey updated successfully"
}
Frontend Integration
To integrate passkeys in your frontend application:
- WebAuthn API: Use the browser's Web Authentication API
- Registration: Call
navigator.credentials.create()with the options from/register/start - Authentication: Call
navigator.credentials.get()with the options from/authenticate/start - Data Encoding: Encode binary data as base64url for transmission
Example JavaScript Registration:
// Get registration options from backend
const options = await fetch('/api/v1/passkeys/register/start', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username: 'john.doe' })
}).then(r => r.json());
// Create credential
const credential = await navigator.credentials.create({
publicKey: options.data
});
// Send to backend
await fetch('/api/v1/passkeys/register/finish', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
credentialId: btoa(String.fromCharCode(...new Uint8Array(credential.rawId))),
publicKey: btoa(String.fromCharCode(...new Uint8Array(credential.response.getPublicKey()))),
attestationObject: btoa(String.fromCharCode(...new Uint8Array(credential.response.attestationObject))),
clientDataJSON: btoa(String.fromCharCode(...new Uint8Array(credential.response.clientDataJSON)))
})
});
Security Benefits
- Phishing Resistant: Keys are bound to specific domains
- No Password Storage: Eliminates password-related security risks
- Multi-Device Support: Users can have passkeys on multiple devices
- Biometric Integration: Can combine with device biometrics (fingerprint, face)
- Backup & Recovery: Supports passkey backup and cross-device authentication
π¦Ύ Biometric Device Authentication Flow
This service supports secure biometric authentication for devices using public-key cryptography. The flow enables passwordless authentication through device-based biometrics (fingerprint, face recognition, etc.).
π§ Prerequisites & Setup
Before implementing biometric authentication:
- Device Capabilities: Ensure your device supports biometric authentication
- Key Storage: Implement secure private key storage (e.g., KeyStore on Android, Keychain on iOS)
- Network Security: Use HTTPS for all API communications
- Error Handling: Implement proper error handling for biometric failures
π± Complete Biometric Authentication Flow
sequenceDiagram
participant Client
participant API
participant Database
Note over Client,Database: π INITIAL SETUP PHASE
Client->>API: GET /api/v1/device/keypair
API-->>Client: Return public/private keypair
Client->>API: POST /api/v1/device/register
API->>Database: Store device info + public key
API-->>Client: Registration success + tokens
Note over Client,Database: π AUTHENTICATION PHASE
Client->>API: POST /api/v1/biometric/challenge
API->>Database: Generate & store challenge
API-->>Client: Challenge + session token
Note over Client: π± BIOMETRIC PROMPT
Client->>Client: User provides biometric (fingerprint/face)
Client->>Client: Sign challenge with private key
Client->>API: POST /api/v1/biometric/authenticate
API->>Database: Verify signature against public key
API-->>Client: Authentication success + access token
Note over Client,Database: π MANAGEMENT PHASE
Client->>API: GET /api/v1/device (list devices)
API->>Database: Fetch user's devices
API-->>Client: Paginated device list
Client->>API: GET /api/v1/device/{deviceId}
API->>Database: Fetch specific device
API-->>Client: Device details
π Quick Start Implementation
1. Device Keypair Generation
Endpoint: GET /api/v1/device/keypair
Purpose: Generate cryptographic keypair for biometric authentication
// Generate device keypair
async function generateDeviceKeypair() {
try {
const response = await fetch('/api/v1/device/keypair');
const keypair = await response.json();
if (keypair.success) {
// Store private key securely on device
await secureStore.setItem('biometric_private_key', keypair.data.privateKey);
// Return public key for registration
return keypair.data.publicKey;
}
} catch (error) {
console.error('Keypair generation failed:', error);
}
}
Response:
{
"timestamp": "2025-09-07T12:00:00.000Z",
"success": true,
"message": "Operation successful",
"data": {
"publicKey": "BASE64_ENCODED_PUBLIC_KEY",
"privateKey": "BASE64_ENCODED_PRIVATE_KEY"
},
"metadata": {
"processingTime": "50ms",
"serverId": "user-service-01",
"version": "1.0.0"
}
}
2. Device Registration
Endpoint: POST /api/v1/device/register
Purpose: Register a device with its public key for biometric authentication
// Register device for biometric authentication
async function registerDevice(publicKey) {
const deviceInfo = {
deviceName: navigator.userAgent.split(' ').pop() || 'Unknown Device',
deviceModel: navigator.platform || 'Unknown Model',
osName: getOSName(),
osVersion: getOSVersion(),
publicKey: publicKey
};
try {
const response = await fetch('/api/v1/device/register', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${accessToken}` // If user is already authenticated
},
body: JSON.stringify(deviceInfo)
});
const result = await response.json();
if (result.success) {
// Store device ID and tokens securely
await secureStore.setItem('device_id', result.data.deviceId);
await secureStore.setItem('access_token', result.data.token.accessToken);
await secureStore.setItem('refresh_token', result.data.token.refreshToken);
console.log('Device registered successfully!');
return result.data;
} else {
throw new Error(result.message || 'Registration failed');
}
} catch (error) {
console.error('Device registration failed:', error);
throw error;
}
}
// Helper functions
function getOSName() {
const userAgent = navigator.userAgent;
if (userAgent.includes('Windows')) return 'Windows';
if (userAgent.includes('Mac')) return 'macOS';
if (userAgent.includes('Linux')) return 'Linux';
if (userAgent.includes('Android')) return 'Android';
if (userAgent.includes('iPhone') || userAgent.includes('iPad')) return 'iOS';
return 'Unknown';
}
function getOSVersion() {
// Implementation depends on platform
return navigator.userAgent.match(/OS (\d+)_/)?.[1] ||
navigator.userAgent.match(/Android (\d+)/)?.[1] ||
'Unknown';
}
Request Example:
{
"deviceName": "Pixel 7",
"deviceModel": "GXLX",
"osName": "Android",
"osVersion": "14",
"publicKey": "LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS1NSUdZQ0FvR0d..."
}
Success Response:
{
"timestamp": "2025-09-07T12:00:00.000Z",
"success": true,
"message": "Operation successful",
"data": {
"deviceId": {
"userId": "uuid",
"tenantId": "uuid",
"deviceName": "Pixel 7",
"deviceModel": "GXLX",
"osName": "Android",
"osVersion": "14",
"ipAddress": "192.168.1.100",
"deviceFingerprint": "a1b2c3d4e5f678901234567890123456789012345678901234567890",
"publicKey": "LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS1NSUdZQ0FvR0d...",
"biometricEnabled": true,
"active": true,
"created": "2025-09-07T12:00:00.000Z",
"updated": "2025-09-07T12:00:00.000Z"
},
"token": {
"accessToken": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"expiresIn": 3600,
"refreshExpiresIn": 7200
},
"biometricEnabled": true,
"qrCodeData": null
},
"metadata": {
"processingTime": "150ms",
"serverId": "user-service-01",
"version": "1.0.0"
}
}
Error Response (Device Already Registered):
{
"timestamp": "2025-09-07T12:00:00.000Z",
"success": false,
"message": "This device is already registered",
"errorDetails": "Device fingerprint already exists in the system",
"status": "CONFLICT",
"path": "/api/v1/device/register",
"metadata": {
"processingTime": "50ms",
"serverId": "user-service-01",
"version": "1.0.0"
}
}
3. Request Biometric Challenge
Endpoint: POST /api/v1/biometric/challenge
Purpose: Request a cryptographic challenge that the device will sign with its biometric key
// Request biometric challenge
async function requestBiometricChallenge() {
try {
const response = await fetch('/api/v1/biometric/challenge', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${await getAccessToken()}`
},
body: JSON.stringify({
deviceId: await secureStore.getItem('device_id')
})
});
const result = await response.json();
if (result.success) {
// Store session token for later authentication
await secureStore.setItem('biometric_session_token', result.data.sessionToken);
return result.data.challenge;
} else {
throw new Error(result.message || 'Failed to get challenge');
}
} catch (error) {
console.error('Challenge request failed:', error);
throw error;
}
}
Request Example:
{
"deviceId": "b1c2d3e4-5678-1234-9abc-def012345678"
}
Response:
{
"timestamp": "2025-09-07T12:00:00.000Z",
"success": true,
"message": "Operation successful",
"data": {
"challenge": "a1b2c3d4e5f678901234567890123456789012345678901234567890",
"sessionToken": "session_abc123def456ghi789jkl012mno345pqr678stu901vwx345yz"
},
"metadata": {
"processingTime": "25ms",
"serverId": "user-service-01",
"version": "1.0.0"
}
}
4. Device Signs Challenge with Biometric Key
Security Note: This step happens on the device and never exposes the private key.
// Sign challenge with biometric authentication
async function signChallengeWithBiometric(challenge) {
try {
// Check if biometric authentication is available
const isAvailable = await checkBiometricAvailability();
if (!isAvailable) {
throw new Error('Biometric authentication not available on this device');
}
// Prompt user for biometric authentication
const biometricResult = await authenticateWithBiometric();
if (!biometricResult.success) {
throw new Error('Biometric authentication failed');
}
// Get private key from secure storage
const privateKey = await secureStore.getItem('biometric_private_key');
// Sign the challenge
const signature = await signData(challenge, privateKey);
return signature;
} catch (error) {
console.error('Biometric signing failed:', error);
throw error;
}
}
// Helper functions
async function checkBiometricAvailability() {
// Check if device supports biometric authentication
if (window.PublicKeyCredential) {
return await PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable();
}
return false;
}
async function authenticateWithBiometric() {
// Use WebAuthn or native biometric APIs
try {
const credential = await navigator.credentials.get({
publicKey: {
challenge: new Uint8Array(32), // Random challenge for biometric prompt
rpId: window.location.hostname,
userVerification: 'required'
}
});
return { success: true, credential };
} catch (error) {
return { success: false, error };
}
}
async function signData(data, privateKey) {
// Use Web Crypto API or native crypto libraries
const encoder = new TextEncoder();
const dataBuffer = encoder.encode(data);
const key = await crypto.subtle.importKey(
'pkcs8',
base64ToArrayBuffer(privateKey),
{ name: 'RSASSA-PKCS1-v1_5', hash: 'SHA-256' },
false,
['sign']
);
const signature = await crypto.subtle.sign('RSASSA-PKCS1-v1_5', key, dataBuffer);
return arrayBufferToBase64(signature);
}
5. Authenticate with Biometric Signature
Endpoint: POST /api/v1/biometric/authenticate
Purpose: Complete authentication by verifying the signed challenge
// Complete biometric authentication
async function completeBiometricAuthentication(signature) {
try {
const sessionToken = await secureStore.getItem('biometric_session_token');
const response = await fetch('/api/v1/biometric/authenticate', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
sessionToken: sessionToken,
signature: signature
})
});
const result = await response.json();
if (result.success) {
// Store new access tokens
await secureStore.setItem('access_token', result.data.sessionToken.accessToken);
await secureStore.setItem('refresh_token', result.data.sessionToken.refreshToken);
// Clear the biometric session token
await secureStore.removeItem('biometric_session_token');
console.log('Biometric authentication successful!');
return result.data;
} else {
throw new Error(result.message || 'Authentication failed');
}
} catch (error) {
console.error('Biometric authentication failed:', error);
throw error;
}
}
// Complete authentication flow
async function authenticateWithBiometric() {
try {
// Step 1: Request challenge
const challenge = await requestBiometricChallenge();
// Step 2: Sign challenge with biometric
const signature = await signChallengeWithBiometric(challenge);
// Step 3: Complete authentication
const authResult = await completeBiometricAuthentication(signature);
return authResult;
} catch (error) {
console.error('Biometric authentication flow failed:', error);
throw error;
}
}
Request Example:
{
"sessionToken": "session_abc123def456ghi789jkl012mno345pqr678stu901vwx345yz",
"signature": "MEUCIEZ9ZJGQ2FkHnB8yQ7vQZ8Q7vQZ8Q7vQZ8Q7vQZ8Q7AiEA..."
}
Success Response:
{
"timestamp": "2025-09-07T12:00:00.000Z",
"success": true,
"message": "Operation successful",
"data": {
"sessionToken": {
"accessToken": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"expiresIn": 3600,
"refreshExpiresIn": 7200
},
"user": {
"id": "user-uuid",
"username": "john.doe",
"email": "john.doe@example.com",
"firstName": "John",
"lastName": "Doe"
},
"device": {
"deviceId": "device-uuid",
"deviceName": "Pixel 7",
"biometricEnabled": true
}
},
"metadata": {
"processingTime": "200ms",
"serverId": "user-service-01",
"version": "1.0.0"
}
}
Error Response (Invalid Signature):
{
"timestamp": "2025-09-07T12:00:00.000Z",
"success": false,
"message": "Biometric authentication failed",
"errorDetails": "Invalid signature or challenge expired",
"status": "UNAUTHORIZED",
"path": "/api/v1/biometric/authenticate",
"metadata": {
"processingTime": "150ms",
"serverId": "user-service-01",
"version": "1.0.0"
}
}
6. Device Management
List User's Devices: GET /api/v1/device
// Get paginated list of user's devices
async function getUserDevices(page = 0, size = 10) {
try {
const response = await fetch(`/api/v1/device?page=${page}&size=${size}&sort=created,desc`, {
headers: {
'Authorization': `Bearer ${await getAccessToken()}`
}
});
const result = await response.json();
if (result.success) {
console.log(`Found ${result.totalElements} devices`);
return {
devices: result.data,
totalPages: result.totalPages,
currentPage: result.page,
hasMore: result.page < result.totalPages - 1
};
} else {
throw new Error(result.message || 'Failed to fetch devices');
}
} catch (error) {
console.error('Failed to fetch devices:', error);
throw error;
}
}
Get Device Details: GET /api/v1/device/{deviceId}
// Get specific device details
async function getDeviceDetails(deviceId) {
try {
const response = await fetch(`/api/v1/device/${deviceId}`, {
headers: {
'Authorization': `Bearer ${await getAccessToken()}`
}
});
const result = await response.json();
if (result.success) {
return result.data;
} else {
throw new Error(result.message || 'Failed to fetch device details');
}
} catch (error) {
console.error('Failed to fetch device details:', error);
throw error;
}
}
π¨ Error Handling & Edge Cases
Common Error Scenarios
1. Device Not Found
{
"timestamp": "2025-09-07T12:00:00.000Z",
"success": false,
"message": "Device not found",
"errorDetails": "No device found with ID: b1c2d3e4-5678-1234-9abc-def012345678",
"status": "NOT_FOUND",
"path": "/api/v1/device/b1c2d3e4-5678-1234-9abc-def012345678"
}
2. Challenge Expired
{
"timestamp": "2025-09-07T12:00:00.000Z",
"success": false,
"message": "Challenge expired",
"errorDetails": "Biometric challenge has expired. Please request a new challenge.",
"status": "UNAUTHORIZED",
"path": "/api/v1/biometric/authenticate"
}
3. Invalid Signature
{
"timestamp": "2025-09-07T12:00:00.000Z",
"success": false,
"message": "Invalid signature",
"errorDetails": "The provided signature does not match the expected signature for this challenge.",
"status": "UNAUTHORIZED",
"path": "/api/v1/biometric/authenticate"
}
4. Biometric Not Available
{
"timestamp": "2025-09-07T12:00:00.000Z",
"success": false,
"message": "Biometric authentication not available",
"errorDetails": "This device does not support biometric authentication or it is not configured.",
"status": "BAD_REQUEST",
"path": "/api/v1/biometric/challenge"
}
Best Practices for Error Handling
// Comprehensive error handling for biometric flow
async function handleBiometricErrors(operation) {
try {
return await operation();
} catch (error) {
if (error.response) {
const errorData = await error.response.json();
switch (errorData.status) {
case 'UNAUTHORIZED':
if (errorData.message.includes('expired')) {
// Challenge expired, request new one
return await requestNewChallenge();
} else if (errorData.message.includes('Invalid signature')) {
// Signature verification failed
return await handleInvalidSignature();
}
break;
case 'NOT_FOUND':
if (errorData.message.includes('Device not found')) {
// Device was removed, re-register
return await reRegisterDevice();
}
break;
case 'CONFLICT':
if (errorData.message.includes('already registered')) {
// Device already exists, proceed with authentication
return await proceedWithExistingDevice();
}
break;
case 'BAD_REQUEST':
if (errorData.message.includes('Biometric not available')) {
// Fallback to password authentication
return await fallbackToPasswordAuth();
}
break;
}
}
// Generic error handling
console.error('Biometric operation failed:', error);
throw error;
}
}
π§Ύ Biometric Device API Field Reference
Below is a quick reference for the key fields used in the biometric device authentication flow. This helps you understand what each field means and how to use them in your integration.
Device Registration Fields
| Field | Type | Description |
|---|---|---|
deviceName |
String | Human-readable name for the device (e.g., "Pixel 7") |
deviceModel |
String | Model identifier (e.g., "GXLX") |
osName |
String | Operating system name (e.g., "Android", "iOS") |
osVersion |
String | Operating system version (e.g., "14") |
publicKey |
String | Base64-encoded public key for biometric authentication |
Note:
deviceFingerprintis generated by hashing device headers and IP. It helps uniquely identify a device.
Device Registration Response Fields
| Field | Type | Description |
|---|---|---|
deviceId |
UUID | Device details (see below) |
token |
Object | Auth tokens (access/refresh) |
biometricEnabled |
Boolean | Whether biometric authentication is enabled |
qrCodeData |
String | (Optional) QR code data for device pairing |
Device Object Fields
| Field | Type | Description |
|---|---|---|
userId |
UUID | ID of the user who owns the device |
tenantId |
UUID | Tenant ID |
deviceName |
String | Name of the device |
deviceModel |
String | Model of the device |
osName |
String | OS name |
osVersion |
String | OS version |
ipAddress |
String | Last known IP address of the device |
deviceFingerprint |
String | Unique fingerprint for the device |
publicKey |
String | Public key for biometric authentication |
biometricEnabled |
Boolean | Whether biometric auth is enabled |
active |
Boolean | Whether the device is active |
created |
Date | Creation timestamp |
updated |
Date | Last update timestamp |
Challenge & Authentication Fields
| Field | Type | Description |
|---|---|---|
challenge |
String | Random string generated by backend, to be signed by device |
sessionToken |
String | Token representing the challenge session, used for subsequent auth |
signature |
String | Base64-encoded signature of the challenge, signed with device's private key |
Auth Response Fields
| Field | Type | Description |
|---|---|---|
accessToken |
String | JWT access token for API authentication |
refreshToken |
String | JWT refresh token |
expiresIn |
Number | Access token expiry (seconds) |
refreshExpiresIn |
Number | Refresh token expiry (seconds) |
π Multi-Language Support
The User Service provides comprehensive multi-language support with translation management. The service supports the following languages:
- en-US (English - Default)
- es-ES (Spanish)
- fr-FR (French)
- de-DE (German)
- pt-BR (Portuguese - Brazil)
- zh-CN (Chinese - Simplified)
- ar-SA (Arabic)
- hi-IN (Hindi)
- ja-JP (Japanese)
- ru-RU (Russian)
Language API Endpoints
| Endpoint | Method | Description |
|---|---|---|
/api/v1/languages |
GET | Get list of all supported languages with code, name, and country information |
/api/v1/languages/{lang} |
GET | Get all translations for a specific language (e.g., en-US, fr-FR) |
/api/v1/languages/all/translations |
GET | Get all translations for all supported languages in a single response |
/api/v1/languages/batch |
POST | Get translations for multiple specified languages (request body: list of language codes) |
Language Translation Caching
The service uses Redis caching with a 1-hour TTL (Time-To-Live) for improved performance: - Individual language translations are cached per language - All translations cache is updated whenever individual language translations are requested - Cache is invalidated after 3600 seconds
Example Language Requests
Get Supported Languages
curl -X GET http://localhost:8080/api/v1/languages
Response:
{
"timestamp": "2025-12-06T10:30:45.123Z",
"success": true,
"message": "Operation successful",
"data": [
{
"code": "en-US",
"language": "English",
"country": "United States"
},
{
"code": "es-ES",
"language": "Spanish",
"country": "Spain"
},
{
"code": "fr-FR",
"language": "French",
"country": "France"
},
{
"code": "de-DE",
"language": "German",
"country": "Germany"
},
{
"code": "pt-BR",
"language": "Portuguese",
"country": "Brazil"
},
{
"code": "zh-CN",
"language": "Chinese",
"country": "China"
},
{
"code": "ar-SA",
"language": "Arabic",
"country": "Saudi Arabia"
},
{
"code": "hi-IN",
"language": "Hindi",
"country": "India"
},
{
"code": "ja-JP",
"language": "Japanese",
"country": "Japan"
},
{
"code": "ru-RU",
"language": "Russian",
"country": "Russia"
}
]
}
Get Translations for Specific Language
curl -X GET http://localhost:8080/api/v1/languages/fr-FR
Response:
{
"timestamp": "2025-12-06T10:30:45.123Z",
"success": true,
"message": "Operation successful",
"data": {
"welcome": "Bienvenue",
"goodbye": "Au revoir",
"thank_you": "Merci"
}
}
Get Translations for Multiple Languages (Batch)
curl -X POST http://localhost:8080/api/v1/languages/batch \
-H "Content-Type: application/json" \
-d '["en-US", "es-ES", "fr-FR"]'
Response:
{
"timestamp": "2025-12-06T10:30:45.123Z",
"success": true,
"message": "Operation successful",
"data": {
"en-US": {
"welcome": "Welcome",
"goodbye": "Goodbye",
"thank_you": "Thank you"
},
"es-ES": {
"welcome": "Bienvenido",
"goodbye": "AdiΓ³s",
"thank_you": "Gracias"
},
"fr-FR": {
"welcome": "Bienvenue",
"goodbye": "Au revoir",
"thank_you": "Merci"
}
}
}
π Complete API Endpoints Reference
Below is a comprehensive table of all available endpoints. For each endpoint, a summary table is provided, followed by detailed sample requests and responses for clarity.
| HTTP Method | Endpoint | Description |
|---|---|---|
GET |
/api/v1/device/keypair |
Generate a new public/private keypair for device |
POST |
/api/v1/device/register |
Register a new biometric device |
GET |
/api/v1/device |
List all devices for the current user |
GET |
/api/v1/device/{deviceId} |
Get details of a specific device |
POST |
/api/v1/biometric/challenge |
Request a biometric authentication challenge |
POST |
/api/v1/biometric/authenticate |
Authenticate using a signed biometric challenge |
POST |
/api/v1/auth/register |
Register a new user |
POST |
/api/v1/auth/login |
Authenticate a user |
POST |
/api/v1/auth/refresh |
Refresh access token |
POST |
/api/v1/auth/forgot-password |
Initiate password reset |
POST |
/api/v1/passkeys/register/start |
Start passkey registration |
POST |
/api/v1/passkeys/register/finish |
Complete passkey registration |
POST |
/api/v1/passkeys/authenticate/start |
Start passkey authentication |
POST |
/api/v1/passkeys/authenticate/finish |
Complete passkey authentication |
GET |
/api/v1/passkeys |
List user's passkeys (paginated) |
PUT |
/api/v1/passkeys/{credentialId} |
Update a passkey |
DELETE |
/api/v1/passkeys/{credentialId} |
Delete a passkey |
GET |
/api/v1/users |
Get a list of users |
GET |
/api/v1/users/{userId} |
Get details of a specific user |
PUT |
/api/v1/users/{userId} |
Update user details |
DELETE |
/api/v1/users/{userId} |
Delete a user |
POST |
/api/v1/users/{userId}/deactivate |
Deactivate a user |
POST |
/api/v1/users/{userId}/unlock |
Unlock a locked user account |
GET |
/api/v1/users/{userId}/referrals |
Get referrals made by a specific user |
POST |
/api/v1/admin/users |
Create a new user (Admin) |
GET |
/api/v1/admin/users |
Get all users across all tenants (Admin) |
POST |
/api/v1/admin/users/{userId}/deactivate |
Deactivate a user (Admin) |
POST |
/api/v1/admin/users/{userId}/unlock |
Unlock a user account (Admin) |
GET |
/api/v1/profile |
Get the current user's profile |
PUT |
/api/v1/profile |
Update the current user's profile |
PUT |
/api/v1/profile/address |
Update the current user's address |
POST |
/api/v1/profile/deactivate |
Deactivate the current user's account |
GET |
/api/v1/roles |
Get a list of roles |
POST |
/api/v1/roles |
Create a new role |
GET |
/api/v1/roles/{roleId} |
Get details of a specific role |
PUT |
/api/v1/roles |
Update role details |
DELETE |
/api/v1/roles/{roleId} |
Delete a role |
POST |
/api/v1/roles/{roleId}/assign-user/{userId} |
Assign role to user |
DELETE |
/api/v1/roles/{roleId}/remove-user/{userId} |
Remove role from user |
GET |
/api/v1/permissions |
Get a list of permissions |
GET |
/api/v1/languages |
Get list of supported languages |
GET |
/api/v1/languages/{lang} |
Get all translations for a specific language |
GET |
/api/v1/languages/all/translations |
Get all translations for all supported languages |
POST |
/api/v1/languages/batch |
Get translations for multiple specified languages |
POST |
/api/v1/otp/generate |
Generate an OTP |
POST |
/api/v1/otp/validate |
Validate an OTP |
POST |
/api/v1/otp/resend |
Resend an OTP |
POST |
/api/v1/verifications/request-email |
Request email verification |
POST |
/api/v1/verifications |
Verify email or phone |
GET |
/api/v1/tenants |
Get a list of tenants |
POST |
/api/v1/tenants |
Create a new tenant |
GET |
/api/v1/tenants/{tenantId} |
Get tenant by ID |
PUT |
/api/v1/tenants/{tenantId} |
Update tenant details |
DELETE |
/api/v1/tenants/{tenantId} |
Delete a tenant |
PATCH |
/api/v1/tenants/{tenantId}/status |
Update the status of a tenant |
Tip: All endpoints are versioned under
/api/v1/for consistency.
Device Endpoints
Generate Device Keypair
GET /api/v1/device/keypair
Response:
{
"publicKey": "BASE64_ENCODED_PUBLIC_KEY",
"privateKey": "BASE64_ENCODED_PRIVATE_KEY"
}
Register Device
POST /api/v1/device/register
Request:
{
"deviceName": "Pixel 7",
"deviceModel": "GXLX",
"osName": "Android",
"osVersion": "14",
"publicKey": "BASE64_ENCODED_PUBLIC_KEY"
}
{
"timestamp": "2025-05-04T00:55:29.721615Z",
"success": true,
"message": "Operation successful",
"data": {
"deviceId": {
"userId": "uuid",
"tenantId": "uuid",
"deviceId": "uuid",
"deviceName": "Samsung",
"deviceModel": "A2",
"osName": "Android",
"osVersion": "Oreo",
"ipAddress": "0:0:0:0:0:0:0:1",
"deviceFingerprint": "548b83d28879fa9adec87c231a4eeba07bc9ea387c1d735d4d65df8dfb796275",
"publicKey": "BASE64_ENCODED_PUBLIC_KEY",
"biometricEnabled": true,
"active": true,
"created": "date",
"updated": "date"
},
"token": {
"accessToken": "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIEZVIn0******",
"refreshToken": "eyJhbGciOiJIUzUxMiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICI0****",
"expiresIn": 300,
"refreshExpiresIn": 1800
},
"biometricEnabled": true,
"qrCodeData": null
},
"metadata": {
"processingTime": "0ms",
"serverId": "Olaolus-MacBook-Pro.local",
"version": "1.0.0"
}
}
List Devices for Current User
GET /api/v1/device
Response:
{
"timestamp": "2025-05-04T01:23:45.123Z",
"success": true,
"message": "Operation successful",
"data": {
"content": [
{
"userId": "uuid",
"tenantId": "uuid",
"deviceId": "uuid",
"deviceName": "Samsung",
"deviceModel": "A2",
"osName": "Android",
"osVersion": "Oreo",
"ipAddress": "0:0:0:0:0:0:0:1",
"deviceFingerprint": "548b83d28879fa9adec87c231a4eeba07bc9ea387c1d735d4d65df8dfb796275",
"publicKey": "BASE64_ENCODED_PUBLIC_KEY",
"biometricEnabled": true,
"active": true,
"created": "2025-05-04T01:00:00Z",
"updated": "2025-05-04T01:00:00Z"
}
],
"pageable": {
"pageNumber": 0,
"pageSize": 1,
"offset": 0,
"paged": true,
"unpaged": false
},
"totalPages": 1,
"totalElements": 1,
"last": true,
"first": true,
"numberOfElements": 1,
"size": 1,
"number": 0,
"empty": false
},
"metadata": {
"processingTime": "0ms",
"serverId": "Olaolus-MacBook-Pro.local",
"version": "1.0.0"
}
}
Get Device by ID
GET /api/v1/device/{deviceId}
Response:
{
"timestamp": "2025-05-04T01:23:45.123Z",
"success": true,
"message": "Operation successful",
"data": {
"userId": "uuid",
"tenantId": "uuid",
"deviceId": "uuid",
"deviceName": "Samsung",
"deviceModel": "A2",
"osName": "Android",
"osVersion": "Oreo",
"ipAddress": "0:0:0:0:0:0:0:1",
"deviceFingerprint": "548b83d28879fa9adec87c231a4eeba07bc9ea387c1d735d4d65df8dfb796275",
"publicKey": "BASE64_ENCODED_PUBLIC_KEY",
"biometricEnabled": true,
"active": true,
"created": "2025-05-04T01:00:00Z",
"updated": "2025-05-04T01:00:00Z"
},
"metadata": {
"processingTime": "0ms",
"serverId": "Olaolus-MacBook-Pro.local",
"version": "1.0.0"
}
}
Biometric Authentication Endpoints
Request Biometric Challenge
POST /api/v1/biometric/challenge
Request:
{
"deviceId": "b1c2d3e4-5678-1234-9abc-def012345678"
}
{
"challenge": "random-challenge-string",
"sessionToken": "random-session-token"
}
Authenticate with Biometric Signature
POST /api/v1/biometric/authenticate
Request:
{
"sessionToken": "random-session-token",
"signature": "BASE64_SIGNATURE"
}
{
"sessionToken": {
"accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refreshToken": "...",
"expiresIn": 3600,
"refreshExpiresIn": 18000
},
"user": { /* user object */ },
"device": { /* device object */ }
}
Auth Endpoints
Register User
POST /api/v1/auth/register
Request:
{
"email": "user@example.com",
"password": "Password123!",
"firstName": "John",
"lastName": "Doe"
}
{
"timestamp": "2025-05-07T10:00:00.000Z",
"success": true,
"message": "User registered successfully",
"data": {
"userId": "uuid",
"email": "user@example.com",
"firstName": "John",
"lastName": "Doe",
"roles": ["USER"],
"created": "2025-05-07T10:00:00.000Z"
}
}
Login
POST /api/v1/auth/login
Request:
{
"email": "user@example.com",
"password": "Password123!"
}
{
"accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refreshToken": "...",
"expiresIn": 3600,
"refreshExpiresIn": 18000
}
Forgot Password
POST /api/v1/auth/forgot-password
Request:
{
"email": "user@example.com"
}
{
"success": true,
"message": "Password reset instructions sent to your email."
}
User Endpoints
List Users
GET /api/v1/users
Query Parameters:
- q (optional): Search query to filter users by name, email, etc.
- roleId (optional): Filter users by specific role ID
- page (optional): Page number (0-based, default: 0)
- size (optional): Number of items per page (default: 20)
- sort (optional): Sort criteria (e.g., createdAt,desc)
Example Requests:
GET /api/v1/users
GET /api/v1/users?q=john&page=0&size=10&sort=createdAt,desc
GET /api/v1/users?roleId=123e4567-e89b-12d3-a456-426614174000
Response:
{
"timestamp": "2025-09-07T12:00:00.000Z",
"success": true,
"message": "Operation successful",
"data": [
{
"userId": "uuid",
"email": "user@example.com",
"firstName": "John",
"lastName": "Doe",
"roles": ["USER"],
"created": "2025-09-07T12:00:00.000Z"
}
],
"totalElements": 1,
"totalPages": 1,
"page": 0,
"size": 20,
"metadata": {
"processingTime": "30ms",
"serverId": "user-service-01",
"version": "1.0.0"
}
}
Get User by ID
GET /api/v1/users/{userId}
Response:
{
"userId": "uuid",
"email": "user@example.com",
"firstName": "John",
"lastName": "Doe",
"roles": ["USER"],
"created": "2025-05-07T10:00:00.000Z"
}
Update User
PUT /api/v1/users/{userId}
Request:
{
"firstName": "Jane",
"lastName": "Smith",
"email": "jane.smith@example.com"
}
{
"userId": "uuid",
"email": "jane.smith@example.com",
"firstName": "Jane",
"lastName": "Smith",
"roles": ["USER"],
"updated": "2025-05-07T10:10:00.000Z"
}
Delete User
DELETE /api/v1/users/{userId}
Response:
{
"success": true,
"message": "User deleted successfully"
}
Deactivate User
POST /api/v1/users/{userId}/deactivate
Response:
{
"success": true,
"message": "User deactivated successfully"
}
Unlock User
POST /api/v1/users/{userId}/unlock
Response:
{
"success": true,
"message": "User account unlocked successfully"
}
Get Current User Profile
GET /api/v1/users/profile
Response:
{
"userId": "uuid",
"email": "user@example.com",
"firstName": "John",
"lastName": "Doe",
"roles": ["USER"]
}
Get User Referrals
GET /api/v1/users/{userId}/referrals
Description: Retrieves a paginated list of users that were referred by the specified user. This endpoint uses projections for efficient querying and returns referral-specific information.
Parameters:
- userId (Path): UUID of the user whose referrals to fetch
- page (Query, optional): Page number (default: 0)
- size (Query, optional): Page size (default: 20)
- sort (Query, optional): Sort criteria (e.g., "created,desc")
Response:
{
"timestamp": "2025-06-27T10:00:00.000Z",
"success": true,
"message": "Operation successful",
"data": [
{
"id": "uuid",
"referredEmail": "referred-user@example.com",
"referredFirstName": "Jane",
"referredLastName": "Smith",
"referralDate": "2025-06-20T10:00:00.000Z",
"status": "COMPLETED",
"rewardIssued": false
},
{
"id": "uuid",
"referredEmail": "another-user@example.com",
"referredFirstName": "Bob",
"referredLastName": "Johnson",
"referralDate": "2025-06-15T14:30:00.000Z",
"status": "PENDING",
"rewardIssued": false
}
],
"totalElements": 2,
"totalPages": 1,
"page": 0,
"size": 20,
"metadata": {
"processingTime": "45ms",
"serverId": "user-service-01",
"version": "1.0.0"
}
}
Error Response:
{
"timestamp": "2025-06-27T10:00:00.000Z",
"success": false,
"message": "User not found with ID: {userId}",
"status": "NOT_FOUND",
"path": "/api/v1/users/{userId}/referrals"
}
Profile Endpoints
Get Profile
GET /api/v1/profile
Response:
{
"userId": "uuid",
"email": "user@example.com",
"firstName": "John",
"lastName": "Doe",
"phone": "+1234567890"
}
Update Profile
PUT /api/v1/profile
Request:
{
"firstName": "Jane",
"lastName": "Smith",
"phone": "+1234567890"
}
{
"userId": "uuid",
"email": "jane.smith@example.com",
"firstName": "Jane",
"lastName": "Smith",
"phone": "+1234567890"
}
Update Address
PUT /api/v1/profile/address
Request:
{
"street": "123 Main St",
"city": "Lagos",
"state": "LA",
"country": "Nigeria",
"zip": "100001"
}
{
"success": true,
"message": "Address updated successfully"
}
#### Deactivate Current User Profile
**POST /api/v1/profile/deactivate**
_Response:_
```json
{
"success": true,
"message": "User account deactivated successfully"
}
---
### Role Endpoints
#### List Roles
**GET /api/v1/roles**
_Response:_
```json
{
"timestamp": "2025-06-27T10:00:00.000Z",
"success": true,
"message": "Operation successful",
"data": [
{
"id": "uuid",
"name": "admin",
"description": "Administrator role",
"permissions": [
{
"id": "uuid",
"name": "user:read",
"description": "Read user data",
"scope": "TENANT"
}
]
}
],
"totalElements": 1,
"totalPages": 1,
"page": 0,
"size": 20,
"metadata": {
"processingTime": "40ms",
"serverId": "user-service-01",
"version": "1.0.0"
}
}
Create Role
POST /api/v1/roles
Request:
{
"name": "admin",
"description": "Administrator role",
"permissionIds": ["uuid"]
}
{
"id": "uuid",
"name": "admin",
"description": "Administrator role",
"permissions": [
{ "id": "uuid", "name": "user:read", "description": "Read user data" }
]
}
Update Role
PUT /api/v1/roles/{id}
Request:
{
"id": "uuid",
"name": "manager",
"description": "Manager role",
"permissionIds": ["uuid"]
}
{
"id": "uuid",
"name": "manager",
"description": "Manager role",
"permissions": [
{ "id": "uuid", "name": "user:read", "description": "Read user data" }
]
}
Delete Role
DELETE /api/v1/roles/{id}
Response:
{
"success": true,
"message": "Role deleted successfully"
}
Assign Role to User
POST /api/v1/roles/{roleId}/assign-user/{userId}
Description: Assigns a specific role to a user. This operation updates both the local database and Keycloak, ensuring the role appears in the user's JWT token.
Parameters:
- roleId (Path): UUID of the role to assign
- userId (Path): UUID of the user to assign the role to
Response:
{
"timestamp": "2025-06-27T10:00:00.000Z",
"success": true,
"message": "Role assigned to user successfully",
"metadata": {
"processingTime": "150ms",
"serverId": "user-service-01",
"version": "1.0.0"
}
}
Error Responses:
{
"timestamp": "2025-06-27T10:00:00.000Z",
"success": false,
"message": "Role not found with ID: {roleId}",
"status": "NOT_FOUND",
"path": "/api/v1/roles/{roleId}/assign-user/{userId}"
}
{
"timestamp": "2025-06-27T10:00:00.000Z",
"success": false,
"message": "User already has this role assigned",
"status": "CONFLICT",
"path": "/api/v1/roles/{roleId}/assign-user/{userId}"
}
Remove Role from User
DELETE /api/v1/roles/{roleId}/remove-user/{userId}
Description: Removes a specific role from a user. This operation updates both the local database and Keycloak, ensuring the role is removed from the user's JWT token.
Parameters:
- roleId (Path): UUID of the role to remove
- userId (Path): UUID of the user to remove the role from
Response:
{
"timestamp": "2025-06-27T10:00:00.000Z",
"success": true,
"message": "Role removed from user successfully",
"metadata": {
"processingTime": "120ms",
"serverId": "user-service-01",
"version": "1.0.0"
}
}
Error Responses:
{
"timestamp": "2025-06-27T10:00:00.000Z",
"success": false,
"message": "User not found with ID: {userId}",
"status": "NOT_FOUND",
"path": "/api/v1/roles/{roleId}/remove-user/{userId}"
}
{
"timestamp": "2025-06-27T10:00:00.000Z",
"success": false,
"message": "User does not have this role assigned",
"status": "CONFLICT",
"path": "/api/v1/roles/{roleId}/remove-user/{userId}"
}
Admin User Endpoints
Note: These endpoints require administrative privileges and are protected with and specific authority checks.
Create User (Admin)
POST /api/v1/admin/users
Description: Creates a new user in the system. This endpoint is restricted to administrators and allows creating users across different tenants.
Request:
{
"email": "newuser@example.com",
"firstName": "John",
"lastName": "Doe",
"password": "SecurePass123!",
"phone": "+1234567890",
"roleIds": ["uuid-role-1", "uuid-role-2"],
"address": {
"street": "123 Main St",
"city": "Lagos",
"state": "LA",
"country": "Nigeria",
"zipCode": "100001"
}
}
Response:
{
"timestamp": "2025-09-07T10:00:00.000Z",
"success": true,
"message": "Operation successful",
"data": {
"userId": "uuid",
"email": "newuser@example.com",
"firstName": "John",
"lastName": "Doe",
"phone": "+1234567890",
"roles": [
{
"id": "uuid-role-1",
"name": "USER",
"description": "Standard user role"
}
],
"tenant": {
"id": "uuid-tenant",
"name": "Default Tenant",
"code": "DEFAULT"
},
"created": "2025-09-07T10:00:00.000Z",
"status": "ACTIVE"
},
"metadata": {
"processingTime": "150ms",
"serverId": "user-service-01",
"version": "1.0.0"
}
}
Get Users by Tenant ID (Admin)
GET /api/v1/admin/users/by-tenant/{tenantId}
Description: Retrieves all users belonging to a specific tenant. Administrators can access users from any tenant.
Parameters:
- tenantId (Path): UUID of the tenant
- page (Query, optional): Page number (default: 0)
- size (Query, optional): Page size (default: 20)
- sort (Query, optional): Sort criteria
Response:
{
"timestamp": "2025-09-07T10:00:00.000Z",
"success": true,
"message": "Operation successful",
"data": {
"content": [
{
"userId": "uuid-1",
"email": "user1@example.com",
"firstName": "John",
"lastName": "Doe",
"roles": ["USER"],
"status": "ACTIVE",
"created": "2025-09-07T09:00:00.000Z"
},
{
"userId": "uuid-2",
"email": "user2@example.com",
"firstName": "Jane",
"lastName": "Smith",
"roles": ["ADMIN"],
"status": "ACTIVE",
"created": "2025-09-07T08:00:00.000Z"
}
],
"totalElements": 2,
"totalPages": 1,
"page": 0,
"size": 20
},
"metadata": {
"processingTime": "45ms",
"serverId": "user-service-01",
"version": "1.0.0"
}
}
Get All Users (Admin)
GET /api/v1/admin/users
Description: Retrieves all users across all tenants. This endpoint provides system-wide visibility for administrators.
Parameters:
- page (Query, optional): Page number (default: 0)
- size (Query, optional): Page size (default: 20)
- sort (Query, optional): Sort criteria
- q (Query, optional): Search query for filtering users
- roleId (Query, optional): Filter by role ID
- tenantId (Query, optional): Filter by tenant ID
Response:
{
"timestamp": "2025-09-07T10:00:00.000Z",
"success": true,
"message": "Operation successful",
"data": {
"content": [
{
"userId": "uuid-1",
"email": "user1@tenant1.com",
"firstName": "John",
"lastName": "Doe",
"tenant": {
"id": "tenant-1",
"name": "Tenant One",
"code": "TENANT1"
},
"roles": ["USER"],
"status": "ACTIVE"
},
{
"userId": "uuid-2",
"email": "user2@tenant2.com",
"firstName": "Jane",
"lastName": "Smith",
"tenant": {
"id": "tenant-2",
"name": "Tenant Two",
"code": "TENANT2"
},
"roles": ["ADMIN"],
"status": "ACTIVE"
}
],
"totalElements": 2,
"totalPages": 1,
"page": 0,
"size": 20
},
"metadata": {
"processingTime": "60ms",
"serverId": "user-service-01",
"version": "1.0.0"
}
}
Deactivate User (Admin)
POST /api/v1/admin/users/{userId}/deactivate
Description: Deactivates a user account. This prevents the user from logging in while preserving their data.
Response:
{
"timestamp": "2025-09-07T10:00:00.000Z",
"success": true,
"message": "User deactivated successfully",
"metadata": {
"processingTime": "30ms",
"serverId": "user-service-01",
"version": "1.0.0"
}
}
Unlock User Account (Admin)
POST /api/v1/admin/users/{userId}/unlock
Description: Unlocks a locked user account and resets the failed login attempts counter.
Response:
{
"timestamp": "2025-09-07T10:00:00.000Z",
"success": true,
"message": "User account unlocked successfully",
"metadata": {
"processingTime": "25ms",
"serverId": "user-service-01",
"version": "1.0.0"
}
}
Permission Endpoints
List Permissions
GET /api/v1/permissions
Description: Retrieves a paginated list of permissions with optional search functionality.
Parameters:
- q (Query, optional): Search query to filter permissions by name or description
- page (Query, optional): Page number (default: 0)
- size (Query, optional): Page size (default: 20)
- sort (Query, optional): Sort criteria (e.g., "name,asc")
Response:
{
"timestamp": "2025-06-27T10:00:00.000Z",
"success": true,
"message": "Operation successful",
"data": [
{
"id": "uuid",
"name": "user:read",
"description": "Read user data",
"scope": "TENANT"
},
{
"id": "uuid",
"name": "user:write",
"description": "Create and update user data",
"scope": "TENANT"
},
{
"id": "uuid",
"name": "system:admin",
"description": "Full system administration access",
"scope": "SUPER_ADMIN"
}
],
"totalElements": 3,
"totalPages": 1,
"page": 0,
"size": 20,
"metadata": {
"processingTime": "25ms",
"serverId": "user-service-01",
"version": "1.0.0"
}
}
Create Permission
POST /api/v1/permissions
Description: Creates a new permission with the specified name, description, and scope.
Request:
{
"name": "product:read",
"description": "Read product data",
"scope": "TENANT"
}
Response:
{
"timestamp": "2025-06-27T10:00:00.000Z",
"success": true,
"message": "Permission created successfully",
"data": {
"id": "uuid",
"name": "product:read",
"description": "Read product data",
"scope": "TENANT"
},
"metadata": {
"processingTime": "80ms",
"serverId": "user-service-01",
"version": "1.0.0"
}
}
Error Response:
{
"timestamp": "2025-06-27T10:00:00.000Z",
"success": false,
"message": "Permission already exists: product:read",
"status": "CONFLICT",
"path": "/api/v1/permissions"
}
Permission Scope Values
TENANT: Permission applies within a specific tenant contextADMIN: Permission applies within a specific tenant contextSUPER_ADMIN: Permission applies across all tenants (system-wide access)
OTP Endpoints
Generate OTP
POST /api/v1/otp/generate
Request:
{
"identifier": "user@example.com",
"type": "EMAIL_VERIFICATION"
}
{
"success": true,
"message": "OTP generated successfully for user@example.com. Please check your email for the OTP."
}
Validate OTP
POST /api/v1/otp/validate
Request:
{
"userId": "uuid",
"identifier": "user@example.com",
"code": "123456",
"type": "EMAIL_VERIFICATION"
}
{
"success": true,
"message": "OTP validated successfully for user@example.com."
}
Resend OTP
POST /api/v1/otp/resend
Request:
{
"identifier": "user@example.com",
"type": "EMAIL_VERIFICATION"
}
{
"success": true,
"message": "OTP resent successfully for user@example.com. Please check your email for the OTP."
}
Verification Endpoints
Request Email Verification
POST /api/v1/verifications/request-email
Request:
{
"email": "user@example.com"
}
{
"success": true,
"message": "Verification email sent"
}
Verify Email or Phone
POST /api/v1/verifications
Request:
{
"identifier": "user@example.com",
"otpCode": "123456",
"type": "EMAIL_VERIFICATION"
}
{
"success": true,
"message": "Verification successful"
}
Tenant Endpoints
List Tenants
GET /api/v1/tenants
Response:
{
"content": [
{ "id": "uuid", "name": "Acme Corp", "status": "ACTIVE" }
],
"totalElements": 1
}
Create Tenant
POST /api/v1/tenants
Request:
{
"name": "Acme Corp"
}
{
"id": "uuid",
"name": "Acme Corp",
"status": "ACTIVE"
}
Get Tenant by ID
GET /api/v1/tenants/{tenantId}
Response:
{
"id": "uuid",
"name": "Acme Corp",
"status": "ACTIVE"
}
Update Tenant
PUT /api/v1/tenants/{tenantId}
Request:
{
"name": "Acme Corp Updated"
}
{
"id": "uuid",
"name": "Acme Corp Updated",
"status": "ACTIVE"
}
Delete Tenant
DELETE /api/v1/tenants/{tenantId}
Response:
{
"success": true,
"message": "Tenant deleted successfully"
}
Update Tenant Status
PATCH /api/v1/tenants/{tenantId}/status
Request:
{
"status": "INACTIVE"
}
{
"id": "uuid",
"name": "Acme Corp",
"status": "INACTIVE"
}
π¨ Visual Flow: Biometric Authentication
graph TD
subgraph "π Initial Setup"
A[Generate Keypair] --> B[Register Device]
B --> C[Store Private Key Securely]
end
subgraph "π Authentication Flow"
D[Request Challenge] --> E[User Provides Biometric]
E --> F[Sign Challenge with Private Key]
F --> G[Send Signature to Server]
G --> H{Verify Signature}
H --> I[Issue Access Token]
H --> J[Reject Authentication]
end
subgraph "π Device Management"
K[List Devices] --> L[View Device Details]
L --> M[Update Device Info]
M --> N[Deactivate Device]
end
subgraph "π¨ Error Handling"
O[Challenge Expired] --> D
P[Invalid Signature] --> J
Q[Device Not Found] --> R[Re-register Device]
S[Biometric Unavailable] --> T[Fallback to Password]
end
C --> D
I --> K
J --> O
J --> P
J --> Q
J --> S
π‘οΈ Security Considerations
Key Security Features
- Private Key Never Leaves Device: Keys are generated and stored securely on the device
- Challenge-Response Protocol: Prevents replay attacks with time-limited challenges
- Device Fingerprinting: Unique device identification prevents unauthorized access
- Biometric Verification: Strong user verification through device biometrics
- Token-Based Authentication: Short-lived access tokens with automatic expiration
Best Practices
- Key Storage: Use platform-specific secure storage (KeyStore, Keychain, TPM)
- Challenge Expiration: Implement short challenge lifetimes (1-5 minutes)
- Rate Limiting: Prevent brute force attacks on biometric authentication
- Device Validation: Regularly validate device integrity and biometrics
- Audit Logging: Log all authentication attempts for security monitoring
Security Threats & Mitigations
| Threat | Mitigation |
|---|---|
| Private Key Theft | Secure hardware storage, biometric protection |
| Replay Attacks | Challenge expiration, nonce validation |
| Man-in-the-Middle | HTTPS encryption, certificate pinning |
| Device Cloning | Device fingerprinting, hardware validation |
| Biometric Spoofing | Multi-modal biometrics, liveness detection |
π§ͺ Testing Your Implementation
Unit Testing
// Test keypair generation
describe('Biometric Keypair Generation', () => {
test('should generate valid keypair', async () => {
const keypair = await generateDeviceKeypair();
expect(keypair.publicKey).toBeDefined();
expect(keypair.privateKey).toBeDefined();
});
});
// Test challenge signing
describe('Challenge Signing', () => {
test('should sign challenge correctly', async () => {
const challenge = 'test-challenge-123';
const signature = await signChallengeWithBiometric(challenge);
expect(signature).toBeDefined();
expect(typeof signature).toBe('string');
});
});
Integration Testing
// Test complete authentication flow
describe('Biometric Authentication Flow', () => {
test('should authenticate user with valid biometrics', async () => {
// Mock biometric success
mockBiometricSuccess();
const result = await authenticateWithBiometric();
expect(result.success).toBe(true);
expect(result.data.sessionToken).toBeDefined();
expect(result.data.user).toBeDefined();
});
test('should handle biometric failure', async () => {
// Mock biometric failure
mockBiometricFailure();
await expect(authenticateWithBiometric()).rejects.toThrow('Biometric authentication failed');
});
});
π οΈ Troubleshooting Biometric Authentication
π Common Issues & Solutions
1. "Biometric authentication not available"
Symptoms:
- API returns BAD_REQUEST with message about biometrics not available
- Device registration fails
Solutions:
// Check biometric availability
async function checkBiometricSupport() {
try {
// Check WebAuthn support
if (!window.PublicKeyCredential) {
console.log('WebAuthn not supported');
return false;
}
// Check platform authenticator
const available = await PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable();
console.log('Platform authenticator available:', available);
return available;
} catch (error) {
console.error('Biometric check failed:', error);
return false;
}
}
2. "Challenge expired" Error
Solutions: - Reduce challenge timeout on server (default: 5 minutes) - Implement automatic challenge refresh - Show user-friendly timeout messages
3. "Invalid signature" Error
Debugging:
// Debug signature generation
async function debugSignature() {
const challenge = 'test-challenge-123';
const privateKey = await secureStore.getItem('biometric_private_key');
console.log('Challenge:', challenge);
console.log('Private key length:', privateKey.length);
const signature = await signData(challenge, privateKey);
console.log('Signature:', signature);
return signature;
}
π Performance Optimization
Client-Side Optimizations
- Lazy Loading: Load biometric libraries only when needed
- Caching: Cache device information and public keys
- Background Sync: Sync device status in background
- Memory Management: Clean up cryptographic keys when not needed
Server-Side Optimizations
- Challenge Pool: Pre-generate challenges for faster response
- Signature Caching: Cache recent signature validations
- Database Indexing: Ensure proper indexing on device fingerprints
- Rate Limiting: Implement smart rate limiting based on device trust
π Example Usage Scenarios
- Register a device: Generate a keypair, register the device, and store the private key securely.
- Authenticate biometrically: Request a challenge, sign it with the device's private key, and authenticate.
- List devices: Fetch all registered devices for the current user with pagination support.
- Fetch device details: Get information about a specific device by its ID.
- Handle errors gracefully: Implement comprehensive error handling for various failure scenarios.
- Test implementation: Use provided testing utilities to validate your biometric integration.
π οΈ General Troubleshooting
- Database Connection Issues:
- Ensure Postgres is running and accessible.
- Check JDBC URL (
localhostvspostgresin Docker). - Keycloak Issues:
- Verify Keycloak server URL and credentials.
- Eureka Registration:
- Ensure Eureka server is running and accessible.
- Check
eureka.client.serviceUrl.defaultZone. - Flyway Migration Errors:
- Check migration scripts and DB connectivity.
π License
This project is licensed under the MIT License.
Olara User Service β Secure, scalable, and ready for the cloud.
Designed with β€οΈ by the Olara team.