Authentication Component¶
This component addresses the need to reduce our dependency on country users by removing all authentication logic from the Authenticatable mixin.
Usage¶
The authentication component provides an api to manage authentication for our front office users (company admin and members). In this component SHOULD include: - an api for other components to create/update/delete an identity - a database for linking a profile with an identity - blueprints that can be bootstrapped in the different apps to handle login/logout/registration flows - all the factories for testing authentication flows
⚠️ This component is global and should not include any country specific logic.
State of the migration¶
- [x] All identity management code has been migration
- [x] Login verification has been migrated
- [ ] User isn't linked to an identity anymore (or at least the link isn't used as the source of truth)
- [ ] All token management code has been migrated
How to use the api¶
from components.authentication.public.api import AuthenticationService
authentication_service = AuthenticationService.create()
from components.authentication.public.inject_authentication_service import inject_authentication_service
@inject_authentication_service
def foo(authentication_service: AuthenticationService, other_arg):
...
Backward compatibility¶
During the migration phase, we need to keep backward compatibility with the existing authentication system.
- The RetrocompatibilityRepository is used to read data from the existing authentication system (in the country user models)
- The DoubleWriteRepository is used to maintain the consistency between the new authentication system and the existing one.
A few flow examples¶
Create an identity and assign it a password¶
from components.authentication.public.api import AuthenticationService
authentication_service = AuthenticationService.create()
# Create the identity in keycloak and safe the link between this identity and a profile in the authentication_identity table
identity_id = authentication_service.create_identity(email=email, language=lang, mfa_required=False, profile_id=profile_id)
# Set a password for this identity
authentication_service.set_identity_credentials(identity_id=identity_id, prehashed_password=prehashed_password)
Get an identity¶
from components.authentication.public.api import AuthenticationService
from components.authentication.public.entities import AuthIdentity
authentication_service = AuthenticationService.create()
identity: AuthIdentity | None = authentication_service.get_identity_by_profile_id(profile_id=profile_id)
# OR
identity: AuthIdentity | None = authentication_service.get_identity_by_email(email=email)
# OR
identity: AuthIdentity | None = authentication_service.get_keycloak_identity(keycloak_id=keycloak_id)
if identity is not None:
email = identity.email
first_name = identity.first_name
last_name = identity.last_name
is_mfa_required = identity.mfa_required
is_mfa_enabled = identity.mfa_enabled
Update an identity MFA status¶
from components.authentication.public.api import AuthenticationService
authentication_service = AuthenticationService.create()
authentication_service.change_mfa_status(identity_id=identity_id, mfa_enabled=True, mfa_required=False)
Verify an identity credentials¶
from components.authentication.public.inject_authentication_service import inject_authentication_service
@inject_authentication_service
def verify_credentials(authentication_service: AuthenticationService, user_input):
email = user_input.email
prehashed_password = user_input.prehashed_password
identity: AuthIdentity | None = authentication_service.get_identity_by_email(email=email)
if identity is None:
raise ValueError("No identity found for this email")
if not authentication_service.check_identity_has_password(identity_id=identity.id):
raise ValueError("This identity has no password set")
if not authentication_service.check_identity_password(identity_id=identity.id, prehashed_password=prehashed_password):
raise ValueError("Invalid credentials")
return "Congrats the credentials are valid"
Events¶
The authentication component follows an event-driven architecture, publishing domain events when authentication-related operations occur. These events enable other components to react to authentication changes without tight coupling.
Published Events¶
IdentityCreatedEvent- Emitted when a new identity is successfully created in KeycloakIdentityEmailChangedEvent- Emitted when an identity's email address is successfully changedPasswordResetEmailSentEvent- Emitted when a password reset email is sent to an identityIdentityMergedEvent- Emitted when two identities are merged due to email conflicts during email changesIdentityEmailCleared- Emitted when an identity's email is replaced with an invalidated placeholder
Event-Driven Workflows¶
// TODO: @thibaut.caillierez: modify this documentation once we start using a transaction uow Identity Creation:
identity_id = authentication_service.create_identity(email="user@example.com", language=Lang.ENGLISH)
# → Publishes IdentityCreatedEvent
Email Change with Conflict Resolution:
authentication_service.change_identity_email(identity_id=identity_id, email="existing@example.com")
# → May publish IdentityEmailChangedEvent OR IdentityMergedEvent depending on email availability
Security & Authentication Details¶
Password¶
The authentication component follows strict security practices for credential management:
Pre-hashed Passwords:
- All passwords must be pre-hashed on the client side before being sent to the server
- The component never handles plaintext passwords
- Use check_identity_password() with pre-hashed passwords for verification
Credential Lifecycle:
# Set initial credentials (identity must exist first)
authentication_service.set_identity_credentials(
identity_id=identity_id,
email="user@example.com",
prehashed_password=hashed_password,
is_email_verified=True
)
# Verify credentials during login
has_password = authentication_service.check_identity_has_password(identity_id)
is_valid = authentication_service.check_identity_password(identity_id=identity_id, prehashed_password=hashed_password)
Multi-Factor Authentication (MFA)¶
The component provides granular MFA control with two independent settings:
mfa_enabled- Whether the user has configured MFA (has authenticator app, etc.)mfa_required- Whether MFA is mandatory for this identity (policy enforcement)
MFA States: - Required + Enabled: User must use MFA and has it configured ✅ - Required + Not Enabled: User must configure MFA before login ⚠️ - Not Required + Enabled: User can optionally use MFA - Not Required + Not Enabled: Standard password-only login
# Require MFA for high-privilege users
authentication_service.change_mfa_status(identity_id=admin_identity_id, mfa_required=True)
# User successfully configured MFA
authentication_service.change_mfa_status(identity_id=identity_id, mfa_enabled=True)
CLI Commands¶
The authentication component provides CLI commands for administrative operations and data management.
Available Commands¶
Data Migration & Backfill:
# Backfill authentication_identity table from existing user data across all countries -> It will only work from fr_api, be_api and es_api since they are the only apps with access to all models
flask authentication backfill_users_identity [--dry-run]
# Find keycloak_id conflicts between different country user models
flask authentication find_identity_conlicts
# Check that all users with keycloak_id have corresponding authentication identity
flask authentication check_all_user_have_identity
Identity Data Consistency:
# Fix profile/identity inconsistencies for a specific profile
flask authentication fix_identity_inconsistencies <profile_id> [--dry-run] [--first-name TEXT] [--last-name TEXT] [--language TEXT]
# Fix inconsistencies for multiple profiles (automatically when possible)
flask authentication fix_many_identities_inconsistencies <profile_id1> <profile_id2> ... [--dry-run]
Administrative Operations:
# Reset user credentials (removes password and revokes all sessions)
flask authentication reset_users_credentials <keycloak_id1> <keycloak_id2> ... [--dry-run]
Note: All commands support --dry-run flag to preview changes without applying them.
Testing with TestUserWithAuthenticationFactory¶
For testing authentication flows in your components, use the provided test factory:
from components.authentication.public.tests.test_user_with_authentication_factory import TestUserWithAuthenticationFactory
def test_authentication_flow():
# Create a complete test user with authentication identity
test_user = TestUserWithAuthenticationFactory.create(
email="test@example.com",
password="hashed_password_123",
mfa_required=False
)
# Access the user profile
profile = test_user.profile
assert profile.email == "test@example.com"
# Access the authentication identity
auth_identity = test_user.authentication_identity
assert auth_identity.keycloak_id is not None
# Use with authentication service
authentication_service = AuthenticationService.create()
identity = authentication_service.get_keycloak_identity_by_profile_id(profile.id)
assert identity is not None
assert identity.email == "test@example.com"
def test_mfa_enabled_user():
# Create user with MFA enabled
test_user = TestUserWithAuthenticationFactory.create(
email="mfa-user@example.com",
mfa_required=True,
mfa_enabled=True
)
identity = test_user.authentication_identity
assert identity.mfa_required is True
assert identity.mfa_enabled is True
Developer guide¶
How to use a blueprint in my app¶
How does the api work¶
Read data:¶
- Start a unit of work -> it will build instantiate the proper repository (to read the link between profile and identity) and the proper identity provider (eg to reach keycloak's api)
- Find the requested data in keycloak or in the authentication_identity table depending on the input
- (Optional) Enrich it with data from the authentication_identity table or from keycloak
- Map this data to a public entities and return it
- Exits the unit of work
Write data:¶
- Start a unit of work -> it will build instantiate the proper repository (to write the link between profile and identity) and the proper identity provider (eg to reach keycloak's api)
- Create a command with the input data and add it to the message bus to handle
- The command handler mapped to the command will be called and handle the logic
- it may call the repository to handle data persistence in our DB
- it may call the identity provider to communicate with keycloak
- it may add events to the unit of work's events list
- If there are still domain events in the message bus to handle, repeat step 3 until it's empty
- Exits the unit of work, dispatching events and flushing the session
Component structure¶
authentication/
├── conftest.py # Shared pytest configuration for component tests
├── bootstrap/ # Component bootstrapping and Flask integration
├── internal/
│ ├── application/ # Application layer (CQRS pattern)
│ │ ├── command_handlers.py # Command handlers for business operations
│ │ ├── commands.py # Command definitions (data structures)
│ │ ├── event_handlers.py # Event handlers for domain events
│ │ ├── message_bus.py # Message bus for domain command/event routing
│ │ ├── subscribers.py # Implemenentation of other components' events handlers
│ ├── commands/ # CLI commands implementation
│ ├── controllers/ # Internal API controllers
│ │ └── token.py # Token management endpoints
│ ├── domain/ # Domain layer (business logic)
│ │ ├── entities.py # Domain entities and value objects
│ │ └── events.py # Domain events definitions
│ ├── infrastructure/
│ │ ├── double_write_repository.py # Dual-write pattern for data migration
│ │ ├── event_dispatcher.py # Event dispatching implementation
│ │ ├── flask_auth.py # Flask authentication integration
│ │ ├── identity_provider.py # Identity provider layer, connecting the component to keycloak
│ │ ├── mfa_auth.py # Multi-factor authentication service
│ │ ├── repository.py # Main repository implementation -> writting to authentication.authentication_identity
│ │ ├── retro_compatibility_repository.py # Backward compatibility data access ⚠️ only for retrocompatibility
│ │ ├── unit_of_work.py # Unit of work pattern implementations
│ └── models/
│ ├── authentication_identity.py # Authentication identity ORM model to link an identity to a profile
│ └── helpers.py # Model helper functions
└── public/
├── api.py # Public API interface
├── entities.py # Public entities (data contracts)
├── feature_flags.py # Feature flags configuration
├── identity_provider.py # Public identity provider interface ⚠️ only for retrocompatility
├── inject_authentication_service.py # Dependency injection helpers
├── blueprints/ # Flask blueprints
├── events/
│ ├── component_events.py # Component integration event definitions
│ └── subscription.py # Subscription to other components' events
└── tests/
└── test_user_with_authentication_factory.py # Test factory to create a test user with an identity