Visión general
🛡️ Guía de seguridad
Single Sign-On

¿Por qué necesita seguridad avanzada?

Un SSO centraliza el acceso: con un solo inicio de sesión se accede a todos los sistemas, lo que lo convierte en el punto más crítico de la infraestructura.

⚠️

Un único punto de falla

Si alguien compromete el SSO, se accede a todo: correo, datos, herramientas internas, sistemas de producción. Por eso las capas de seguridad adicionales no son opcionales.

Las dos capas esenciales

Para proteger correctamente un SSO, se necesitan dos tecnologías complementarias que atacan el problema desde ángulos distintos.

📱
Doble Factor (2FA)
Exige un segundo elemento de verificación además de la contraseña. Mitiga el robo de credenciales.
🗝️
Passkeys
Reemplaza las contraseñas con criptografía asimétrica. Elimina el phishing y el robo de passwords.
🔒
Combinación
Juntas cubren prácticamente todos los vectores de ataque conocidos en autenticación web.

Cada capa puede explorarse mediante el menú lateral para comprender su funcionamiento y las amenazas que neutraliza.

Segunda capa

Autenticación de doble factor

El 2FA exige dos elementos independientes para verificar la identidad del usuario. Si un atacante obtiene la contraseña, no podrá acceder sin el segundo factor.

Los tres factores posibles

  • 🧠
    Algo que se sabe → Contraseña El factor más común y el más vulnerable. Puede ser robado, adivinado o filtrado.
  • 📱
    Algo que se posee → Código TOTP, SMS, llave física Un dispositivo o token que genera o recibe códigos de un solo uso. Mucho más difícil de comprometer remotamente.
  • 👁️
    Algo que se es → Biometría Huella dactilar, reconocimiento facial. Difícil de falsificar pero requiere hardware especializado.

¿Cómo funciona el flujo?

Paso 01

Ingreso de contraseña

El usuario introduce su usuario y contraseña en el SSO. Este es el primer factor: algo que sabe.

Paso 02

Solicitud del segundo factor

El servidor verifica la contraseña y, si es correcta, solicita el segundo factor: un código TOTP, una notificación push, un SMS o una llave física.

Paso 03

Verificación del segundo factor

El usuario presenta el segundo elemento (algo que tiene o es). El servidor lo valida y, solo si ambos factores son correctos, concede el acceso al SSO.

Paso 04

Acceso a todos los sistemas

Con la sesión SSO establecida, el usuario accede a todos los servicios integrados sin necesidad de autenticarse nuevamente.


Ventajas concretas en un SSO

  • Mitiga el robo de contraseñas Si un atacante obtiene la contraseña (phishing, brute force, filtración), no podrá acceder sin el segundo factor.
  • Reduce el impacto de credenciales filtradas Bases de datos comprometidas en servicios externos no ponen en riesgo el SSO.
  • Cumplimiento normativo SOC 2, ISO 27001 y PCI-DSS exigen MFA para accesos a sistemas críticos.
  • Detección temprana de ataques Intentos fallidos del segundo factor permiten alertar sobre ataques en curso antes de que tengan éxito.
⚠️

Limitación importante

El 2FA basado en TOTP (códigos de 6 dígitos) puede ser interceptado en ataques de phishing sofisticados (adversary-in-the-middle). Para una protección más robusta, considerá las Passkeys.

Tecnología FIDO2/WebAuthn

Passkeys: sin contraseñas

Las Passkeys usan criptografía asimétrica para autenticar. La clave privada nunca sale del dispositivo — el servidor nunca conoce el secreto del usuario.

¿Cómo funciona?

Paso 01

Registro

El dispositivo genera un par de claves únicas para ese sitio. La clave pública se envía al servidor. La privada queda en el dispositivo.

Paso 02

Desafío criptográfico

Al autenticarse, el servidor envía un challenge. El dispositivo lo firma con la clave privada usando biometría o PIN como confirmación local.

Paso 03

Verificación

El servidor verifica la firma con la clave pública almacenada. Si coincide, acceso concedido. Ningún secreto viajó por la red.


