# Authentication Flow **Document Version:** 1.0 **Last Updated:** 2026-01-10 **Status:** Production LIVE **Flow Type:** User Authentication & Session Management --- ## Overview This document describes the **complete authentication flow** for the Norda Biznes Partner application, covering: - **User Registration** with email verification - **Login** with session management - **Email Verification** process - **Password Reset** flow - **Session Management** and cookies - **Authorization Levels** and access control **Key Technology:** - **Authentication Framework:** Flask-Login - **Password Hashing:** PBKDF2:SHA256 (werkzeug.security) - **Session Storage:** Server-side session cookies - **Email Delivery:** Microsoft Graph API (OAuth 2.0) - **CSRF Protection:** Flask-WTF **Security Features:** - Email verification required before login - Secure session cookies (HttpOnly, Secure, SameSite=Lax) - CSRF protection on all forms - Password strength requirements - Input sanitization against XSS - Rate limiting on authentication endpoints --- ## 1. User Registration Flow ### 1.1 Registration Sequence Diagram ```mermaid sequenceDiagram actor User participant Browser participant Flask as Flask App
(app.py) participant DB as PostgreSQL
(users table) participant EmailSvc as Email Service
(email_service.py) participant MSGraph as Microsoft Graph API User->>Browser: Navigate to /register Browser->>Flask: GET /register Flask->>Browser: Render register.html User->>Browser: Fill form (email, password, name, company_nip) Browser->>Flask: POST /register Note over Flask: CSRF token validated (automatic) Note over Flask: Input sanitization Flask->>Flask: validate_email(email) Flask->>Flask: validate_password(password)
(8+ chars, uppercase, lowercase, digit) Flask->>Flask: validate NIP format (10 digits) Flask->>DB: SELECT * FROM users WHERE email = ? DB->>Flask: Check if exists alt Email already exists Flask->>Browser: Flash "Email już jest zarejestrowany" Browser->>User: Show error message else Email available Flask->>DB: SELECT * FROM companies WHERE nip = ? AND status = 'active' DB->>Flask: Company data (if NORDA member) Flask->>Flask: generate_password_hash(password)
(PBKDF2:SHA256) Flask->>Flask: secrets.token_urlsafe(32)
(verification token) Flask->>Flask: Calculate token expiry
(now + 24 hours) Flask->>DB: INSERT INTO users
(email, password_hash, name,
verification_token, is_verified=FALSE,
company_id, is_norda_member) DB->>Flask: User created (id) Flask->>EmailSvc: send_verification_email(email, token) EmailSvc->>MSGraph: POST /users/noreply@nordabiznes.pl/sendMail
(OAuth 2.0 + Bearer token) MSGraph->>EmailSvc: 202 Accepted Flask->>Browser: Flash "Sprawdź email"
Redirect to /login Browser->>User: Show success message end ``` ### 1.2 Registration Implementation Details **Route:** `POST /register` **File:** `app.py` (lines ~3077-3183) **Rate Limit:** 5 requests per hour per IP **Input Fields:** - `email` (required, max 255 chars) - `password` (required, 8+ chars with complexity requirements) - `name` (required, max 255 chars) - `company_nip` (required, 10 digits) **Validation Steps:** 1. **Email validation:** Regex pattern check 2. **Password validation:** - Minimum 8 characters - At least 1 uppercase letter - At least 1 lowercase letter - At least 1 digit 3. **NIP validation:** Must be exactly 10 digits 4. **Company membership check:** NIP lookup in `companies` table **Database Operations:** ```sql -- Check email uniqueness SELECT * FROM users WHERE email = ?; -- Verify company membership SELECT * FROM companies WHERE nip = ? AND status = 'active'; -- Create user account INSERT INTO users ( email, password_hash, name, company_nip, verification_token, verification_token_expires, is_verified, is_norda_member, company_id, created_at ) VALUES (?, ?, ?, ?, ?, ?, FALSE, ?, ?, NOW()); ``` **Security Measures:** - **CSRF Protection:** Automatic via Flask-WTF - **Input Sanitization:** `sanitize_input()` strips HTML/malicious patterns - **Password Hashing:** PBKDF2:SHA256 (werkzeug default) - **Rate Limiting:** 5 attempts/hour via Flask-Limiter - **XSS Prevention:** All user inputs sanitized **Email Verification Token:** - Generated via `secrets.token_urlsafe(32)` (256-bit entropy) - Stored in `users.verification_token` column - Expires after 24 hours - Single-use (cleared after verification) --- ## 2. Email Verification Flow ### 2.1 Email Verification Sequence Diagram ```mermaid sequenceDiagram actor User participant Email as Email Client participant Browser participant Flask as Flask App
(app.py) participant DB as PostgreSQL Note over User,Email: User receives verification email User->>Email: Open verification email Email->>Browser: Click link:
https://nordabiznes.pl/verify-email/ Browser->>Flask: GET /verify-email/ Flask->>Flask: Extract token from URL Flask->>DB: SELECT * FROM users
WHERE verification_token = ?
AND verification_token_expires > NOW()
AND is_active = TRUE alt Token valid and not expired DB->>Flask: User found alt User already verified Flask->>Browser: Flash "Email został już zweryfikowany"
Redirect to /login else User not verified yet Flask->>DB: UPDATE users SET
is_verified = TRUE,
verified_at = NOW(),
verification_token = NULL,
verification_token_expires = NULL
WHERE id = ? DB->>Flask: Update successful Flask->>Browser: Flash "Email zweryfikowany!
Możesz się teraz zalogować"
Redirect to /login Browser->>User: Show success message end else Token invalid or expired Flask->>Browser: Flash "Link weryfikacyjny
jest nieprawidłowy lub wygasł"
Redirect to /login Browser->>User: Show error message end ``` ### 2.2 Email Verification Implementation Details **Route:** `GET /verify-email/` **File:** `app.py` (lines ~3369-3405) **Rate Limit:** None (public endpoint) **Verification Logic:** ```python # Query user by token user = db.query(User).filter( User.verification_token == token, User.verification_token_expires > datetime.now(), User.is_active == True ).first() if user and not user.is_verified: user.is_verified = True user.verified_at = datetime.now() user.verification_token = None user.verification_token_expires = None db.commit() ``` **Token Expiry:** - Verification tokens expire after **24 hours** - Expired tokens cannot be used - Users can request new verification email via `/resend-verification` **Database Schema (users table):** ```sql verification_token VARCHAR(255) NULL verification_token_expires TIMESTAMP NULL is_verified BOOLEAN DEFAULT FALSE verified_at TIMESTAMP NULL ``` --- ## 3. Login Flow ### 3.1 Login Sequence Diagram ```mermaid sequenceDiagram actor User participant Browser participant Flask as Flask App
(app.py) participant FlaskLogin as Flask-Login participant DB as PostgreSQL participant Session as Session Cookie User->>Browser: Navigate to /login Browser->>Flask: GET /login Flask->>Browser: Render login.html with CSRF token User->>Browser: Enter email & password
(optional: remember me) Browser->>Flask: POST /login
(email, password, remember, csrf_token) Note over Flask: CSRF token validated Flask->>DB: SELECT * FROM users WHERE email = ? alt User not found DB->>Flask: No user Flask->>Browser: Flash "Nieprawidłowy email lub hasło" Browser->>User: Show error else User found DB->>Flask: User data Flask->>Flask: check_password_hash(
user.password_hash,
password
) alt Password invalid Flask->>Browser: Flash "Nieprawidłowy email lub hasło" Browser->>User: Show error else Password valid alt User not active Flask->>Browser: Flash "Konto zostało dezaktywowane" Browser->>User: Show error else User not verified Flask->>Browser: Flash "Musisz potwierdzić adres email" Browser->>User: Show error with resend link else User active and verified Flask->>FlaskLogin: login_user(user, remember=remember) FlaskLogin->>Session: Set session cookie
(secure, httponly, samesite=Lax) Session->>Browser: Store session cookie Flask->>DB: UPDATE users SET last_login = NOW() WHERE id = ? DB->>Flask: Update successful Flask->>Browser: Redirect to /dashboard
(or 'next' URL if specified) Browser->>User: Show dashboard end end end ``` ### 3.2 Login Implementation Details **Route:** `POST /login` **File:** `app.py` (lines ~3184-3240) **Rate Limit:** - **Development:** 1000 requests per hour - **Production:** 5 requests per hour per IP **Login Validation Steps:** 1. Check email exists in database 2. Verify password hash matches 3. Check `is_active = TRUE` 4. Require `is_verified = TRUE` 5. Create session via Flask-Login 6. Update `last_login` timestamp **Session Configuration:** ```python # app.py session settings (lines ~161-168) app.config['SECRET_KEY'] = os.getenv('SECRET_KEY') app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(days=7) app.config['SESSION_COOKIE_SECURE'] = True # HTTPS only in production app.config['SESSION_COOKIE_HTTPONLY'] = True # No JS access app.config['SESSION_COOKIE_SAMESITE'] = 'Lax' # CSRF protection ``` **Flask-Login Configuration:** ```python # app.py Flask-Login setup (lines ~192-196) login_manager = LoginManager() login_manager.init_app(app) login_manager.login_view = 'login' login_manager.login_message = 'Zaloguj się, aby uzyskać dostęp do tej strony.' ``` **User Loader Function:** ```python @login_manager.user_loader def load_user(user_id): """Load user from database for Flask-Login""" db = SessionLocal() try: return db.query(User).get(int(user_id)) except: return None finally: db.close() ``` **Remember Me Functionality:** - When enabled: Session cookie persists for **7 days** - When disabled: Session cookie expires when browser closes - Implemented via Flask-Login's `login_user(user, remember=True/False)` **Next URL Redirect:** ```python # Prevent open redirect vulnerability next_page = request.args.get('next') if next_page and not next_page.startswith('/'): next_page = None return redirect(next_page or url_for('dashboard')) ``` --- ## 4. Session Management ### 4.1 Session Lifecycle ```mermaid stateDiagram-v2 [*] --> Anonymous: User visits site Anonymous --> Authenticated: Successful login Authenticated --> Authenticated: Normal activity Authenticated --> Anonymous: Logout Authenticated --> Anonymous: Session expires (7 days) Authenticated --> Anonymous: User deactivated Anonymous --> [*] ``` ### 4.2 Session Cookie Details **Cookie Name:** `session` (Flask default) **Storage:** Server-side (encrypted session data) **Attributes:** - `Secure` = True (HTTPS only in production) - `HttpOnly` = True (prevents XSS cookie theft) - `SameSite` = Lax (CSRF protection) - `Max-Age` = 7 days (with remember me) **Session Data Stored:** - User ID (for user loader) - CSRF token (automatic via Flask-WTF) - Login timestamp - Remember me flag **Session Security:** - Session cookie is **signed** with `SECRET_KEY` - Session data is **encrypted** (Flask built-in) - Session is **regenerated** on login (prevents session fixation) - Session is **cleared** on logout --- ## 5. Logout Flow ### 5.1 Logout Sequence Diagram ```mermaid sequenceDiagram actor User participant Browser participant Flask as Flask App participant FlaskLogin as Flask-Login participant Session as Session Cookie User->>Browser: Click "Wyloguj" button Browser->>Flask: GET /logout Note over Flask: @login_required decorator
verifies user is authenticated Flask->>FlaskLogin: logout_user() FlaskLogin->>Session: Clear session data Session->>Browser: Delete session cookie Flask->>Browser: Flash "Wylogowano pomyślnie"
Redirect to / Browser->>User: Show homepage (logged out) ``` ### 5.2 Logout Implementation Details **Route:** `GET /logout` **File:** `app.py` (lines ~3242-3248) **Authentication:** Required (`@login_required`) **Implementation:** ```python @app.route('/logout') @login_required def logout(): """User logout""" logout_user() # Flask-Login clears session flash('Wylogowano pomyślnie.', 'success') return redirect(url_for('index')) ``` **What Happens on Logout:** 1. Flask-Login calls `logout_user()` 2. Session cookie is deleted 3. User object is removed from `current_user` 4. Browser redirected to homepage 5. All subsequent requests are anonymous --- ## 6. Password Reset Flow ### 6.1 Password Reset Sequence Diagram ```mermaid sequenceDiagram actor User participant Browser participant Flask as Flask App participant DB as PostgreSQL participant EmailSvc as Email Service participant MSGraph as Microsoft Graph API Note over User,Browser: Phase 1: Request Reset User->>Browser: Navigate to /forgot-password Browser->>Flask: GET /forgot-password Flask->>Browser: Render forgot_password.html User->>Browser: Enter email address Browser->>Flask: POST /forgot-password (email) Flask->>Flask: validate_email(email) Flask->>DB: SELECT * FROM users
WHERE email = ? AND is_active = TRUE alt User found DB->>Flask: User data Flask->>Flask: secrets.token_urlsafe(32)
(generate reset token) Flask->>Flask: Calculate expiry (now + 1 hour) Flask->>DB: UPDATE users SET
reset_token = ?,
reset_token_expires = ?
WHERE email = ? DB->>Flask: Update successful Flask->>EmailSvc: send_password_reset_email(email, token) EmailSvc->>MSGraph: POST /users/noreply@nordabiznes.pl/sendMail MSGraph->>EmailSvc: 202 Accepted Flask->>Browser: Flash "Sprawdź email"
Redirect to /login Browser->>User: Show message else User not found Note over Flask: Still show success message
(prevent email enumeration) Flask->>Browser: Flash "Sprawdź email"
Redirect to /login Browser->>User: Show message end Note over User,Browser: Phase 2: Reset Password User->>Browser: Click link in email:
https://nordabiznes.pl/reset-password/ Browser->>Flask: GET /reset-password/ Flask->>DB: SELECT * FROM users
WHERE reset_token = ?
AND reset_token_expires > NOW() alt Token valid DB->>Flask: User found Flask->>Browser: Render reset_password.html
(password form) User->>Browser: Enter new password (twice) Browser->>Flask: POST /reset-password/
(new_password, confirm_password) Flask->>Flask: validate_password(new_password) Flask->>Flask: Check passwords match Flask->>Flask: generate_password_hash(new_password) Flask->>DB: UPDATE users SET
password_hash = ?,
reset_token = NULL,
reset_token_expires = NULL
WHERE reset_token = ? DB->>Flask: Update successful Flask->>Browser: Flash "Hasło zostało zmienione"
Redirect to /login Browser->>User: Show success message else Token invalid or expired Flask->>Browser: Flash "Link resetowania
jest nieprawidłowy lub wygasł"
Redirect to /forgot-password Browser->>User: Show error end ``` ### 6.2 Password Reset Implementation Details **Routes:** - `POST /forgot-password` - Request reset - `GET /reset-password/` - Show reset form - `POST /reset-password/` - Process new password **Files:** `app.py` (lines ~3251-3368) **Rate Limit:** 5 requests per hour per IP **Reset Token Properties:** - Generated via `secrets.token_urlsafe(32)` (256-bit entropy) - Expires after **1 hour** - Single-use (cleared after successful reset) - Stored in `users.reset_token` column **Database Schema:** ```sql reset_token VARCHAR(255) NULL reset_token_expires TIMESTAMP NULL ``` **Security Considerations:** - Reset tokens expire after 1 hour - Tokens are cleared after use - Password strength validation applied - Email enumeration prevented (always show success message) - Rate limiting prevents brute force --- ## 7. Authorization & Access Control ### 7.1 Authorization Levels ```mermaid graph TB subgraph "Authorization Hierarchy" Public[👥 Public
Anonymous Users] Auth[🔐 Authenticated
Logged-in Users] Member[👔 NORDA Members
is_norda_member=TRUE] Admin[👨‍💼 Administrators
is_admin=TRUE] Public --> Auth Auth --> Member Member --> Admin end subgraph "Access Permissions" PublicRoutes["Public Routes:
/, /search, /company/*,
/audit/*, /api/companies"] AuthRoutes["Authenticated Routes:
/dashboard, /chat,
/forum/*, /wiadomosci/*,
/kalendarz/*, /tablica/*"] AdminRoutes["Admin Routes:
/admin/*, /api/*/audit"] end Public --> PublicRoutes Auth --> AuthRoutes Admin --> AdminRoutes ``` ### 7.2 Route Protection Decorators **Public Access (No decorator):** ```python @app.route('/') def index(): """Public company directory""" # No authentication required return render_template('index.html') ``` **Authenticated Users Only:** ```python @app.route('/dashboard') @login_required def dashboard(): """User dashboard - requires login""" # current_user is automatically available return render_template('dashboard.html', user=current_user) ``` **Admin Only (Custom check):** ```python @app.route('/admin/users') @login_required def admin_users(): """Admin user management""" if not current_user.is_admin: flash('Brak uprawnień administratora.', 'error') return redirect(url_for('index')) # Admin logic here return render_template('admin/users.html') ``` ### 7.3 Access Control Matrix | Route Category | Public | Authenticated | NORDA Member | Admin | |---------------|--------|---------------|--------------|-------| | `/` (Company directory) | ✅ | ✅ | ✅ | ✅ | | `/search` | ✅ | ✅ | ✅ | ✅ | | `/company/` | ✅ | ✅ | ✅ | ✅ | | `/audit/*/` | ✅ | ✅ | ✅ | ✅ | | `/api/companies` | ✅ | ✅ | ✅ | ✅ | | `/register`, `/login` | ✅ | ❌ | ❌ | ❌ | | `/dashboard` | ❌ | ✅ | ✅ | ✅ | | `/chat` | ❌ | ✅ | ✅ | ✅ | | `/forum/*` | ❌ | ✅ | ✅ | ✅ | | `/wiadomosci/*` | ❌ | ✅ | ✅ | ✅ | | `/kalendarz/*` | ❌ | ✅ | ✅ | ✅ | | `/tablica/*` | ❌ | ✅ | ✅ | ✅ | | `/admin/*` | ❌ | ❌ | ❌ | ✅ | | `/api/*/audit` | ❌ | ❌ | ❌ | ✅ | **Legend:** - ✅ Access granted - ❌ Access denied (redirect to login or show error) --- ## 8. User Model Database Schema ### 8.1 Users Table Structure ```sql CREATE TABLE users ( -- Primary Key id SERIAL PRIMARY KEY, -- Authentication email VARCHAR(255) UNIQUE NOT NULL, password_hash VARCHAR(255) NOT NULL, -- Profile name VARCHAR(255), phone VARCHAR(50), -- Company Association company_nip VARCHAR(10), company_id INTEGER REFERENCES companies(id), -- Status Flags is_active BOOLEAN DEFAULT TRUE, is_verified BOOLEAN DEFAULT FALSE, is_admin BOOLEAN DEFAULT FALSE, is_norda_member BOOLEAN DEFAULT FALSE, -- Timestamps created_at TIMESTAMP DEFAULT NOW(), last_login TIMESTAMP, verified_at TIMESTAMP, -- Email Verification verification_token VARCHAR(255), verification_token_expires TIMESTAMP, -- Password Reset reset_token VARCHAR(255), reset_token_expires TIMESTAMP, -- Indexes INDEX idx_users_email (email), INDEX idx_users_company_id (company_id) ); ``` ### 8.2 User Model (SQLAlchemy) **File:** `database.py` (lines ~119-158) ```python class User(Base, UserMixin): """User accounts with Flask-Login integration""" __tablename__ = 'users' # Authentication fields id = Column(Integer, primary_key=True) email = Column(String(255), unique=True, nullable=False, index=True) password_hash = Column(String(255), nullable=False) # Profile name = Column(String(255)) company_nip = Column(String(10)) company_id = Column(Integer, ForeignKey('companies.id'), nullable=True) phone = Column(String(50)) # Status is_active = Column(Boolean, default=True) is_verified = Column(Boolean, default=False) is_admin = Column(Boolean, default=False) is_norda_member = Column(Boolean, default=False) # Timestamps created_at = Column(DateTime, default=datetime.now) last_login = Column(DateTime) verified_at = Column(DateTime) # Verification token verification_token = Column(String(255)) verification_token_expires = Column(DateTime) # Password reset token reset_token = Column(String(255)) reset_token_expires = Column(DateTime) # Relationships company = relationship('Company', backref='users', lazy='joined') conversations = relationship('AIChatConversation', back_populates='user') forum_topics = relationship('ForumTopic', back_populates='author') forum_replies = relationship('ForumReply', back_populates='author') ``` **UserMixin Methods (Flask-Login):** - `is_authenticated` - Always True for logged-in users - `is_active` - Returns `self.is_active` - `is_anonymous` - Always False for logged-in users - `get_id()` - Returns `str(self.id)` for session storage --- ## 9. Security Features Summary ### 9.1 Security Measures Implemented | Feature | Implementation | Protection Against | |---------|----------------|---------------------| | **CSRF Protection** | Flask-WTF automatic tokens | Cross-Site Request Forgery | | **Password Hashing** | PBKDF2:SHA256 (werkzeug) | Rainbow table attacks | | **Secure Cookies** | HttpOnly, Secure, SameSite=Lax | XSS cookie theft, CSRF | | **Email Verification** | Required before login | Fake accounts | | **Input Sanitization** | `sanitize_input()` function | XSS attacks | | **Rate Limiting** | Flask-Limiter (5 req/hour) | Brute force attacks | | **Session Regeneration** | Flask-Login automatic | Session fixation | | **Token Expiry** | 24h (verify), 1h (reset) | Token replay attacks | | **Open Redirect Prevention** | Next URL validation | Phishing attacks | | **Password Strength** | 8+ chars, complexity rules | Weak passwords | ### 9.2 Security Best Practices **Implemented:** ✅ HTTPS enforced in production ✅ Password hashing with secure algorithm ✅ CSRF protection on all forms ✅ Session cookies with security flags ✅ Email verification required ✅ Rate limiting on auth endpoints ✅ Input sanitization ✅ Token expiry ✅ Open redirect prevention **Potential Improvements:** ⚠️ Add account lockout after N failed attempts ⚠️ Implement 2FA (TOTP) for admins ⚠️ Add password history (prevent reuse) ⚠️ Log authentication events for auditing ⚠️ Add CAPTCHA on registration ⚠️ Implement session timeout (idle) ⚠️ Add IP-based rate limiting --- ## 10. Testing Accounts (Production) ### 10.1 Test Users **IMPORTANT:** Use only these accounts for testing production features. | Account | Email | Role | Purpose | |---------|-------|------|---------| | Test User | `test@nordabiznes.pl` | Regular User | Test user-level features | | Test Admin | `testadmin@nordabiznes.pl` | Administrator | Test admin features | **Test Account Credentials:** - Password stored in CLAUDE.md (do not commit to repository) - Accounts are pre-verified (`is_verified = TRUE`) - Test Admin has `is_admin = TRUE` flag **Usage:** - Always use test accounts for production testing - Never modify real user accounts for testing - Test authentication flows, authorization, session management --- ## 11. Related Documentation ### 11.1 Architecture Documents - [System Context Diagram](../01-system-context.md) - External actors and integrations - [Container Diagram](../02-container-diagram.md) - Flask app and session storage - [Flask Components](../04-flask-components.md) - Authentication routes and decorators - [Database Schema](../05-database-schema.md) - Users table and relationships ### 11.2 Code Files **Authentication Routes:** - `app.py` lines ~3077-3500 (register, login, logout, verify, reset) **User Model:** - `database.py` lines ~119-158 (User model with Flask-Login) **Email Service:** - `email_service.py` (Microsoft Graph email sending) **Security Utilities:** - `app.py` - `sanitize_input()`, `validate_email()`, `validate_password()` ### 11.3 External Dependencies - **Flask-Login:** User session management - **Flask-WTF:** CSRF protection - **Flask-Limiter:** Rate limiting - **werkzeug.security:** Password hashing - **secrets:** Cryptographic token generation --- ## 12. Maintenance & Updates ### 12.1 When to Update This Document Update this document when: - Authentication flow changes (new steps, validation) - Session management changes (cookie settings, expiry) - Security measures are added/modified - New authorization levels are introduced - User model schema changes ### 12.2 Verification Checklist When updating authentication flow: - [ ] Test registration with valid/invalid data - [ ] Test email verification with valid/expired tokens - [ ] Test login with correct/incorrect credentials - [ ] Test session persistence (remember me) - [ ] Test logout clears session - [ ] Test password reset flow end-to-end - [ ] Verify CSRF protection is active - [ ] Verify rate limiting works - [ ] Test authorization levels (public, user, admin) - [ ] Verify security headers on cookies ### 12.3 Security Audit Checklist Periodic security audit: - [ ] Review password hashing algorithm (current: PBKDF2:SHA256) - [ ] Check token expiry times (verify: 24h, reset: 1h) - [ ] Verify session cookie security flags - [ ] Review rate limiting thresholds - [ ] Check for SQL injection vulnerabilities - [ ] Test XSS prevention in inputs - [ ] Verify CSRF protection coverage - [ ] Review authentication logs for anomalies - [ ] Test open redirect prevention - [ ] Verify email verification enforcement --- **Document End** *This document is maintained as part of the Norda Biznes Partner architecture documentation. For questions or updates, contact the development team.*