---
title: "Server Authentication"
description: "Server Authentication manages credential verification, JWT lifecycle management, API key issuance, and session routing for the self-hosted FastAPI application and Next.js admin dashboard. It implem..."
last_updated: "2026-10-11T17:37:10.257685+00:00"
canonical_url: "https://www.doc0.dev/docs/faa36707-7c28-4f69-a18f-700ff61c704e/technical/server-dashboard/server-authentication"
---

<details>
<summary>Relevant source files</summary>

The following files were used as context for generating this wiki page:

- [server/auth.py](https://github.com/blade47/mem0/blob/b7ad69afda6b6ed030347c66d48a13e4de9dec08/server/auth.py)
- [server/routers/auth.py](https://github.com/blade47/mem0/blob/b7ad69afda6b6ed030347c66d48a13e4de9dec08/server/routers/auth.py)
- [cli/python/src/mem0_cli/commands/init_cmd.py](https://github.com/blade47/mem0/blob/b7ad69afda6b6ed030347c66d48a13e4de9dec08/cli/python/src/mem0_cli/commands/init_cmd.py)
- [integrations/openclaw/cli/commands.ts](https://github.com/blade47/mem0/blob/b7ad69afda6b6ed030347c66d48a13e4de9dec08/integrations/openclaw/cli/commands.ts)
- [server/dashboard/src/app/root/dashboard/api-keys/page.tsx](https://github.com/blade47/mem0/blob/b7ad69afda6b6ed030347c66d48a13e4de9dec08/server/dashboard/src/app/(root)/dashboard/api-keys/page.tsx)
- [server/dashboard/src/app/setup/page.tsx](https://github.com/blade47/mem0/blob/b7ad69afda6b6ed030347c66d48a13e4de9dec08/server/dashboard/src/app/setup/page.tsx)
- [server/main.py](https://github.com/blade47/mem0/blob/b7ad69afda6b6ed030347c66d48a13e4de9dec08/server/main.py)
- [cli/node/src/commands/init.ts](https://github.com/blade47/mem0/blob/b7ad69afda6b6ed030347c66d48a13e4de9dec08/cli/node/src/commands/init.ts)
- [server/dashboard/src/app/api/auth/refresh/route.ts](https://github.com/blade47/mem0/blob/b7ad69afda6b6ed030347c66d48a13e4de9dec08/server/dashboard/src/app/api/auth/refresh/route.ts)
- [server/routers/api_keys.py](https://github.com/blade47/mem0/blob/b7ad69afda6b6ed030347c66d48a13e4de9dec08/server/routers/api_keys.py)
- [server/dashboard/src/lib/auth.tsx](https://github.com/blade47/mem0/blob/b7ad69afda6b6ed030347c66d48a13e4de9dec08/server/dashboard/src/lib/auth.tsx)
- [server/models.py](https://github.com/blade47/mem0/blob/b7ad69afda6b6ed030347c66d48a13e4de9dec08/server/models.py)
- [server/dashboard/src/middleware.ts](https://github.com/blade47/mem0/blob/b7ad69afda6b6ed030347c66d48a13e4de9dec08/server/dashboard/src/middleware.ts)
- [server/alembic/versions/001_create_users_and_api_keys.py](https://github.com/blade47/mem0/blob/b7ad69afda6b6ed030347c66d48a13e4de9dec08/server/alembic/versions/001_create_users_and_api_keys.py)
- [server/dashboard/src/app/auth/login/login-form.tsx](https://github.com/blade47/mem0/blob/b7ad69afda6b6ed030347c66d48a13e4de9dec08/server/dashboard/src/app/(auth)/login/login-form.tsx)
- [server/scripts/reset_admin_password.py](https://github.com/blade47/mem0/blob/b7ad69afda6b6ed030347c66d48a13e4de9dec08/server/scripts/reset_admin_password.py)
- [server/routers/__init__.py](https://github.com/blade47/mem0/blob/b7ad69afda6b6ed030347c66d48a13e4de9dec08/server/routers/__init__.py)
- [mem0-ts/src/oss/src/vector_stores/databricks.ts](https://github.com/blade47/mem0/blob/b7ad69afda6b6ed030347c66d48a13e4de9dec08/mem0-ts/src/oss/src/vector_stores/databricks.ts)
- [server/scripts/seed.sh](https://github.com/blade47/mem0/blob/b7ad69afda6b6ed030347c66d48a13e4de9dec08/server/scripts/seed.sh)
- [server/dashboard/src/utils/api.ts](https://github.com/blade47/mem0/blob/b7ad69afda6b6ed030347c66d48a13e4de9dec08/server/dashboard/src/utils/api.ts)
- [mem0/client/main.py](https://github.com/blade47/mem0/blob/b7ad69afda6b6ed030347c66d48a13e4de9dec08/mem0/client/main.py)
- [mem0-ts/src/oss/src/vector_stores/memory.ts](https://github.com/blade47/mem0/blob/b7ad69afda6b6ed030347c66d48a13e4de9dec08/mem0-ts/src/oss/src/vector_stores/memory.ts)
- [server/alembic/versions/005_refresh_token_jtis.py](https://github.com/blade47/mem0/blob/b7ad69afda6b6ed030347c66d48a13e4de9dec08/server/alembic/versions/005_refresh_token_jtis.py)
- [server/dashboard/src/utils/api-endpoints.ts](https://github.com/blade47/mem0/blob/b7ad69afda6b6ed030347c66d48a13e4de9dec08/server/dashboard/src/utils/api-endpoints.ts)
- [server/routers/requests.py](https://github.com/blade47/mem0/blob/b7ad69afda6b6ed030347c66d48a13e4de9dec08/server/routers/requests.py)
- [server/Makefile](https://github.com/blade47/mem0/blob/b7ad69afda6b6ed030347c66d48a13e4de9dec08/server/Makefile)
- [server/rate_limit.py](https://github.com/blade47/mem0/blob/b7ad69afda6b6ed030347c66d48a13e4de9dec08/server/rate_limit.py)
- [server/dashboard/src/hooks/use-auth.ts](https://github.com/blade47/mem0/blob/b7ad69afda6b6ed030347c66d48a13e4de9dec08/server/dashboard/src/hooks/use-auth.ts)
- [server/telemetry.py](https://github.com/blade47/mem0/blob/b7ad69afda6b6ed030347c66d48a13e4de9dec08/server/telemetry.py)
</details>

## Overview

Server Authentication manages credential verification, JWT lifecycle management, API key issuance, and session routing for the self-hosted [FastAPI application](https://www.doc0.dev/docs/faa36707-7c28-4f69-a18f-700ff61c704e/technical/server-dashboard/fastapi-application) and Next.js admin dashboard. It implements bcrypt password hashing with timing attack mitigation, secure access and refresh token generation, atomic refresh token rotation via database `jti` tracking, and multi-scheme request resolution supporting Bearer tokens, custom `X-API-Key` headers, and legacy administrator keys.

Sources: [server/auth.py:11-165](https://github.com/blade47/mem0/blob/b7ad69afda6b6ed030347c66d48a13e4de9dec08/server/auth.py#L11-L165), [server/routers/auth.py:94-165](https://github.com/blade47/mem0/blob/b7ad69afda6b6ed030347c66d48a13e4de9dec08/server/routers/auth.py#L94-L165)

## Authentication Architecture and Credential Verification

### Credential Hashing and Timing Mitigation

The server utilizes `passlib` with a `CryptContext` configured for the `bcrypt` hashing scheme with automatic deprecation handling. Passwords and API keys are processed through explicit hashing and verification routines.

```python
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")

def hash_password(password: str) -> str:
    return pwd_context.hash(password)

def verify_password(plain: str, hashed: str) -> bool:
    return pwd_context.verify(plain, hashed)
```
Sources: [server/auth.py:22-30](https://github.com/blade47/mem0/blob/b7ad69afda6b6ed030347c66d48a13e4de9dec08/server/auth.py#L22-L30)

To prevent email enumeration via timing attacks during login requests where a target email address does not exist in the database, the authentication flow invokes a dummy password verification method that executes identical bcrypt computation cycles.

```python
def dummy_verify_password() -> None:
    """Burn the same bcrypt cycles as a real verify so login timing doesn't leak whether an email exists."""
    pwd_context.dummy_verify()
```
Sources: [server/auth.py:33-36](https://github.com/blade47/mem0/blob/b7ad69afda6b6ed030347c66d48a13e4de9dec08/server/auth.py#L33-L36)

> [!NOTE]
> `dummy_verify_password` ensures that missing email lookups consume equivalent CPU time to legitimate password verifications, neutralizing response-time side-channel analysis.
> Sources: [server/auth.py:33-36](https://github.com/blade47/mem0/blob/b7ad69afda6b6ed030347c66d48a13e4de9dec08/server/auth.py#L33-L36), [server/routers/auth.py:128-131](https://github.com/blade47/mem0/blob/b7ad69afda6b6ed030347c66d48a13e4de9dec08/server/routers/auth.py#L128-L131)

### User Resolution Mechanisms

Incoming requests can be authenticated via three distinct mechanisms: JSON Web Tokens supplied via Bearer authorization headers, custom API keys transmitted in the `X-API-Key` header, or legacy admin keys. User resolution is decoupled into specific helper functions operating on isolated database sessions.

```mermaid
flowchart TD
    A["Incoming Request"] --> B{"Bearer Token Present?"}
    B -- Yes --> C["_resolve_user_from_jwt"]
    B -- No --> D{"X-API-Key Present?"}
    D -- Yes --> E{"Matches ADMIN_API_KEY?"}
    E -- Yes --> F["Return None (Admin bypass)"]
    E -- No --> G["_resolve_user_from_api_key"]
    D -- No --> H{"AUTH_DISABLED = True?"}
    H -- Yes --> I["Return None (No-auth mode)"]
    H -- No --> J["Raise 401 Unauthorized"]
```
Sources: [server/auth.py:144-175](https://github.com/blade47/mem0/blob/b7ad69afda6b6ed030347c66d48a13e4de9dec08/server/auth.py#L144-L175)

JWT verification extracts the payload, validates that the token type is explicitly set to `access`, and queries the database for the corresponding user ID stored in the subject (`sub`) claim.

```python
def _resolve_user_from_jwt(token: str, db: Session) -> User:
    payload = decode_token(token)
    if payload.get("type") != "access":
        raise HTTPException(status_code=401, detail="Invalid token type.")
    user = db.get(User, payload.get("sub"))
    if user is None:
        raise HTTPException(status_code=401, detail="User not found.")
    return user
```
Sources: [server/auth.py:116-123](https://github.com/blade47/mem0/blob/b7ad69afda6b6ed030347c66d48a13e4de9dec08/server/auth.py#L116-L123)

API key resolution inspects the first 12 characters of the supplied key to isolate candidate records matching the stored `key_prefix`. It then iterates through candidates to verify the plaintext key against stored hashes using `verify_api_key_hash`, updating `last_used_at` upon a successful match.

```python
def _resolve_user_from_api_key(key: str, db: Session) -> User:
    prefix = key[:12] if len(key) >= 12 else key
    candidates = (
        db.execute(select(APIKey).where(APIKey.key_prefix == prefix, APIKey.revoked_at.is_(None))).scalars().all()
    )

    for candidate in candidates:
        if verify_api_key_hash(key, candidate.key_hash):
            candidate.last_used_at = datetime.now(timezone.utc)
            db.commit()
            user = db.get(User, candidate.created_by)
            if user is None:
                raise HTTPException(status_code=401, detail="API key owner not found.")
            return user

    raise HTTPException(status_code=401, detail="Invalid API key.")
```
Sources: [server/auth.py:126-141](https://github.com/blade47/mem0/blob/b7ad69afda6b6ed030347c66d48a13e4de9dec08/server/auth.py#L126-L141)

> [!CAUTION]
> If an API key hash matches a candidate record but the associated creator user (`candidate.created_by`) no longer exists in the database, a `401` HTTP exception is raised with the detail `API key owner not found`.
> Sources: [server/auth.py:136-139](https://github.com/blade47/mem0/blob/b7ad69afda6b6ed030347c66d48a13e4de9dec08/server/auth.py#L136-L139)

## User Registration and Setup Status

### Setup Status Inspection and Password Policies

The initial deployment workflow relies on determining whether an administrator account has been provisioned. The server exposes a setup status route configured at `/auth/setup-status` via the dashboard API endpoint collection, querying the database row count for `User` records. If the user count is precisely zero, the server responds with a boolean indicator signaling that setup is required.

```python
@router.get("/setup-status", response_model=SetupStatusResponse)
def setup_status(db: Session = Depends(get_db)):
    count = db.scalar(select(func.count(User.id)))
    return SetupStatusResponse(needsSetup=count == 0)
```
Sources: [server/routers/auth.py:88-91](https://github.com/blade47/mem0/blob/b7ad69afda6b6ed030347c66d48a13e4de9dec08/server/routers/auth.py#L88-L91), [server/dashboard/src/utils/api-endpoints.ts:2](https://github.com/blade47/mem0/blob/b7ad69afda6b6ed030347c66d48a13e4de9dec08/server/dashboard/src/utils/api-endpoints.ts#L2)

Password length enforcement is handled uniformly across registration and password modification paths. A mandatory validation helper inspects plain text strings against a fixed constant defining the minimum required character length.

| Constant / Function | Value / Type | Purpose |
| :--- | :--- | :--- |
| `MIN_PASSWORD_LENGTH` | `8` (integer) | Enforces the minimum character requirement for administrator passwords |
| `_require_password_length` | Function (`str -> None`) | Evaluates password length and raises HTTP 400 if constraints are violated |

Sources: [server/routers/auth.py:28-36](https://github.com/blade47/mem0/blob/b7ad69afda6b6ed030347c66d48a13e4de9dec08/server/routers/auth.py#L28-L36)

> [!WARNING]
> If a password supplied during administrator registration contains fewer than 8 characters, `_require_password_length` immediately aborts execution by raising an `HTTPException` with status code 400.
> Sources: [server/routers/auth.py:31-36](https://github.com/blade47/mem0/blob/b7ad69afda6b6ed030347c66d48a13e4de9dec08/server/routers/auth.py#L31-L36), [server/routers/auth.py:98](https://github.com/blade47/mem0/blob/b7ad69afda6b6ed030347c66d48a13e4de9dec08/server/routers/auth.py#L98)

### Admin Account Registration and Telemetry Execution

Admin registration is permanently locked down after the first account is successfully persisted. The registration endpoint checks if any user already exists, hashes the verified password, assigns the `"admin"` role, and commits the transaction. To prevent race conditions from concurrent registration requests, database integrity errors trigger a rollback and raise a forbidden exception.

```python
@router.post("/register", response_model=TokenResponse)
@limiter.limit("5/minute")
def register(request: Request, body: RegisterRequest, db: Session = Depends(get_db)):
    """Create the first admin account. Blocked once any user exists."""
    _require_password_length(body.password)

    if db.scalar(select(func.count(User.id))) > 0:
        raise HTTPException(status_code=403, detail="Registration is closed. An admin account already exists.")

    user = User(
        name=body.name,
        email=body.email,
        password_hash=hash_password(body.password),
        role="admin",
    )
    db.add(user)
    try:
        db.commit()
    except IntegrityError:
        db.rollback()
        raise HTTPException(status_code=403, detail="Registration is closed. An admin account already exists.")
    db.refresh(user)

    capture_admin_registered(email=body.email)

    return TokenResponse(
        access_token=create_access_token(str(user.id), user.role),
        refresh_token=create_refresh_token(str(user.id), db),
    )
```
Sources: [server/routers/auth.py:94-122](https://github.com/blade47/mem0/blob/b7ad69afda6b6ed030347c66d48a13e4de9dec08/server/routers/auth.py#L94-L122)

Upon successful creation of the administrator account, the execution flow triggers anonymous telemetry reporting via a specific function sequence. This sequence captures the event state, loads persistent state settings, generates installation identifiers, and initializes the PostHog client interface.

1. `register` — Invokes `capture_admin_registered(email=body.email)` after committing the new user to the database.
2. `capture_admin_registered` — Passes arguments to `_capture_once(email, "admin_registered", "admin_registered_sent_at")`.
3. `_capture_once` — Verifies telemetry enablement, thread locks state access, and checks if the event was previously dispatched.
4. `_get_client` — Resolves or instantiates the PostHog tracking client using `PROJECT_API_KEY` and `HOST`.
5. `_load_state` — Reads persistent metrics configuration from `STATE_PATH`.
6. `_install_id` — Generates or retrieves the unique installation UUID.
7. `_save_state` — Serializes updated telemetry status back to disk.

```mermaid
sequenceDiagram
    autonumber
    participant Router as server/routers/auth.py
    participant Telemetry as server/telemetry.py
    participant State as telemetry.json
    participant PostHog as PostHog Client

    Router->>Telemetry: capture_admin_registered(email)
    Telemetry->>Telemetry: _capture_once(email, "admin_registered", ...)
    Telemetry->>Telemetry: _load_state()
    Telemetry->>State: Read JSON file
    State-->>Telemetry: Return state dict
    Telemetry->>Telemetry: _install_id(state)
    Telemetry->>Telemetry: _get_client()
    Telemetry->>PostHog: client.capture(distinct_id, event, properties)
    Telemetry->>Telemetry: _save_state(state)
    Telemetry->>State: Write updated JSON
```
Sources: [server/routers/auth.py:117](https://github.com/blade47/mem0/blob/b7ad69afda6b6ed030347c66d48a13e4de9dec08/server/routers/auth.py#L117), [server/telemetry.py:35-109](https://github.com/blade47/mem0/blob/b7ad69afda6b6ed030347c66d48a13e4de9dec08/server/telemetry.py#L35-L109)

> [!NOTE]
> Anonymous telemetry transmits domain-level email characteristics, server version strings, and installation UUIDs through PostHog, but can be completely disabled by setting the environment variable `MEM0_TELEMETRY=false`.
> Sources: [server/telemetry.py:1-27](https://github.com/blade47/mem0/blob/b7ad69afda6b6ed030347c66d48a13e4de9dec08/server/telemetry.py#L1-L27)

## JWT Token Generation and Invalidation

### Access and Refresh Token Issuance

Authentication sessions utilize JSON Web Tokens encoded using the HS256 algorithm with a secret key retrieved via `_get_secret()`. If `JWT_SECRET` is unconfigured, the helper raises an HTTP 500 error. Access tokens carry claims for the user ID (`sub`), role, expiration timestamp (`exp`), and a type identifier set to `"access"`, with a fixed lifetime defined by `ACCESS_TOKEN_EXPIRE_MINUTES`.

```python
def create_access_token(user_id: str, role: str) -> str:
    expire = datetime.now(timezone.utc) + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
    payload = {"sub": user_id, "role": role, "exp": expire, "type": "access"}
    return jwt.encode(payload, _get_secret(), algorithm=JWT_ALGORITHM)
```
Sources: [server/auth.py:51-60](https://github.com/blade47/mem0/blob/b7ad69afda6b6ed030347c66d48a13e4de9dec08/server/auth.py#L51-L60)

Refresh tokens operate with an extended expiration window governed by `REFRESH_TOKEN_EXPIRE_DAYS` and incorporate a unique JWT ID (`jti`) generated as a UUID. When a refresh token is created, a corresponding record is persisted in the database via the `RefreshTokenJti` model to track its usage and expiration state.

```python
def create_refresh_token(user_id: str, db: Session) -> str:
    expire = datetime.now(timezone.utc) + timedelta(days=REFRESH_TOKEN_EXPIRE_DAYS)
    jti = uuid.uuid4()
    db.add(RefreshTokenJti(jti=jti, user_id=uuid.UUID(user_id), expires_at=expire))
    db.commit()
    payload = {"sub": user_id, "exp": expire, "jti": str(jti), "type": "refresh"}
    return jwt.encode(payload, _get_secret(), algorithm=JWT_ALGORITHM)
```
Sources: [server/auth.py:63-70](https://github.com/blade47/mem0/blob/b7ad69afda6b6ed030347c66d48a13e4de9dec08/server/auth.py#L63-L70)

### Refresh Token JTI Database Schema

The persistence layer for single-use refresh token tracking is defined by the `refresh_token_jtis` table created in Alembic migration version `005`. The table stores the primary key `jti`, a cascade-deleting foreign key to `users.id`, expiration timestamps with timezone support, and a `used_at` timestamp marking consumption.

| Column | Type | Constraints / Attributes | Purpose |
| :--- | :--- | :--- | :--- |
| `jti` | `sa.Uuid()` | Primary Key | Unique JWT identifier for the refresh token |
| `user_id` | `sa.Uuid()` | Foreign Key (`users.id`, ondelete="CASCADE"), `nullable=False` | Owner account associated with the refresh token |
| `expires_at` | `sa.DateTime(timezone=True)` | `nullable=False`, Indexed (`ix_refresh_token_jtis_expires_at`) | Expiration boundary for token validity |
| `used_at` | `sa.DateTime(timezone=True)` | `nullable=True` | Timestamp recording when the token was consumed |
| `created_at` | `sa.DateTime(timezone=True)` | `nullable=False`, Default `sa.func.now()` | Audit timestamp of JTI record creation |

```python
def upgrade() -> None:
    op.create_table(
        "refresh_token_jtis",
        sa.Column("jti", sa.Uuid(), primary_key=True),
        sa.Column(
            "user_id",
            sa.Uuid(),
            sa.ForeignKey("users.id", ondelete="CASCADE"),
            nullable=False,
        ),
        sa.Column("expires_at", sa.DateTime(timezone=True), nullable=False),
        sa.Column("used_at", sa.DateTime(timezone=True), nullable=True),
        sa.Column("created_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now()),
    )
    op.create_index("ix_refresh_token_jtis_expires_at", "refresh_token_jtis", ["expires_at"])

```
Sources: [server/alembic/versions/005_refresh_token_jtis.py:20-35](https://github.com/blade47/mem0/blob/b7ad69afda6b6ed030347c66d48a13e4de9dec08/server/alembic/versions/005_refresh_token_jtis.py#L20-L35)

### Single-Use Token Rotation and Concurrency Control

To prevent token replay attacks and enforce single-use mechanics, refresh tokens are consumed atomically using a conditional database update. The `consume_refresh_jti` function attempts to set `used_at` only on rows where the `jti` matches, `used_at` is currently null, and `expires_at` exceeds the current UTC time.

```python
def consume_refresh_jti(jti: str, db: Session) -> None:
    try:
        jti_uuid = uuid.UUID(jti)
    except (TypeError, ValueError):
        raise HTTPException(status_code=401, detail="Refresh token is no longer valid.")
    now = datetime.now(timezone.utc)
    result = db.execute(
        update(RefreshTokenJti)
        .where(
            RefreshTokenJti.jti == jti_uuid,
            RefreshTokenJti.used_at.is_(None),
            RefreshTokenJti.expires_at > now,
        )
        .values(used_at=now)
    )
    if result.rowcount == 0:
        raise HTTPException(status_code=401, detail="Refresh token is no longer valid.")
    db.commit()
```
Sources: [server/auth.py:72-94](https://github.com/blade47/mem0/blob/b7ad69afda6b6ed030347c66d48a13e4de9dec08/server/auth.py#L72-L94)

> [!WARNING]
> The conditional `UPDATE` statement in `consume_refresh_jti` closes read-check-write race conditions during concurrent refresh requests. Because concurrent replays target the exact same row, at most one update affects a row and any concurrent replayed requests observe a `rowcount` of 0, immediately triggering an HTTP 401 exception.
> Sources: [server/auth.py:72-94](https://github.com/blade47/mem0/blob/b7ad69afda6b6ed030347c66d48a13e4de9dec08/server/auth.py#L72-L94)

The token refresh router endpoint validates the incoming refresh token payload type, extracts the `jti` claim, checks user existence, invokes `consume_refresh_jti`, and issues a brand new access and refresh token pair upon success.

```python
@router.post("/refresh", response_model=TokenResponse)
@limiter.limit("20/minute")
def refresh(request: Request, body: RefreshRequest, db: Session = Depends(get_db)):
    payload = decode_token(body.refresh_token)
    if payload.get("type") != "refresh":
        raise HTTPException(status_code=401, detail="Invalid token type.")

    jti = payload.get("jti")
    if not jti:
        raise HTTPException(status_code=401, detail="Refresh token is no longer valid.")

    user = db.get(User, payload["sub"])
    if user is None:
        raise HTTPException(status_code=401, detail="User not found.")

    consume_refresh_jti(jti, db)

    return TokenResponse(
        access_token=create_access_token(str(user.id), user.role),
        refresh_token=create_refresh_token(str(user.id), db),
    )
```
Sources: [server/routers/auth.py:144-164](https://github.com/blade47/mem0/blob/b7ad69afda6b6ed030347c66d48a13e4de9dec08/server/routers/auth.py#L144-L164)

## API Key Generation and Resolution

### Key Generation and Hashing Mechanics

The server generates new API keys using cryptographic randomness combined with a fixed string prefix for identification. The `generate_api_key` function generates 32 bytes of URL-safe random data via `secrets.token_urlsafe`, prepends the `m0sk_` format identifier, extracts a 12-character prefix for database indexing, and computes a secure bcrypt hash of the full plaintext key using `CryptContext`.

```python
def generate_api_key() -> tuple[str, str, str]:
    """Returns (full_key, prefix, hash)."""
    raw = secrets.token_urlsafe(32)
    full_key = f"m0sk_{raw}"
    prefix = full_key[:12]
    key_hash = pwd_context.hash(full_key)
    return full_key, prefix, key_hash
```
Sources: [server/auth.py:38-44](https://github.com/blade47/mem0/blob/b7ad69afda6b6ed030347c66d48a13e4de9dec08/server/auth.py#L38-L44)

| Component | Function / Value | Purpose |
| :--- | :--- | :--- |
| Randomness | `secrets.token_urlsafe(32)` | Generates 32 bytes of secure entropy |
| Full Key | `f"m0sk_{raw}"` | Complete plaintext API key returned to the caller once |
| Prefix | `full_key[:12]` | First 12 characters used for efficient key lookup |
| Hashing Scheme | `pwd_context.hash(full_key)` | Secure bcrypt digest stored in the database |

Sources: [server/auth.py:38-44](https://github.com/blade47/mem0/blob/b7ad69afda6b6ed030347c66d48a13e4de9dec08/server/auth.py#L38-L44)

### Request Header Resolution and Authentication Flow

Incoming requests can authenticate through multiple headers inspected by `verify_auth` and dependency helpers. The server reads credentials from either the `Authorization: Bearer <token>` header via `bearer_scheme` or the `X-API-Key` header via `api_key_header`. 

```python
bearer_scheme = HTTPBearer(auto_error=False)
api_key_header = APIKeyHeader(name="X-API-Key", auto_error=False)
```
Sources: [server/auth.py:104-105](https://github.com/blade47/mem0/blob/b7ad69afda6b6ed030347c66d48a13e4de9dec08/server/auth.py#L104-L105)

When an `X-API-Key` header is present, the resolver checks against the global environment variable `ADMIN_API_KEY` using constant-time comparison `secrets.compare_digest`. If it does not match the admin override, it queries the database for active records matching the key's 12-character prefix where `revoked_at` is null. It then verifies each candidate hash, updates `last_used_at` upon success, and resolves the owning user.

```python
def _resolve_user_from_api_key(key: str, db: Session) -> User:
    prefix = key[:12] if len(key) >= 12 else key
    candidates = (
        db.execute(select(APIKey).where(APIKey.key_prefix == prefix, APIKey.revoked_at.is_(None))).scalars().all()
    )

    for candidate in candidates:
        if verify_api_key_hash(key, candidate.key_hash):
            candidate.last_used_at = datetime.now(timezone.utc)
            db.commit()
            user = db.get(User, candidate.created_by)
            if user is None:
                raise HTTPException(status_code=401, detail="API key owner not found.")
            return user

    raise HTTPException(status_code=401, detail="Invalid API key.")
```
Sources: [server/auth.py:126-141](https://github.com/blade47/mem0/blob/b7ad69afda6b6ed030347c66d48a13e4de9dec08/server/auth.py#L126-L141)

> [!NOTE]
> Database sessions for authentication are opened lazily inside short-lived context managers only on branches requiring database queries. This ensures pooled connections are not held open for the entire lifetime of long-running requests.
> Sources: [server/auth.py:149-156](https://github.com/blade47/mem0/blob/b7ad69afda6b6ed030347c66d48a13e4de9dec08/server/auth.py#L149-L156)

## Dashboard Session Refresh and Middleware

### Session Cookie Rotation and Refresh Route

The Next.js dashboard manages authentication persistence via an HTTP-only cookie containing the refresh token (`mem0_refresh_token`). The server route handler exposes `POST`, `PUT`, and `DELETE` methods at `/api/auth/refresh` to rotate tokens, update sessions, and clear credentials.

```typescript
const COOKIE_NAME = "mem0_refresh_token";

const COOKIE_OPTIONS = {
  httpOnly: true,
  secure: shouldUseSecureCookie(),
  sameSite: "lax" as const,
  path: "/",
  maxAge: 30 * 24 * 60 * 60, // 30 days
};
```
Sources: [server/dashboard/src/app/api/auth/refresh/route.ts:6-27](https://github.com/blade47/mem0/blob/b7ad69afda6b6ed030347c66d48a13e4de9dec08/server/dashboard/src/app/api/auth/refresh/route.ts#L6-L27)

When a `POST` request is received, the route extracts the refresh token from the cookie store and forwards it to the backend server endpoint. If the backend accepts the token, a new refresh token is stored in the cookie, and a short-lived access token is returned in the response body. If verification fails, the refresh token cookie is deleted and a 401 status is returned.

| HTTP Method | Route Handler Action | Response Behavior |
| :--- | :--- | :--- |
| `POST` | Forwards stored `mem0_refresh_token` to backend refresh endpoint | Sets rotated refresh token cookie; returns JSON with new `access_token` |
| `PUT` | Validates and sets `refresh_token` from request body into cookie | Returns `{ ok: true }` or 400 error on missing token |
| `DELETE` | Deletes `mem0_refresh_token` cookie | Returns `{ ok: true }` |

Sources: [server/dashboard/src/app/api/auth/refresh/route.ts:29-75](https://github.com/blade47/mem0/blob/b7ad69afda6b6ed030347c66d48a13e4de9dec08/server/dashboard/src/app/api/auth/refresh/route.ts#L29-L75)

> [!WARNING]
> If `DASHBOARD_URL` is omitted from the environment, `shouldUseSecureCookie()` falls back to checking `process.env.NODE_ENV === "production"`. In local environments configured with custom proxy setups, ensure `DASHBOARD_URL` explicitly reflects the protocol scheme to prevent cookie transmission failures over secure transport.
> Sources: [server/dashboard/src/app/api/auth/refresh/route.ts:8-19](https://github.com/blade47/mem0/blob/b7ad69afda6b6ed030347c66d48a13e4de9dec08/server/dashboard/src/app/api/auth/refresh/route.ts#L8-L19)

### Next.js Middleware Token Routing and Setup Enforcements

The Next.js middleware intercepts incoming requests across the application, bypassing static assets and authentication endpoints defined in `PUBLIC_PATHS`. It inspects the presence of the `mem0_refresh_token` cookie and queries the backend setup status when users visit root or setup routes.

```typescript
const PUBLIC_PATHS = [
  "/_next",
  "/api/auth",
  "/api/health",
  "/fonts",
  "/favicon",
];
```
Sources: [server/dashboard/src/middleware.ts:5-11](https://github.com/blade47/mem0/blob/b7ad69afda6b6ed030347c66d48a13e4de9dec08/server/dashboard/src/middleware.ts#L5-L11)

The matcher configuration excludes static assets and icons from middleware evaluation, while routing protected navigation targets appropriately.

```typescript
export const config = {
  matcher: ["/((?!_next/static|_next/image|favicon.ico|fonts|images|icons).*)"],
};
```
Sources: [server/dashboard/src/middleware.ts:65-67](https://github.com/blade47/mem0/blob/b7ad69afda6b6ed030347c66d48a13e4de9dec08/server/dashboard/src/middleware.ts#L65-L67)

> [!NOTE]
> When requests hit the root path `/`, the middleware inspects `hasRefreshToken`. Unauthenticated users are redirected to `/login`, while authenticated users are directed straight to `/dashboard/requests`.
> Sources: [server/dashboard/src/middleware.ts:46-50](https://github.com/blade47/mem0/blob/b7ad69afda6b6ed030347c66d48a13e4de9dec08/server/dashboard/src/middleware.ts#L46-L50)

## Administrative Password Reset Script

### Utility Execution and Credential Updates

The administrative password reset utility is designed to run directly inside the running container environment via Python. It reads the target user's `EMAIL` and new `PASSWORD` values directly from environment variables.
Sources: [server/scripts/reset_admin_password.py:1-17](https://github.com/blade47/mem0/blob/b7ad69afda6b6ed030347c66d48a13e4de9dec08/server/scripts/reset_admin_password.py#L1-L17)

```python
MIN_PASSWORD_LENGTH = 8
```
Sources: [server/scripts/reset_admin_password.py:12-12](https://github.com/blade47/mem0/blob/b7ad69afda6b6ed030347c66d48a13e4de9dec08/server/scripts/reset_admin_password.py#L12-L12)

The script opens a database session using `SessionLocal`, queries the `User` model by email address, computes a fresh bcrypt password hash via `hash_password`, and commits the updated credential to the database.
Sources: [server/scripts/reset_admin_password.py:27-35](https://github.com/blade47/mem0/blob/b7ad69afda6b6ed030347c66d48a13e4de9dec08/server/scripts/reset_admin_password.py#L27-L35)

| Exit Code | Condition | Action / Stderr Output |
| :--- | :--- | :--- |
| `0` | Password successfully updated | Prints confirmation message: `Password reset for <email>.` |
| `1` | User email not found in database | Prints error: `error: no user found with email <email>.` |
| `2` | Missing variables or validation failure | Prints error: `error: EMAIL and PASSWORD environment variables are required.` or minimum length violation |

Sources: [server/scripts/reset_admin_password.py:19-37](https://github.com/blade47/mem0/blob/b7ad69afda6b6ed030347c66d48a13e4de9dec08/server/scripts/reset_admin_password.py#L19-L37)

> [!WARNING]
> The password update script enforces a strict minimum length of 8 characters. Providing a shorter password causes the script to abort with exit code 2 before querying or modifying the database.
> Sources: [server/scripts/reset_admin_password.py:12-25](https://github.com/blade47/mem0/blob/b7ad69afda6b6ed030347c66d48a13e4de9dec08/server/scripts/reset_admin_password.py#L12-L25)

### Makefile Integration

Container orchestration commands expose the reset script through a dedicated `reset-admin-password` Makefile target. This target validates that both `EMAIL` and `PASSWORD` parameters are supplied before executing the command inside the docker compose container environment.
Sources: [server/Makefile:50-54](https://github.com/blade47/mem0/blob/b7ad69afda6b6ed030347c66d48a13e4de9dec08/server/Makefile#L50-L54)

```Makefile
reset-admin-password:
	@test -n "$(EMAIL)" || (echo "usage: make reset-admin-password EMAIL=<email> PASSWORD=<password>" && exit 2)
	@test -n "$(PASSWORD)" || (echo "usage: make reset-admin-password EMAIL=<email> PASSWORD=<password>" && exit 2)
	@docker compose exec -T -e EMAIL="$(EMAIL)" -e PASSWORD="$(PASSWORD)" -e PYTHONPATH=/app mem0 python scripts/reset_admin_password.py
```
Sources: [server/Makefile:50-54](https://github.com/blade47/mem0/blob/b7ad69afda6b6ed030347c66d48a13e4de9dec08/server/Makefile#L50-L54)

## Related

- [FastAPI Application](https://www.doc0.dev/docs/faa36707-7c28-4f69-a18f-700ff61c704e/technical/server-dashboard/fastapi-application)
- [Database and Migrations](https://www.doc0.dev/docs/faa36707-7c28-4f69-a18f-700ff61c704e/technical/server-dashboard/database-and-migrations)


## Sitemap

See the full [sitemap](https://www.doc0.dev/docs/faa36707-7c28-4f69-a18f-700ff61c704e/llms.txt) for all pages in this wiki.