Ventajas concretas

  • 🛡️
    Resistencia nativa al phishing La clave está vinculada criptográficamente al dominio real. Un sitio falso no puede usarla.
  • 🛡️
    Sin secretos compartidos El servidor solo guarda la clave pública. Una brecha en el servidor no expone credenciales útiles.
  • 🛡️
    2FA integrado en un solo gesto Posesión del dispositivo + biometría = doble factor sin fricción adicional para el usuario.
  • 🛡️
    Sin reutilización de credenciales Cada passkey es única por sitio. El credential stuffing se vuelve imposible.
  • 🛡️
    Mejor experiencia de usuario Autenticarse con huella o cara es más rápido y cómodo que recordar contraseñas complejas.
Análisis comparativo

2FA vs Passkeys

Cada tecnología tiene fortalezas distintas. La siguiente tabla muestra qué amenaza neutraliza cada una.

Amenaza / Escenario 2FA tradicional Passkeys
Contraseña robada ✅ Mitiga ✅ Elimina el problema
Phishing básico ⚠️ Parcial ✅ Resistencia total
Phishing avanzado (AiTM) ⚠️ TOTP interceptable ✅ Bloqueado
Credential stuffing ✅ Mitiga ✅ Elimina
Brecha en el servidor ⚠️ Depende del método ✅ Clave pública inútil sola
Experiencia de usuario ⚠️ Fricción añadida ✅ Más rápido que passwords
Cumplimiento normativo ✅ MFA estándar ✅ MFA + más

Ninguna tecnología por sí sola cubre todo. Por eso la combinación de ambas es la estrategia más robusta disponible hoy.

Estrategia recomendada

La combinación ideal

Usar 2FA y Passkeys juntos en un SSO no es redundante: son capas complementarias que se refuerzan mutuamente.

🧱
El 2FA es la red de seguridad
Para dispositivos que no soportan passkeys, o como mecanismo de recuperación cuando el dispositivo principal no está disponible.
🔑
Passkeys son el estándar
Para el flujo principal de autenticación. Eliminan el vector de ataque más común: las contraseñas débiles o robadas.

Resumen ejecutivo

  • 🔐
    El SSO es la llave maestra Comprometer el SSO significa comprometer todo. El nivel de protección debe ser proporcional al riesgo.
  • 📱
    El 2FA protege contra contraseñas comprometidas Si un atacante obtiene la contraseña, aún necesita un segundo factor físico para acceder.
  • 🗝️
    Las Passkeys eliminan la contraseña como vector de ataque Van un paso más allá: si no hay contraseña, no puede ser robada. El phishing queda neutralizado por diseño.
  • Juntas cubren prácticamente todo En un SSO, donde una sola autenticación abre muchas puertas, esta combinación no es un lujo — es una necesidad de diseño.
Guía técnica paso a paso · VB.NET · .NET Framework 4.8

Cómo implementarlo en el SSO

Pasos detallados para agregar 2FA y Passkeys a un SSO en VB.NET con ASP.NET MVC (.NET Framework 4.8). Se cubren NuGet, base de datos SQL Server, backend y frontend.

ℹ️

Librerías compatibles con VB.NET y .NET 4.8

Para TOTP se usa Otp.NET (compatible con .NET 4.5+). Para WebAuthn/Passkeys se usa Fido2NetLib, que soporta .NET Framework 4.8. Ambas son instalables via NuGet y funcionan igual en VB.NET que en C#, ya que comparten el mismo CLR.

Parte 1 — Implementar 2FA (TOTP)

TOTP (RFC 6238) es el estándar compatible con Google Authenticator, Microsoft Authenticator y Authy. Se implementa completamente en el servidor sin servicios externos.

