> ## Documentation Index
> Fetch the complete documentation index at: https://docs.didit.me/llms.txt
> Use this file to discover all available pages before exploring further.

# Migrar usuarios: importar contraseñas y MFA

> Didit importa usuarios con contraseñas, emails verificados, cuentas sociales, apps de autenticación y roles desde CSV, JSON o por lotes.

> [!WARNING]
>
> Authentication es una función nueva. Ejecuta cada importación primero en una aplicación de sandbox y después importa en tu aplicación de producción.

Una importación traslada a las personas de otra plataforma de identidad a Didit Authentication. Con ellas se trasladan su contraseña, su correo electrónico verificado, sus vínculos de inicio de sesión social, su aplicación de autenticación y sus roles en organizaciones. Para ellas no cambia nada hasta que tu aplicación inicie su sesión con Didit.

Las importaciones usan la API de gestión de Authentication de tu aplicación con una sesión de consola que tenga `write:authentication` (o `read:authentication` para listarlas). Nunca usan un token de usuario final ni una clave de API de verificación. En los ejemplos, `MANAGEMENT_BASE` es:

```text theme={null}
https://apx.didit.me/auth/v2/organizations/{org_id}/applications/{application_id}/authentication/
```

## Dos formas de importar

| | Tarea de archivo | Solicitud por lotes |
| - | - | - |
| Endpoint | `POST import-jobs/` (multipart) | `POST imports/` (JSON) |
| Tamaño | Un archivo CSV o JSON, de hasta 25 MB y 100 000 personas | De 1 a 1000 `rows` por solicitud |
| Simulación (dry run) | Siempre se ejecuta primero, en segundo plano | `dry_run: true` de forma predeterminada |
| Confirmación | `POST import-jobs/{id}/start/` | La misma solicitud con `dry_run: false` |
| Fallo parcial | Cada lote de 1000 se confirma por separado; una importación interrumpida se reanuda | Todo o nada |

## Importar un archivo

### 1. Subir el archivo

```bash theme={null}
curl --fail-with-body "${MANAGEMENT_BASE}import-jobs/" \
  -H "Authorization: Bearer $CONSOLE_ACCESS_TOKEN" \
  -F file=@users.csv \
  -F passwords=import \
  -F keep_social=true \
  -F keep_verified=true \
  -F keep_mfa=true \
  -F keep_ids=true
```

| Campo | Significado |
| - | - |
| `file` | CSV o JSON, en UTF-8 (se admite la marca de orden de bytes), de hasta 25 MB. |
| `passwords` | `import` (el valor predeterminado) conserva los hashes de contraseña. `reset` los ignora, de modo que todos establecen una contraseña nueva. |
| `keep_social` | Conserva los vínculos de inicio de sesión social. |
| `keep_verified` | Conserva `email_verified`. Con `false`, ningún correo electrónico queda verificado. |
| `keep_mfa` | Conserva los secretos de la aplicación de autenticación. |
| `keep_ids` | Conserva `external_id`. Con `false`, `vendor_data` pasa a ser el ID de identidad de Didit. |
| `firebase_signer_key`, `firebase_salt_separator`, `firebase_rounds`, `firebase_mem_cost` | Los parámetros de hash de tu proyecto de Firebase. Envía los cuatro o ninguno. |

Cada opción `keep_*` es `true` salvo que envíes `false`. La llamada devuelve `202` con un trabajo. Una aplicación conserva como máximo 5 archivos subidos a la vez.

### 2. Revisar la simulación

Cada subida inicia una simulación. Comprueba a cada persona y no escribe nada. Consulta periódicamente `GET import-jobs/{id}/`:

