========================================================================= Issue #48: feat: Implement strong authentication for voting (itsme, eID) ========================================================================= :State: **OPEN** :Milestone: Jalon 1: Sécurité & GDPR 🔒 :Labels: phase:k8s,track:software priority:low,proptech:blockchain :Assignees: Unassigned :Created: 2025-10-27 :Updated: 2025-11-13 :URL: `View on GitHub `_ Description =========== .. raw:: html
:: ## Context **Belgian legal requirement:** General assembly votes require **positive identification** of voters to ensure: - Only legitimate co-owners vote - One person = one owner (no impersonation) - Non-repudiation (votes cannot be contested) - Audit trail for legal disputes **Current authentication:** ⚠️ Basic JWT (email + password) - Sufficient for read operations - **Insufficient for legally binding votes** (AG resolutions) - No identity verification - Risk of fraud/impersonation ## Legal Framework (Belgium) **Code civil (Copropriété):** - AG votes are legally binding - Requires proof of identity for contested votes - Minutes (procès-verbaux) must be legally defensible **eIDAS Regulation (EU):** - Electronic identification for cross-border services - Levels: Low, Substantial, **High** (required for voting) **Acceptable Belgian authentication methods:** 1. **itsme®** - Most popular (5M+ users in Belgium) 2. **Belgian eID** (electronic identity card + card reader) 3. **Qualified electronic signature** (eIDAS compliant) ## Objective Implement **strong authentication** specifically for voting operations: - itsme® integration (primary) - Belgian eID support (secondary) - Step-up authentication (elevate from JWT to strong auth when voting) - Audit trail for authenticated votes ## Proposed Architecture ### Two-Tier Authentication **Tier 1 - Standard (JWT):** - Email + password - Used for: browsing, viewing data, non-critical actions - Current implementation ✅ **Tier 2 - Strong Authentication:** - itsme® or eID - **Required for:** casting votes, signing documents - New implementation ❌ ### Step-Up Authentication Flow ``` User logged in (JWT) → Navigates to voting page ↓ Clicks "Vote" button ↓ System detects: requires strong auth ↓ Redirect to itsme® authentication ↓ User authenticates via itsme® app ↓ itsme® returns identity + verification level ↓ System validates identity matches owner record ↓ Vote is cast with strong auth token ↓ Audit log records: owner_id, itsme® transaction_id ``` ## itsme® Integration ### 1. itsme® Overview **What is itsme®?** - Belgian/EU digital identity app - 5+ million active users in Belgium - Used by banks, government, utilities - Mobile app-based (QR code or deeplink) - eIDAS "High" level assurance **Authentication flow:** 1. User scans QR code or clicks deeplink 2. Opens itsme® app on phone 3. Authenticates with PIN/biometric 4. itsme® returns verified identity (name, national register number) ### 2. itsme® OpenID Connect (OIDC) Integration **Provider:** itsme® acts as OpenID Connect Identity Provider **Registration:** - Create account at https://portal.itsme.be/ - Obtain Client ID & Client Secret - Configure redirect URIs **Scopes requested:** ``` openid profile email service:koprogo_voting (custom service code) ``` **Claims returned:** ```json { "sub": "itsme_user_id", "name": "John Doe", "given_name": "John", "family_name": "Doe", "birthdate": "1980-01-01", "national_register_number": "80010112345", // Belgian NISS "email": "john@example.com", "phone_number": "+32470123456", "verified": true, "ial": "http://itsme.services/IAL/HIGH" // Identity Assurance Level } ``` ### 3. Backend Implementation **New entity:** `StrongAuthSession` ```rust // backend/src/domain/entities/strong_auth_session.rs pub struct StrongAuthSession { pub id: Uuid, pub user_id: Uuid, pub owner_id: Uuid, pub auth_provider: AuthProvider, // Itsme, Eid pub provider_transaction_id: String, // itsme transaction ID pub national_register_number: String, // NISS (encrypted) pub identity_assurance_level: String, // "HIGH" pub authenticated_at: DateTime, pub expires_at: DateTime, // 15 minutes pub used_for: String, // "vote:resolution_id" } pub enum AuthProvider { Itsme, BelgianEid, } ``` **Database table:** ```sql CREATE TABLE strong_auth_sessions ( id UUID PRIMARY KEY, user_id UUID NOT NULL REFERENCES users(id), owner_id UUID NOT NULL REFERENCES owners(id), auth_provider VARCHAR(50) NOT NULL, provider_transaction_id VARCHAR(255) NOT NULL, national_register_number_encrypted TEXT NOT NULL, -- Encrypted NISS identity_assurance_level VARCHAR(50) NOT NULL, authenticated_at TIMESTAMP NOT NULL, expires_at TIMESTAMP NOT NULL, used_for VARCHAR(255) NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); CREATE INDEX idx_strong_auth_user ON strong_auth_sessions(user_id); CREATE INDEX idx_strong_auth_expires ON strong_auth_sessions(expires_at); ``` **itsme® OIDC flow:** ```rust // backend/src/infrastructure/auth/itsme_provider.rs use openidconnect::{ AuthenticationFlow, AuthorizationCode, ClientId, ClientSecret, CsrfToken, IssuerUrl, Nonce, RedirectUrl, Scope, }; pub struct ItsmeProvider { client: CoreClient, } impl ItsmeProvider { pub fn new() -> Self { let issuer = IssuerUrl::new("https://idp.e2e.itsme.services/v2".to_string()).unwrap(); let client_id = ClientId::new(env::var("ITSME_CLIENT_ID").unwrap()); let client_secret = ClientSecret::new(env::var("ITSME_CLIENT_SECRET").unwrap()); let redirect_url = RedirectUrl::new(env::var("ITSME_REDIRECT_URI").unwrap()).unwrap(); let client = CoreClient::new(client_id, Some(client_secret), issuer, redirect_url); Self { client } } // Generate authorization URL (redirect user to itsme®) pub fn get_authorization_url(&self, state: String) -> (String, CsrfToken, Nonce) { let (auth_url, csrf_token, nonce) = self.client .authorize_url( AuthenticationFlow::::AuthorizationCode, CsrfToken::new, Nonce::new, ) .add_scope(Scope::new("openid".to_string())) .add_scope(Scope::new("profile".to_string())) .add_scope(Scope::new("service:koprogo_voting".to_string())) .set_state(CsrfToken::new(state)) .url(); (auth_url.to_string(), csrf_token, nonce) } // Exchange authorization code for tokens pub async fn exchange_code(&self, code: AuthorizationCode) -> Result { self.client .exchange_code(code) .request_async(async_http_client) .await .map_err(|e| e.to_string()) } // Verify ID token and extract claims pub async fn verify_identity(&self, id_token: &str) -> Result { // Verify JWT signature, expiration, issuer // Extract claims // Validate IAL = HIGH Ok(ItsmeIdentity { sub: "...".to_string(), name: "John Doe".to_string(), national_register_number: "80010112345".to_string(), identity_assurance_level: "HIGH".to_string(), }) } } ``` ### 4. API Endpoints **Initiate strong authentication:** ``` GET /api/v1/auth/strong/initiate?purpose=vote&resolution_id= → Returns: { "auth_url": "https://idp.itsme.services/...", "state": "csrf_token" } ``` **Callback from itsme®:** ``` GET /api/v1/auth/strong/callback?code=&state= → Validates code, creates StrongAuthSession, returns strong_auth_token ``` **Verify strong auth (middleware):** ```rust #[derive(Debug)] pub struct StrongAuth { pub session: StrongAuthSession, } impl FromRequest for StrongAuth { async fn from_request(req: &HttpRequest, _: &mut Payload) -> Result { let strong_auth_token = req .headers() .get("X-Strong-Auth-Token") .and_then(|h| h.to_str().ok()) .ok_or(ErrorUnauthorized("Strong auth required"))?; // Validate token, check expiration let session = validate_strong_auth_token(strong_auth_token).await?; Ok(StrongAuth { session }) } } ``` **Protected vote endpoint:** ```rust #[post("/resolutions/{id}/vote")] async fn cast_vote( resolution_id: web::Path, vote_data: web::Json, _strong_auth: StrongAuth, // Requires strong auth ) -> Result { // Cast vote with strong auth session logged Ok(HttpResponse::Ok().json(vote)) } ``` ### 5. Frontend Integration **Strong auth button:** ```svelte ``` **Vote with strong auth:** ```svelte async function castVote(resolutionId: string, choice: VoteChoice) { const strongAuthToken = sessionStorage.getItem('strong_auth_token'); const response = await fetch(`/api/v1/resolutions/${resolutionId}/vote`, { method: 'POST', headers: { 'Authorization': `Bearer ${localStorage.getItem('token')}`, // Regular JWT 'X-Strong-Auth-Token': strongAuthToken, // Strong auth token 'Content-Type': 'application/json', }, body: JSON.stringify({ vote_choice: choice }), }); if (response.ok) { showToast('Vote enregistré avec authentification forte'); } else { showToast('Authentification forte expirée, veuillez vous réauthentifier'); } } ``` ## Belgian eID Support (Secondary) **Alternative for users without itsme®:** **Flow:** 1. User connects eID card reader to computer 2. Backend calls Belgian eID middleware (via PKCS#11 or Web eID) 3. User enters PIN on card reader 4. Backend validates certificate chain 5. Extract national register number from eID certificate **Implementation complexity:** Higher (requires middleware installation) **Recommendation:** Phase 2 (after itsme® working) ## Security Considerations 1. **National Register Number encryption:** - Encrypt NISS at rest (AES-256) - Never log NISS in plain text - Access only for audit/dispute resolution 2. **Token expiration:** - Strong auth token valid **15 minutes only** - Cannot be reused for different resolutions - Revoked after vote cast 3. **CSRF protection:** - State parameter validated - Nonce validated in ID token 4. **Audit trail:** - Log all strong auth attempts (success/failure) - Link votes to itsme® transaction IDs - Immutable audit log 5. **Privacy (GDPR):** - Minimal data collection (only required claims) - Delete strong auth sessions after 90 days (audit retention) - User consent for itsme® authentication ## Testing - [ ] itsme® sandbox environment working - [ ] Authorization flow completes successfully - [ ] ID token validated correctly - [ ] Identity matches owner record (NISS comparison) - [ ] Strong auth token expires after 15 min - [ ] Vote endpoint rejects without strong auth - [ ] Vote endpoint accepts with valid strong auth - [ ] Audit log records itsme® transaction ID ## Acceptance Criteria - [ ] itsme® OIDC integration complete - [ ] StrongAuthSession entity implemented - [ ] Database schema with encrypted NISS storage - [ ] API endpoints (initiate, callback) functional - [ ] Strong auth middleware working - [ ] Vote endpoint protected by strong auth - [ ] Frontend itsme® button + callback handling - [ ] Audit trail complete - [ ] GDPR compliant (minimal data, consent) - [ ] itsme® logo usage approved (branding guidelines) - [ ] Legal review passed (if applicable) ## Dependencies **Backend:** ```toml [dependencies] openidconnect = "3.5" jsonwebtoken = "9.2" ring = "0.17" # For encryption ``` **Environment variables:** ```bash ITSME_CLIENT_ID= ITSME_CLIENT_SECRET= ITSME_REDIRECT_URI=https://koprogo.com/auth/itsme/callback ITSME_ISSUER=https://idp.e2e.itsme.services/v2 # Sandbox # ITSME_ISSUER=https://idp.prd.itsme.services/v2 # Production NISS_ENCRYPTION_KEY=<32-byte-key> # For NISS encryption ``` ## itsme® Registration Process 1. Create account: https://portal.itsme.be/ 2. Submit service request (KoproGo voting) 3. Provide legal entity info (company registration) 4. Sign itsme® Terms of Service 5. Integration review (sandbox testing) 6. Production approval (~2-4 weeks) **Cost:** Free for basic usage, paid plans for high volume ## Effort Estimate **Large** (5-7 days) - Day 1-2: itsme® registration + sandbox setup - Day 3-4: Backend OIDC integration + StrongAuthSession - Day 5: Frontend integration + UX - Day 6: Testing + audit trail - Day 7: Security review + documentation ## Related - **Blocks:** Issue #46 (Meeting voting system) - Strong auth required before votes - Supports: Legal compliance (Belgian copropriété law) - Enhances: Security posture ## Future Enhancements (Post-MVP) - Belgian eID support (card reader) - Qualified electronic signature (eIDAS) - French FranceConnect integration (if expanding to France) - Mobile biometric authentication (fallback) ## References - itsme® Developer Portal: https://brand.belgianmobileid.be/ - itsme® Technical Documentation: https://belgianmobileid.github.io/doc/ - OpenID Connect: https://openid.net/connect/ - eIDAS Regulation: https://ec.europa.eu/digital-building-blocks/sites/display/DIGITAL/eIDAS - Belgian eID: https://eid.belgium.be/ .. raw:: html