Base de datos — SQL Server
1
Agregar columnas al modelo de usuario
-- Agregar a la tabla Users existente en SQL Server ALTER TABLE Users ADD TotpSecret NVARCHAR(64) NULL; ALTER TABLE Users ADD TotpEnabled BIT NOT NULL DEFAULT 0; ALTER TABLE Users ADD TotpVerifiedAt DATETIME NULL; ALTER TABLE Users ADD BackupCodes NVARCHAR(MAX) NULL; -- JSON array de hashes
2
Tabla de sesiones pendientes de MFA
-- Sesión intermedia: credenciales OK pero MFA aún no completado CREATE TABLE PendingMfaSessions ( Id UNIQUEIDENTIFIER PRIMARY KEY DEFAULT NEWID(), UserId INT NOT NULL REFERENCES Users(Id), CreatedAt DATETIME NOT NULL DEFAULT GETUTCDATE(), ExpiresAt DATETIME NOT NULL, -- GETUTCDATE() + 5 minutos IpAddress NVARCHAR(45) NULL, UserAgent NVARCHAR(500) NULL );
NuGet — Paquetes requeridos
3
Instalar paquetes desde la Consola NuGet
' Consola del Administrador de Paquetes en Visual Studio ' TOTP — compatible con .NET 4.5+ Install-Package Otp.NET ' QR Code para la pantalla de configuración Install-Package QRCoder ' JSON (si no está instalado ya) Install-Package Newtonsoft.Json
Backend VB.NET — Servicio TOTP
4
TotpService.vb — generar secreto, QR y validar código
' TotpService.vb Imports OtpNet Imports QRCoder Imports System Imports System.Drawing Public Class TotpService Private Const Issuer As String = "MiSSO" ' Genera un secreto Base32 aleatorio de 20 bytes Public Function GenerateSecret() As String Dim key As Byte() = KeyGeneration.GenerateRandomKey(20) Return Base32Encoding.ToString(key) End Function ' Construye la URI otpauth:// para escanear con la app Public Function GetOtpAuthUri(secret As String, userEmail As String) As String Return $"otpauth://totp/{Uri.EscapeDataString(Issuer)}" & $":{Uri.EscapeDataString(userEmail)}" & $"?secret={secret}&issuer={Uri.EscapeDataString(Issuer)}" & $"&algorithm=SHA1&digits=6&period=30" End Function ' Genera imagen QR en Base64 para mostrar en la vista Razor Public Function GenerateQrBase64(otpAuthUri As String) As String Using qrGenerator As New QRCodeGenerator() Using qrData As QRCodeData = qrGenerator.CreateQrCode(otpAuthUri, QRCodeGenerator.ECCLevel.Q) Using qrCode As New PngByteQRCode(qrData) Dim bytes As Byte() = qrCode.GetGraphic(5) Return Convert.ToBase64String(bytes) End Using End Using End Using End Function ' Valida un código TOTP con ventana de ±1 período (30 segundos) Public Function ValidateCode(base32Secret As String, code As String) As Boolean Dim key As Byte() = Base32Encoding.ToBytes(base32Secret) Dim totp As New Totp(key, step:=30, totpSize:=6) Dim usedIndex As Long Return totp.VerifyTotp(code, usedIndex, VerificationWindow.RfcSpecifiedNetworkDelay) End Function End Class
5
MfaController.vb — Iniciar y completar configuración de 2FA
' MfaController.vb Imports System.Web.Mvc <Authorize> Public Class MfaController Inherits Controller Private ReadOnly _totp As New TotpService() Private ReadOnly _users As New UserRepository() Private ReadOnly _mfaRepo As New MfaRepository() ' GET /Mfa/Setup — muestra el QR al usuario autenticado Public Function Setup() As ActionResult Dim userId As Integer = GetCurrentUserId() Dim secret As String = _totp.GenerateSecret() ' Guardar secreto temporal en sesión (aún NO activado en BD) Session("pending_totp_secret") = secret Dim user = _users.GetById(userId) Dim uri As String = _totp.GetOtpAuthUri(secret, user.Email) Dim qrBase64 As String = _totp.GenerateQrBase64(uri) ViewBag.QrBase64 = qrBase64 ViewBag.Secret = secret ' Para mostrar como texto alternativo Return View() End Function ' POST /Mfa/Setup — verificar el código ingresado y activar 2FA <HttpPost> <ValidateAntiForgeryToken> Public Function Setup(code As String) As ActionResult Dim secret As String = TryCast(Session("pending_totp_secret"), String) If secret Is Nothing Then Return RedirectToAction("Setup") If Not _totp.ValidateCode(secret, code) Then ModelState.AddModelError("", "Código inválido. Intentá de nuevo.") Return View() End If Dim userId As Integer = GetCurrentUserId() Dim backupCodes As String() = GenerateBackupCodes() ' 8 códigos de 10 chars _users.ActivateTotp(userId, secret, backupCodes) Session.Remove("pending_totp_secret") TempData("BackupCodes") = backupCodes Return RedirectToAction("SetupComplete") End Function End Class
6
AccountController.vb — Flujo de login con challenge MFA
' AccountController.vb — Login en dos etapas Imports System.Web.Mvc Imports System.Web.Security Public Class AccountController Inherits Controller <HttpPost> <AllowAnonymous> <ValidateAntiForgeryToken> Public Function Login(model As LoginViewModel) As ActionResult If Not ModelState.IsValid Then Return View(model) Dim user = _users.ValidateCredentials(model.Email, model.Password) If user Is Nothing Then ModelState.AddModelError("", "Credenciales inválidas.") Return View(model) End If If user.TotpEnabled Then ' Crear sesión pendiente — NO autenticar todavía Dim pendingId As Guid = _mfaRepo.CreatePendingSession( userId := user.Id, expiresAt := DateTime.UtcNow.AddMinutes(5), ipAddress := Request.UserHostAddress, userAgent := Request.UserAgent ) Session("mfa_pending_session") = pendingId.ToString() Return RedirectToAction("Challenge", "Mfa") End If ' Sin 2FA: autenticar directamente con FormsAuth FormsAuthentication.SetAuthCookie(user.Id.ToString(), False) Return RedirectToAction("Index", "Home") End Function ' POST /Mfa/Challenge — verificar el código TOTP del challenge <HttpPost> <AllowAnonymous> <ValidateAntiForgeryToken> Public Function Challenge(code As String) As ActionResult Dim pendingIdStr As String = TryCast(Session("mfa_pending_session"), String) If pendingIdStr Is Nothing Then Return RedirectToAction("Login", "Account") Dim pendingId As Guid = Guid.Parse(pendingIdStr) Dim pending = _mfaRepo.GetPendingSession(pendingId) If pending Is Nothing OrElse pending.ExpiresAt < DateTime.UtcNow Then Session.Remove("mfa_pending_session") TempData("Error") = "La sesión expiró. Ingresá nuevamente." Return RedirectToAction("Login", "Account") End If Dim user = _users.GetById(pending.UserId) If Not _totp.ValidateCode(user.TotpSecret, code) Then _auditLog.LogFailedMfa(user.Id, Request.UserHostAddress) ModelState.AddModelError("", "Código incorrecto.") Return View() End If _mfaRepo.DeletePendingSession(pendingId) Session.Remove("mfa_pending_session") FormsAuthentication.SetAuthCookie(user.Id.ToString(), False) Return RedirectToAction("Index", "Home") End Function End Class