| Campo | Significado |
| - | - |
| `kind` | `dry_run` y, después, `import` cuando la inicias. |
| `status` | `queued`, `running` y luego `passed` o `failed` en una simulación, `done` o `error` en una importación. |
| `processed`, `total` | Progreso, en personas. |
| `result` | Lo que contiene el archivo (consulta [resultados](#resultados)). |
| `startable` | `true` cuando la simulación finalizó correctamente y el archivo no ha expirado. |
| `expires_at` | El archivo se elimina 24 horas después de una simulación si no se importó. |

Una simulación fallida enumera sus errores. Corrige el archivo y vuelve a subirlo.

### 3. Iniciar la importación

`POST import-jobs/{id}/start/` importa lo que comprobó la simulación y devuelve `202`.

* Las personas se importan en lotes de 1000. Cada lote se confirma por separado.
* Solo se ejecuta una importación por aplicación a la vez (en caso contrario, se devuelve `import_running`).
* Una importación interrumpida se reanuda después de su último lote confirmado.
* Las personas ya importadas se omiten, así que puedes volver a ejecutar el mismo archivo.
* La persona que inició la importación recibe un correo electrónico cuando termina o se detiene.

El archivo subido se almacena cifrado. Se elimina cuando termina la importación, cuando falla su simulación o 24 horas después de una simulación si no se importó.

## Importar por lotes

Envía hasta 1000 personas por solicitud con una `idempotency_key` de hasta 100 caracteres.

```json theme={null}
{
  "rows": [
    {
      "email": "user@example.com",
      "external_id": "user-1234",
      "email_verified": true,
      "display_name": "Example User",
      "password_hash": "$2b$12$...",
      "social": [{ "provider": "google", "subject": "1234567890" }],
      "organizations": [{ "slug": "acme", "name": "Acme", "role": "admin" }]
    }
  ],
  "dry_run": true,
  "idempotency_key": "migration-batch-0001"
}
```

* Una simulación devuelve `200` con el resultado y no realiza ninguna escritura.
* Con `dry_run: false`, una importación correcta devuelve `201`. Un lote con cualquier error devuelve `400` y no importa a nadie.
* Repetir una solicitud confirmada con la misma clave devuelve su resultado registrado. La misma clave con un contenido distinto devuelve `400 {"idempotency_key": "different_payload"}`.
* Los parámetros de Firebase van en un objeto `firebase`: `{signer_key, salt_separator, rounds, mem_cost}`.
* `GET imports/` lista los lotes confirmados.

## Formato del archivo

### JSON

Un array de personas, o un objeto cuyo `users` contiene el array.

| Campo | Regla |
| - | - |
| `email` | Obligatorio. |
| `external_id` | Hasta 255 caracteres. Pasa a ser el [`vendor_data`](https://docs.didit.me/es/entities/vendor-data-linking) de la persona. |
| `email_verified` | `true` o `false` (también `1`/`0`, `yes`/`no`). |
| `display_name` | Hasta 200 caracteres. |
| `password_hash`, `password_salt`, `password_algorithm` | Consulta [contraseñas](#contraseñas). |
| `totp_secret` | Secreto de autenticación en Base32. |
| `created_at`, `last_signed_in_at` | ISO 8601. Una hora sin zona horaria se interpreta como UTC. |
| `social` | Hasta 8 `{provider, subject, tenant_id}`. `provider` es `google`, `apple`, `github` o `microsoft`. `tenant_id` solo es obligatorio para Microsoft. |
| `organizations` | Hasta 20 `{slug, name, role}`. `role` es `admin`, `member` (el valor predeterminado) o `billing`. |

### CSV

Las mismas columnas de un solo valor, más `google_id`, `apple_id`, `github_id`, `microsoft_id`, `microsoft_tenant_id`, `organization_slug`, `organization_name` y `organization_role`. Una persona que pertenece a varias organizaciones ocupa una línea por organización.

```csv theme={null}
email,external_id,email_verified,password_hash,google_id,organization_slug,organization_role
user@example.com,user-1234,true,$2b$12$...,1234567890,acme,admin
```

Los campos o columnas desconocidos son errores; nunca se ignoran.

### Duplicados y cuentas existentes

* Las líneas con el mismo correo electrónico se convierten en una sola persona. Una línea idéntica se cuenta como duplicado. Una línea con más vínculos, organizaciones o una aplicación de autenticación se fusiona.
* Dos hashes de contraseña, secretos de autenticación o roles en una organización distintos para una misma persona son errores. También lo es un `external_id` o un identificador de sujeto de un proveedor que reclaman dos personas.
* Si ya existe una cuenta con ese correo electrónico y la creó una importación anterior, la persona se omite (`already_imported`). Si la persona se registró por su cuenta, se produce un error `identity_conflict`. Una importación nunca modifica una cuenta existente.

## Contraseñas

| Formato | Se acepta |
| - | - |
| bcrypt | `$2a$`, `$2b$` o `$2y$`, coste de 4 a 13. |
| Argon2id | `$argon2id$v=19$...`. Hasta 64 MiB de memoria, con el producto de la memoria y el número de pasadas de hasta 256 MiB, y hasta 8 carriles. |
| Django PBKDF2-SHA256 | `pbkdf2_sha256$...`, de 10,000 a 2,000,000 iteraciones. |
| Django scrypt | `scrypt$...`, factor de trabajo de hasta 32,768, tamaño de bloque de 1 a 8, paralelismo de 1 a 5. |
| Firebase scrypt | `password_algorithm: "firebase_scrypt"`, `password_hash` y `password_salt` en base64 para cada persona, además de `signer_key`, `salt_separator`, `rounds` (de 1 a 8) y `mem_cost` (de 1 a 14) de tu proyecto, una sola vez por importación. |

Los parámetros de Firebase se almacenan cifrados y solo para esa aplicación. Cualquier otro formato, o un coste por encima de estos límites, implica que la persona necesita una contraseña nueva. Después del primer inicio de sesión correcto de una persona, Didit vuelve a almacenar su contraseña en su propio formato.

### Personas sin una contraseña utilizable

Cuando una persona no tiene hash, o tiene uno no compatible, un intento con contraseña recibe la respuesta habitual de contraseña incorrecta. Además, Didit le envía por correo electrónico un enlace para establecer una contraseña nueva (válido durante 30 minutos, como máximo una vez al día). Esto solo ocurre cuando su correo electrónico está verificado, la cuenta no está bloqueada y el inicio de sesión con contraseña está activado.

En el resultado, `reset_required` cuenta a las personas sin una contraseña utilizable. `reset_emails` cuenta a las que, entre ellas, tienen un correo electrónico verificado: solo a ellas se les puede contactar de esta forma. Las personas con un correo electrónico sin verificar no pueden iniciar sesión con contraseña. Aun así, pueden iniciar sesión con un código por correo electrónico si activas ese método.

## Vínculos sociales, autenticadores y organizaciones

* Los **vínculos sociales** se conservan para los proveedores que tu aplicación ya tiene configurados, con su client ID actual. Así, cuando la persona vuelve a iniciar sesión, no necesita demostrar su correo electrónico. Los identificadores (subjects) de Google y GitHub son globales. Los de Apple son por equipo de desarrolladores y los de Microsoft, por registro de aplicación, por lo que esos vínculos solo coinciden si tu plataforma anterior usaba el mismo equipo o registro. Los vínculos a proveedores que no has configurado se cuentan como `social_unavailable` y no se importan.
* Las **aplicaciones de autenticación** (secretos TOTP: SHA-1, 6 dígitos, 30 segundos) se almacenan cifradas. Las personas importadas deben usarlas como segundo factor. No reciben códigos de recuperación.
* Las **organizaciones** que todavía no existen se crean a partir de su slug.
* Las **claves de acceso** no se pueden importar. Pertenecen a tu dominio de inicio de sesión anterior, por lo que las personas añaden una clave de acceso nueva después de iniciar sesión con Didit.

## Resultados

Cada simulación (dry run), lote y trabajo devuelve el mismo `result`:

| Campo | Significado |
| - | - |
| `rows`, `people_total` | Líneas leídas y personas distintas. |
| `ready`, `already_imported`, `duplicates`, `merged`, `skipped` | Lo que ocurre con cada persona. |
| `passwords` | Recuentos por formato: `bcrypt`, `argon2id`, `firebase_scrypt`, `pbkdf2_sha256`, `scrypt`. |
| `reset_required`, `reset_emails`, `unsupported_hashes` | Personas que necesitan una contraseña nueva. |
| `verified_emails` | Personas importadas con un correo electrónico verificado. |
| `social_links`, `social_unavailable` | Vínculos conservados por proveedor y vínculos omitidos. |
| `authenticator_apps` | Personas con una aplicación de autenticación importada. |
| `organizations` | `count`, `members` y hasta cinco `names`. |
| `error_count`, `errors` | Hasta 100 errores, cada uno con un `row` que empieza en cero (línea del CSV `row + 2`), un `field` y un código de `error`. |

Las respuestas nunca contienen hashes de contraseñas, valores de sal, secretos de autenticación ni valores de campos.

| Error | Significado |
| - | - |
| `file_too_large`, `too_many_rows`, `empty_file` | Supera los 25 MB, supera las 100,000 personas o está vacío. |
| `invalid_encoding`, `invalid_json`, `invalid_csv` | No se puede leer el archivo. |
| `unknown_column`, `missing_column`, `duplicate_column`, `unknown_field` | El archivo no se ajusta al formato. |
| `required`, `invalid_field`, `invalid_row` | Falta un valor o no es válido. |
| `identity_conflict` | Una cuenta con ese correo electrónico se registró por su cuenta. |
| `social_link_conflict`, `password_conflict`, `authenticator_conflict`, `role_conflict` | Dos líneas de una misma persona no coinciden. |
| `too_many_uploads`, `import_running`, `not_startable`, `file_expired` | El trabajo no puede iniciarse ahora. |

## Antes de hacer el cambio

Haz pruebas en tu aplicación de sandbox:

* el inicio de sesión con cada formato de contraseña que importes,
* el correo electrónico para establecer una contraseña nueva de una persona sin un hash utilizable,
* el inicio de sesión social y los códigos de autenticación de las personas importadas,
* la ejecución del mismo archivo dos veces.

Después, comprueba que el `vendor_data` de cada persona coincide con el valor que usas para sus [verificaciones y transacciones](https://docs.didit.me/es/authentication/overview.md#mantener-una-misma-persona-en-todos-los-productos).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.