Skip to content

πŸ§‘β€πŸ’» 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

  • 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]
Hold "Alt" / "Option" to enable pan & zoom

πŸ› οΈ 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-client for 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

  1. Client Request: Frontend requests social login URL for a specific provider
  2. Keycloak Redirect: User is redirected to the social provider's OAuth page
  3. Provider Authentication: User authenticates with the social provider
  4. Callback: Social provider redirects back to Keycloak with authorization code
  5. Token Exchange: Keycloak exchanges code for user info and creates/updates user
  6. JWT Issuance: Keycloak issues JWT token with user information
  7. 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:

  1. Access Keycloak Admin Console
  2. Navigate to your realm (e.g., olara-realm)
  3. Go to Identity Providers section
  4. Add Identity Provider for each social platform:
  5. Google: Use OpenID Connect v1.0
  6. Facebook: Use Facebook
  7. Twitter: Use Twitter
  8. Instagram: Use Instagram
  9. Apple: Use Apple

  10. Configure each provider with:

  11. Client ID and Client Secret
  12. Redirect URI: http://localhost:8090/realms/olara-realm/broker/{provider}/endpoint
  13. 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
Hold "Alt" / "Option" to enable pan & zoom

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
Hold "Alt" / "Option" to enable pan & zoom

Security Considerations

  1. ID Token Verification: The backend verifies the Firebase ID token to ensure authenticity
  2. Secure Token Storage: Use platform-specific secure storage (Keychain, KeyStore)
  3. HTTPS Only: Always use HTTPS for API communications
  4. Tenant Isolation: Include tenant ID in request headers for multi-tenant support
  5. 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:

  1. New user is automatically registered - No separate registration endpoint needed
  2. Email is auto-verified βœ… - Firebase/social providers handle verification
  3. User profile is auto-created from Firebase data:
  4. Email, name, profile picture
  5. Phone number (if available)
  6. Provider-specific unique ID
  7. Social connection is recorded - Links provider account to user
  8. Access & refresh tokens are returned - User is immediately logged in
  9. New user status can be determined - Check if user.created == user.updated to identify new users

Auto-Verified Fields

Field Auto-Verified Reason
Email βœ… 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

  1. Firebase Admin SDK: Enhanced token verification with Firebase Admin SDK
  2. Device Biometric Binding: Link social login to device biometrics
  3. Token Refresh: Automatic token refresh for long-lived sessions
  4. Account Linking: Allow linking multiple social providers to one account
  5. 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

  1. User Registration with Referral Code:
  2. Users can provide a referral code during registration
  3. The system validates the referral code and links the new user to the referrer
  4. A Referral record is created to track the relationship

  5. Referral Code Generation:

  6. Each user gets a unique referral code upon registration
  7. Codes are generated using a secure random algorithm
  8. Collision detection with retry mechanism ensures uniqueness

  9. Referral Tracking:

  10. All referrals are stored in the referrals table
  11. Includes referrer, referred user, status, and reward information
  12. 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

  1. System Administration:
  2. Create users for new tenants
  3. Manage user access across the platform
  4. Handle account lockouts and security issues

  5. Tenant Management:

  6. View all users within a specific tenant
  7. Perform bulk operations on tenant users
  8. Monitor user activity and status

  9. Security Operations:

  10. Unlock accounts after failed login attempts
  11. Deactivate compromised accounts
  12. (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 tenants
  • POST /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.defaultZone and related properties.
  • See application.yml and 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 test to 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:

  1. Enter your username on the login page
  2. Click "Login with Passkey"
  3. Choose your passkey from the browser popup (if multiple)
  4. Authenticate using fingerprint, face scan, PIN, or security key
  5. 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:

  1. WebAuthn API: Use the browser's Web Authentication API
  2. Registration: Call navigator.credentials.create() with the options from /register/start
  3. Authentication: Call navigator.credentials.get() with the options from /authenticate/start
  4. 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:

  1. Device Capabilities: Ensure your device supports biometric authentication
  2. Key Storage: Implement secure private key storage (e.g., KeyStore on Android, Keychain on iOS)
  3. Network Security: Use HTTPS for all API communications
  4. 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
Hold "Alt" / "Option" to enable pan & zoom

πŸš€ 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: deviceFingerprint is 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"
}
Response:
{
  "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"
}
Response:
{
  "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"
}
Response:
{
  "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"
}
Response:
{
  "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!"
}
Response:
{
  "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "refreshToken": "...",
  "expiresIn": 3600,
  "refreshExpiresIn": 18000
}

Forgot Password

POST /api/v1/auth/forgot-password

Request:

{
  "email": "user@example.com"
}
Response:
{
  "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"
}
Response:
{
  "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"
}
Response:
{
  "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"
}
Response:
{
  "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"]
}
Response:
{
  "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"]
}
Response:
{
  "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 context
  • ADMIN: Permission applies within a specific tenant context
  • SUPER_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"
}
Response:
{
  "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"
}
Response:
{
  "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"
}
Response:
{
  "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"
}
Response:
{
  "success": true,
  "message": "Verification email sent"
}

Verify Email or Phone

POST /api/v1/verifications

Request:

{
  "identifier": "user@example.com",
  "otpCode": "123456",
  "type": "EMAIL_VERIFICATION"
}
Response:
{
  "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"
}
Response:
{
  "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"
}
Response:
{
  "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"
}
Response:
{
  "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
Hold "Alt" / "Option" to enable pan & zoom

πŸ›‘οΈ 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

  1. Key Storage: Use platform-specific secure storage (KeyStore, Keychain, TPM)
  2. Challenge Expiration: Implement short challenge lifetimes (1-5 minutes)
  3. Rate Limiting: Prevent brute force attacks on biometric authentication
  4. Device Validation: Regularly validate device integrity and biometrics
  5. 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

  1. Lazy Loading: Load biometric libraries only when needed
  2. Caching: Cache device information and public keys
  3. Background Sync: Sync device status in background
  4. Memory Management: Clean up cryptographic keys when not needed

Server-Side Optimizations

  1. Challenge Pool: Pre-generate challenges for faster response
  2. Signature Caching: Cache recent signature validations
  3. Database Indexing: Ensure proper indexing on device fingerprints
  4. 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 (localhost vs postgres in 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.