Parte 2 — Implementar Passkeys (WebAuthn / FIDO2)

Se usa Fido2NetLib, compatible con .NET Framework 4.8. Requiere HTTPS obligatorio — las passkeys están vinculadas criptográficamente al dominio exacto.

Base de datos — SQL Server
7
Tablas para credenciales WebAuthn y challenges
CREATE TABLE WebAuthnCredentials ( Id UNIQUEIDENTIFIER PRIMARY KEY DEFAULT NEWID(), UserId INT NOT NULL REFERENCES Users(Id) ON DELETE CASCADE, CredentialId VARBINARY(1024) NOT NULL UNIQUE, -- ID del autenticador PublicKey VARBINARY(MAX) NOT NULL, -- Clave pública COSE SignCount BIGINT NOT NULL DEFAULT 0,-- Contador anti-replay AaGuid UNIQUEIDENTIFIER NULL, FriendlyName NVARCHAR(100) NULL, -- "iPhone de Ana" CreatedAt DATETIME NOT NULL DEFAULT GETUTCDATE(), LastUsedAt DATETIME NULL ); -- Challenges de un solo uso y corta duración CREATE TABLE WebAuthnChallenges ( Id UNIQUEIDENTIFIER PRIMARY KEY DEFAULT NEWID(), Challenge VARBINARY(512) NOT NULL, UserId INT NULL REFERENCES Users(Id), Type NVARCHAR(20) NOT NULL, -- 'registration' | 'authentication' ExpiresAt DATETIME NOT NULL, Used BIT NOT NULL DEFAULT 0 );
NuGet — Paquetes requeridos
8
Instalar Fido2NetLib
' Consola NuGet — compatible con .NET Framework 4.8 Install-Package Fido2NetLib ' Para serialización JSON de los options de WebAuthn Install-Package Newtonsoft.Json
Backend VB.NET — Configuración inicial de Fido2
9
WebAuthnConfig.vb — inicializar instancia global
' WebAuthnConfig.vb — instancia estática, inicializar en Global.asax Imports Fido2NetLib Imports System.Collections.Generic Public Module WebAuthnConfig Public ReadOnly Property Instance As Fido2 Public Sub Initialize() _Instance = New Fido2(New Fido2Configuration() With { .ServerDomain = "mi-sso.com", ' Tu dominio exacto .ServerName = "Mi SSO", .Origins = New HashSet(Of String) From {"https://mi-sso.com"}, .TimestampDriftTolerance = 300000 ' 5 minutos en ms }) End Sub End Module ' En Global.asax.vb — Application_Start: Sub Application_Start() WebAuthnConfig.Initialize() ' ... resto de la inicialización End Sub
Backend VB.NET — Registro de Passkey
10
PasskeyController.vb — RegisterBegin: generar opciones
' PasskeyController.vb Imports System.Web.Mvc Imports Fido2NetLib Imports Fido2NetLib.Objects Imports System.Linq <Authorize> Public Class PasskeyController Inherits Controller ' GET /Passkey/RegisterBegin — el frontend llama esto primero Public Function RegisterBegin() As JsonResult Dim userId As Integer = GetCurrentUserId() Dim user = _users.GetById(userId) Dim fidoUser As New Fido2User() With { .Id = BitConverter.GetBytes(user.Id), .Name = user.Email, .DisplayName = user.FullName } ' Credenciales existentes para excluirlas del diálogo del dispositivo Dim existingKeys = _credRepo.GetByUserId(userId) _ .Select(Function(c) New PublicKeyCredentialDescriptor(c.CredentialId)) _ .ToList() Dim options = WebAuthnConfig.Instance.RequestNewCredential( user := fidoUser, excludeCredentials := existingKeys, authenticatorSelection := New AuthenticatorSelection() With { .ResidentKey = ResidentKeyRequirement.Required, .UserVerification = UserVerificationRequirement.Required }, attestationPreference := AttestationConveyancePreference.None ) ' Persistir challenge para verificarlo en RegisterComplete _challengeRepo.Save(New WebAuthnChallenge() With { .Challenge = options.Challenge, .UserId = userId, .Type = "registration", .ExpiresAt = DateTime.UtcNow.AddMinutes(5) }) Session("webauthn_reg_options") = options.ToJson() Return Json(options, JsonRequestBehavior.AllowGet) End Function
11
PasskeyController.vb — RegisterComplete: verificar y guardar
' POST /Passkey/RegisterComplete — el frontend envía la respuesta del autenticador <HttpPost> Public Async Function RegisterComplete( attestationResponse As AuthenticatorAttestationRawResponse, friendlyName As String) As Task(Of JsonResult) Dim optionsJson As String = TryCast(Session("webauthn_reg_options"), String) Dim options = CredentialCreateOptions.FromJson(optionsJson) Dim isUnique As IsCredentialIdUniqueToUserAsyncDelegate = Async Function(args, ct) Return Not _credRepo.CredentialIdExists(args.CredentialId) End Function Dim result = Await WebAuthnConfig.Instance _ .MakeNewCredentialAsync(attestationResponse, options, isUnique) If result.Status <> "ok" Then Return Json(New With {.success = False, .errMsg = result.ErrorMessage}) End If ' Persistir la nueva credencial en BD _credRepo.Save(New WebAuthnCredential() With { .UserId = GetCurrentUserId(), .CredentialId = result.Result.CredentialId, .PublicKey = result.Result.PublicKey, .SignCount = CLng(result.Result.Counter), .AaGuid = result.Result.Aaguid, .FriendlyName = If(friendlyName, "Mi dispositivo"), .CreatedAt = DateTime.UtcNow }) Session.Remove("webauthn_reg_options") Return Json(New With {.success = True}) End Function
Backend VB.NET — Autenticación con Passkey
12
PasskeyController.vb — LoginBegin: generar challenge
' GET /Passkey/LoginBegin — passkeys discoverable, no se necesita email previo <AllowAnonymous> Public Function LoginBegin() As JsonResult Dim options = WebAuthnConfig.Instance.GetAssertionOptions( allowedCredentials := New List(Of PublicKeyCredentialDescriptor)(), ' Vacío = el dispositivo elige userVerification := UserVerificationRequirement.Required ) ' Guardar challenge — UserId NULL, se resuelve en LoginComplete _challengeRepo.Save(New WebAuthnChallenge() With { .Challenge = options.Challenge, .UserId = Nothing, .Type = "authentication", .ExpiresAt = DateTime.UtcNow.AddMinutes(5) }) Session("webauthn_auth_options") = options.ToJson() Return Json(options, JsonRequestBehavior.AllowGet) End Function
13
PasskeyController.vb — LoginComplete: verificar firma
' POST /Passkey/LoginComplete — verifica la firma criptográfica <HttpPost> <AllowAnonymous> Public Async Function LoginComplete( assertionResponse As AuthenticatorAssertionRawResponse) As Task(Of JsonResult) Dim optionsJson As String = TryCast(Session("webauthn_auth_options"), String) Dim options = AssertionOptions.FromJson(optionsJson) ' Buscar credencial por ID para obtener clave pública y contador Dim cred = _credRepo.GetByCredentialId(assertionResponse.Id) If cred Is Nothing Then Return Json(New With {.success = False, .errMsg = "Credencial no encontrada"}) End If Dim storedCounter As UInteger = CUInt(cred.SignCount) Dim result = Await WebAuthnConfig.Instance.MakeAssertionAsync( assertionResponse := assertionResponse, originalOptions := options, storedPublicKey := cred.PublicKey, storedSignatureCounter := storedCounter, isUserHandleOwnerOfCredentialId := Async Function(args, ct) Return _credRepo.UserOwnsCredential(args.UserHandle, args.CredentialId) End Function ) If result.Status <> "ok" Then Return Json(New With {.success = False, .errMsg = result.ErrorMessage}) End If ' Actualizar contador — CRÍTICO para detectar clonación _credRepo.UpdateSignCount(cred.Id, CLng(result.Counter)) _credRepo.UpdateLastUsed(cred.Id, DateTime.UtcNow) Session.Remove("webauthn_auth_options") Dim user = _users.GetById(cred.UserId) FormsAuthentication.SetAuthCookie(user.Id.ToString(), False) Return Json(New With {.success = True}) End Function
Frontend — JavaScript en Razor Views (.vbhtml)
14
Incluir el cliente WebAuthn en _Layout.vbhtml
<!-- En _Layout.vbhtml — no requiere npm, se carga desde CDN --> <script src="https://unpkg.com/@@simplewebauthn/browser/dist/bundle/index.umd.min.js"></script> <!-- O servir localmente desde ~/Scripts/ para entornos sin internet --> <script src="@Url.Content("~/Scripts/simplewebauthn-browser.umd.min.js")"></script>
15
Registrar una Passkey (vista de seguridad / perfil)
// JavaScript en Security.vbhtml async function registerPasskey() { if (!SimpleWebAuthnBrowser.browserSupportsWebAuthn()) { alert('Tu navegador no soporta Passkeys.'); return; } // 1. Pedir opciones al servidor VB.NET const optRes = await fetch('/Passkey/RegisterBegin', { method: 'GET', headers: { 'RequestVerificationToken': getAntiForgeryToken() } }); const options = await optRes.json(); try { // 2. El navegador pide biometría o PIN al usuario const attResp = await SimpleWebAuthnBrowser.startRegistration(options); // 3. Enviar la respuesta al controller para verificación const verRes = await fetch('/Passkey/RegisterComplete', { method: 'POST', headers: { 'Content-Type': 'application/json', 'RequestVerificationToken': getAntiForgeryToken() }, body: JSON.stringify({ attestationResponse: attResp, friendlyName: 'Mi ' + navigator.platform }) }); const result = await verRes.json(); if (result.success) { alert('✅ Passkey registrada correctamente.'); location.reload(); } } catch (err) { console.error('Registro cancelado o fallido:', err); } } function getAntiForgeryToken() { return document.querySelector('input[name="__RequestVerificationToken"]').value; }
16
Login con Passkey (vista Login.vbhtml)
// JavaScript en Login.vbhtml async function loginWithPasskey() { // 1. Obtener challenge del servidor const optRes = await fetch('/Passkey/LoginBegin', { method: 'GET' }); const options = await optRes.json(); try { // 2. El navegador muestra el selector de passkeys del dispositivo const authResp = await SimpleWebAuthnBrowser.startAuthentication(options); // 3. Enviar al controller VB.NET para verificar la firma const verRes = await fetch('/Passkey/LoginComplete', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ assertionResponse: authResp }) }); const result = await verRes.json(); if (result.success) { window.location.href = '/Home/Index'; } else { alert('Error de autenticación: ' + result.errMsg); } } catch (err) { console.error('Autenticación fallida:', err); } }

Parte 3 — Consideraciones de seguridad en VB.NET

  • 🔒
    HTTPS obligatorio — web.config e IIS WebAuthn no funciona en HTTP. En web.config agregar <httpRedirect enabled="true" /> y en IIS activar "Require SSL". El ServerDomain de Fido2NetLib debe coincidir exactamente con el dominio del certificado TLS.
  • 🔒
    Rate limiting con ActionFilterAttribute Crear un RateLimitAttribute que herede de ActionFilterAttribute y use MemoryCache o una tabla SQL para contar intentos fallidos por IP. Máximo 5 intentos en 15 minutos antes de bloquear temporalmente.
  • 🔒
    Validar SignCount para detectar clonación de autenticador Fido2NetLib valida el contador automáticamente. Capturar la excepción que lanza cuando el nuevo valor es menor al guardado, registrarla en auditoría y notificar al usuario — puede indicar que alguien clonó su autenticador físico.
  • 🔒
    Hashing de backup codes con Rfc2898DeriveBytes (PBKDF2) En VB.NET usar Dim pbkdf2 As New Rfc2898DeriveBytes(code, salt, 100000) para hashear los códigos de respaldo. Nunca usar MD5 o SHA1 sin sal. Generar los bytes aleatorios con RNGCryptoServiceProvider.
  • 🔒
    Anti-CSRF en todos los endpoints POST Decorar todos los actions POST de autenticación con <ValidateAntiForgeryToken>. Para llamadas AJAX desde las vistas .vbhtml, incluir el token en el header como se muestra en el paso 15.
  • 🔒
    Auditoría con NLog o log4net Registrar todos los eventos: logins exitosos y fallidos, activación/desactivación de 2FA, registro y eliminación de passkeys, intentos MFA fallidos. Incluir siempre UserId, IP, UserAgent y timestamp UTC.
  • 🔒
    Limpiar challenges y sesiones expiradas Crear un job con Hangfire o Windows Task Scheduler que ejecute DELETE FROM WebAuthnChallenges WHERE ExpiresAt < GETUTCDATE() y lo mismo para PendingMfaSessions. Evita acumulación de registros obsoletos.
Guía de usuario final

Registro de 2FA y dispositivo seguro

Proceso paso a paso que debe seguir cada usuario para activar la autenticación de doble factor y registrar un dispositivo seguro con Passkey en el SSO.

Parte A — Activación del doble factor (2FA)

Este proceso se realiza una única vez por usuario. Se requiere acceso previo al SSO con usuario y contraseña, y tener instalada una aplicación de autenticación (Google Authenticator, Microsoft Authenticator o Authy).

1
Acceso inicial

Ingresar al SSO con credenciales

El usuario accede a la dirección del SSO e introduce su usuario y contraseña. En esta instancia aún no se solicita ningún segundo factor.

💡 Si el sistema ya tiene 2FA obligatorio por política, se redirigirá automáticamente al proceso de activación en el primer inicio de sesión.
2
Configuración de seguridad

Navegar a Configuración → Seguridad → Activar 2FA

Desde el panel principal del SSO, el usuario accede a su perfil o configuración de cuenta y selecciona la opción de autenticación de dos factores. El sistema presentará la pantalla de configuración.

3
Código QR

Escanear el código QR con la aplicación de autenticación

El sistema genera y muestra un código QR único vinculado a la cuenta. El usuario debe abrir su aplicación de autenticación y seleccionar la opción "Agregar cuenta" o el ícono +, luego apuntar la cámara al código QR en pantalla.

📱 Google Authenticator Botón + → Escanear código QR
🔷 Microsoft Authenticator + Agregar cuenta → Otra cuenta → QR
🛡️ Authy Agregar cuenta → Escanear
⚠️ Si no es posible escanear el QR, el sistema proporciona un código de texto alternativo (Base32) que puede ingresarse manualmente en la aplicación.
4
Verificación

Ingresar el código de 6 dígitos generado por la aplicación

Una vez escaneado el QR, la aplicación comenzará a mostrar un código de 6 dígitos que se renueva cada 30 segundos. El usuario debe ingresar ese código en el campo de verificación del SSO y confirmar. El sistema valida que la sincronización sea correcta.

⏱️ El código tiene una vigencia de 30 segundos. Si expira antes de enviarlo, simplemente se espera al siguiente ciclo.
5
Códigos de respaldo

Guardar los códigos de respaldo en lugar seguro

El sistema genera entre 8 y 10 códigos de un solo uso. Estos códigos son la única forma de acceder si el usuario pierde o cambia su dispositivo móvil. Deben guardarse en un lugar seguro y offline.

Opciones recomendadas Gestor de contraseñas, documento impreso guardado bajo llave
Opciones a evitar Correo electrónico, capturas de pantalla, notas en el teléfono
Completado

El doble factor queda activado

A partir de este momento, cada inicio de sesión en el SSO requerirá la contraseña más el código de 6 dígitos. El sistema confirmará la activación y enviará una notificación al correo registrado.


Parte B — Registro de dispositivo seguro (Passkey)

Las Passkeys reemplazan la contraseña con autenticación biométrica o PIN del dispositivo. Pueden registrarse múltiples dispositivos por cuenta (computadora, teléfono, tablet, llave de seguridad física).

ℹ️

Requisitos previos

El dispositivo debe contar con desbloqueo biométrico (huella o Face ID) o PIN configurado. El navegador debe ser Chrome 108+, Safari 16+, Edge 108+ o Firefox 122+. La conexión debe ser HTTPS.

1
Acceso autenticado

Iniciar sesión en el SSO con credenciales actuales

El registro de una Passkey requiere que el usuario ya esté autenticado en el SSO (con contraseña y 2FA si corresponde). Desde su sesión activa, debe dirigirse a Configuración → Seguridad → Dispositivos de confianza.

2
Inicio del registro

Seleccionar "Agregar Passkey" o "Registrar dispositivo seguro"

El sistema presenta la lista de dispositivos ya registrados (si los hubiera) y el botón para agregar uno nuevo. Al presionarlo, el servidor genera un desafío criptográfico único de corta duración que el dispositivo deberá firmar.

3
Diálogo del navegador

Confirmar la creación de la Passkey en el diálogo del sistema operativo

El navegador muestra un diálogo nativo del sistema operativo solicitando confirmación. El usuario debe autenticarse con su método local:

🤳 Face ID / Touch ID iPhone, iPad, Mac con Touch ID
🖐️ Huella dactilar / Windows Hello Android, Windows 10/11
🔑 Llave de seguridad física YubiKey u otra llave FIDO2
🔐 En este momento el dispositivo genera el par de claves criptográficas. La clave privada queda almacenada de forma segura en el dispositivo y nunca es transmitida al servidor.
4
Verificación del servidor

El servidor verifica y almacena la clave pública

El navegador envía la respuesta criptográfica firmada al servidor. El SSO verifica que la firma sea válida y que el challenge coincida, luego almacena únicamente la clave pública asociada a la cuenta del usuario. El proceso completo demora menos de 3 segundos.

5
Nombre del dispositivo

Asignar un nombre descriptivo al dispositivo registrado

Para facilitar la administración, el sistema solicita (o sugiere automáticamente) un nombre para el dispositivo, por ejemplo: "MacBook Pro — Oficina", "iPhone 15 — Personal", "YubiKey 5 — Respaldo". Este nombre aparecerá en la lista de dispositivos de confianza.

Completado

El dispositivo queda registrado como método de acceso

A partir de este momento, el usuario puede iniciar sesión en el SSO utilizando únicamente su biometría o PIN, sin necesidad de ingresar contraseña. Se recomienda registrar al menos dos dispositivos distintos para contar con una alternativa de acceso.

🛡️ Los dispositivos registrados pueden revisarse y revocarse en cualquier momento desde Configuración → Seguridad → Dispositivos de confianza.