Publicada:

.NET 6.0 - Tutorial de API estándar con registro de correo electrónico, verificación, autenticación y contraseña olvidada

Tutorial creado con .NET 6.0

Otras versiones disponibles:

En este tutorial, cubriremos cómo crear una API estándar de registro y autenticación en .NET 6.0 que admita la siguiente funcionalidad:

  • Registro y verificación de correo electrónico
  • Autenticación JWT con tokens de actualización
  • Autorización basada en roles con soporte para dos roles (User y Admin)
  • Olvidé mi contraseña y restablecí la función de contraseña
  • Rutas de administración de cuentas (CRUD) con control de acceso basado en roles
  • Ruta de documentación de la API de Swagger


Contenido del tutorial de .NET

El tutorial repetitivo está organizado en las siguientes secciones principales:


Descripción general estándar de .NET 6.0

La API repetitiva le permite registrar una cuenta de usuario, iniciar sesión y realizar diferentes acciones según su función. El rol de Admin tiene acceso total para administrar (agregar/editar/eliminar) cualquier cuenta en el sistema, el rol de User tiene acceso para actualizar/eliminar su propia cuenta. A la primera cuenta registrada se le asigna automáticamente el rol de Admin y a los registros posteriores se les asigna el rol de User.

Al registrarse, la API envía un correo electrónico de verificación con un token e instrucciones a la dirección de correo electrónico de la cuenta; las cuentas deben verificarse antes de poder autenticarse. La configuración de SMTP para el correo electrónico se configura en appsettings.json. Si no tiene un servicio SMTP, para una prueba rápida puede usar el servicio SMTP falso https://ethereal.email/ para crear una bandeja de entrada temporal, simplemente haga clic en Create Ethereal Account y copie las opciones de configuración de SMTP.

Descripción general de la implementación de la autenticación

La autenticación se implementa con tokens de acceso JWT y tokens de actualización. En una autenticación exitosa, la API devuelve un token de acceso JWT de corta duración que caduca después de 15 minutos y un token de actualización que caduca después de 7 días en una cookie HTTP Only. El JWT se usa para acceder a rutas seguras en la API y el token de actualización se usa para generar nuevos tokens de acceso JWT cuando (o justo antes) caducan.

Las cookies de solo HTTP se utilizan para tokens de actualización para aumentar la seguridad porque no son accesibles para JavaScript del lado del cliente, lo que evita ataques XSS (cross site scripting). Los tokens de actualización solo tienen acceso para generar nuevos tokens JWT (a través de la ruta /accounts/refresh-token), no pueden realizar ninguna otra acción segura que impida que se usen en CSRF (cross site request forgery) ataques.

Puntos finales de la API

La API de .NET 6 de ejemplo tiene los siguientes puntos finales/rutas para demostrar el registro y la verificación de correo electrónico, la autenticación y la autorización basada en funciones, la actualización y revocación de tokens, el olvido de la contraseña y el restablecimiento de la contraseña, y las rutas seguras de administración de cuentas:

  • POST /accounts/authenticate: ruta pública que acepta solicitudes POST que contienen un correo electrónico y una contraseña en el cuerpo. En caso de éxito, se devuelve un token de acceso JWT con detalles básicos de la cuenta y una cookie HTTP Only que contiene un token de actualización.
  • POST /accounts/refresh-token: ruta pública que acepta solicitudes POST que contienen una cookie con un token de actualización. En caso de éxito, se devuelve un nuevo token de acceso JWT con detalles básicos de la cuenta y una cookie solo HTTP que contiene un nuevo token de actualización (consulte rotación de token de actualización justo debajo para obtener una explicación).
  • POST /accounts/revoke-token: ruta segura que acepta solicitudes POST que contienen un token de actualización en el cuerpo de la solicitud o en una cookie, si ambos tienen prioridad se entrega al cuerpo de la solicitud. En caso de éxito, el token se revoca y ya no se puede usar para generar nuevos tokens de acceso JWT.
  • POST /accounts/register: ruta pública que acepta solicitudes POST que contienen detalles de registro de cuenta. En caso de éxito, la cuenta se registra y se envía un correo electrónico de verificación a la dirección de correo electrónico de la cuenta, las cuentas deben verificarse antes de que puedan autenticarse.
  • POST /accounts/verify-email: ruta pública que acepta solicitudes POST que contienen un token de verificación de cuenta. En caso de éxito, la cuenta se verifica y ahora puede iniciar sesión.
  • POST /accounts/forgot-password: ruta pública que acepta solicitudes POST que contienen una dirección de correo electrónico de cuenta. En caso de éxito, se envía un correo electrónico de restablecimiento de contraseña a la dirección de correo electrónico de la cuenta. El correo electrónico contiene un token de reinicio de un solo uso que es válido por un día.
  • POST /accounts/validate-reset-token: ruta pública que acepta solicitudes POST que contienen un token de restablecimiento de contraseña. Se devuelve un mensaje para indicar si el token es válido o no.
  • POST /accounts/reset-password: ruta pública que acepta solicitudes POST que contienen un token de restablecimiento, una contraseña y una contraseña de confirmación. En caso de éxito, la contraseña de la cuenta se restablece.
  • GET /accounts: ruta segura restringida al rol Admin que acepta solicitudes GET y devuelve una lista de todas las cuentas en la aplicación .
  • POST /accounts: ruta segura restringida al rol de Admin que acepta solicitudes POST que contienen detalles de cuentas nuevas. En caso de éxito, la cuenta se crea y se verifica automáticamente.
  • GET /accounts/{id}: ruta segura que acepta solicitudes GET y devuelve los detalles de la cuenta con la identificación especificada. El rol de Admin puede acceder a cualquier cuenta, el rol de User solo puede acceder a su propia cuenta.
  • PUT /accounts/{id}: ruta segura que acepta solicitudes PUT para actualizar los detalles de la cuenta con la identificación especificada. El rol de Admin puede actualizar cualquier cuenta, incluido su rol, el rol de User solo puede actualizar los detalles de su propia cuenta, excepto el rol.
  • DELETE /accounts/{id}: ruta segura que acepta solicitudes DELETE para eliminar la cuenta con la identificación especificada. El rol de Admin puede eliminar cualquier cuenta, el rol de User solo puede eliminar su propia cuenta.

Actualizar rotación de fichas (Refresh Token Rotation)

Cada vez que se usa un token de actualización para generar un nuevo token JWT (a través de la ruta /accounts/refresh-token), el token de actualización se revoca y se reemplaza por un nuevo token de actualización. Esta técnica se conoce como Rotación de token de actualización y aumenta la seguridad al reducir la vida útil de los tokens de actualización, lo que hace que sea menos probable que un token comprometido sea válido (o válido por mucho tiempo). Cuando se rota un token de actualización, el nuevo token se guarda en el campo ReplacedByToken del token revocado para crear una pista de auditoría en la base de datos.

Los registros de tokens de actualización revocados y caducados se mantienen en la base de datos durante la cantidad de días establecidos en la propiedad RefreshTokenTTL en appsettings.json archivo. El valor predeterminado es 2 días, después de los cuales el account service elimina los tokens inactivos antiguos en Authenticate() y RefreshToken() métodos.

Detección de reutilización de tokens revocados

Si se intenta generar un nuevo token JWT utilizando un token de actualización revocado, la API lo trata como un usuario potencialmente malicioso con un token de actualización robado (revocado) o un usuario válido que intenta acceder al sistema después de su token. ha sido revocado por un usuario malintencionado con un token de actualización robado (activo). En cualquier caso, la API revoca todos los tokens descendientes porque es probable que el token y sus descendientes se hayan creado en el mismo dispositivo que puede haberse visto comprometido. El motivo de la revocación se registra como "Attempted reuse of revoked ancestor token" contra los tokens revocados en la base de datos.

Instalación y configuración de la base de datos SQL

Para intentar simplificar las cosas, la API repetitiva utiliza una base de datos SQLite, SQLite es independiente y no requiere la instalación de un servidor de base de datos completo. La base de datos se crea automáticamente al iniciarse en el archivo Program.cs activando la ejecución de las migraciones de EF Core en la carpeta /Migrations.

Código en GitHub

El proyecto de API repetitivo está disponible en GitHub en https: //github.com/cornflourblue/dotnet-6-signup-verification-api.


Herramientas necesarias para ejecutar el ejemplo del tutorial de .NET 6.0 localmente

Para desarrollar y ejecutar aplicaciones .NET 6.0 localmente, descargue e instale lo siguiente:

  • SDK de .NET : incluye el tiempo de ejecución de .NET y las herramientas de línea de comandos
  • Visual Studio Code - editor de código que se ejecuta en Windows, Mac y Linux
  • Extensión de C# para Visual Studio Code: agrega compatibilidad con VS Code para desarrollar aplicaciones .NET


Instalar herramientas dotnet ef

Las herramientas de .NET Entity Framework Core (dotnet ef) se utilizan para generar migraciones de EF Core, para instalar las herramientas de EF Core globalmente ejecute dotnet tool install -g dotnet-ef, o para actualizar ejecute dotnet tool update -g dotnet-ef.

Para obtener más información sobre las herramientas de EF Core, consulte https://docs .microsoft.com/ef/core/cli/dotnet.

Para obtener más información sobre las migraciones de EF Core, consulte https:/ /docs.microsoft.com/ef/core/managing-schemas/migrations.


Ejecute la API estándar de .NET 6.0 localmente

  1. Descargue o clone el código del proyecto del tutorial desde https:/ /github.com/cornflourblue/dotnet-6-signup-verification-api
  2. Configure los ajustes de SMTP para el correo electrónico en la sección AppSettings del archivo /appsettings.json. Para una prueba rápida, puede crear una bandeja de entrada temporal en https://ethereal.email/ y copiar el Opciones de configuración de SMTP.
  3. Inicie la API ejecutando dotnet run desde la línea de comando en la carpeta raíz del proyecto (donde se encuentra el archivo WebApi.csproj), debería ver el mensaje Now listening on: http://localhost:4000, y puede ver la documentación de la API de Swagger en http://localhost:4000/swagger.
  4. Siga las instrucciones a continuación para probar con Postman o conectarse con una de las aplicaciones de ejemplo de una sola página disponibles (Angular o React).


Depuración en VS Code

Puede iniciar la aplicación en modo de depuración en VS Code abriendo la carpeta raíz del proyecto en VS Code y presionando F5 o seleccionando Depurar -> Inicie la depuración desde el menú superior. La ejecución en modo de depuración le permite adjuntar puntos de interrupción para pausar la ejecución y recorrer el código de la aplicación. Para obtener más información sobre la depuración de .NET en VS Code, consulte VS Code + .NET - Depurar una aplicación web .NET en Visual Studio Code.

Antes de ejecutarlo en producción

Antes de ejecutar en producción, también asegúrese de actualizar la propiedad Secret en el archivo appsettings.json, se usa para firmar y verificar los tokens JWT para la autenticación, cámbielo a una cadena aleatoria para asegurarse de que nadie más pueda generar un JWT con el mismo secreto y obtener acceso no autorizado a su API. Una forma rápida y fácil es unir un par de GUID para crear una cadena aleatoria larga (por ejemplo, desde https: //www.guidgenerator.com/).


Ejecute una aplicación Angular con la API estándar de .NET 6.0

Para obtener detalles completos sobre la aplicación repetitiva Angular 10, consulte la publicación Angular 10 Boilerplate - Email Sign Up with Verification, Authentication & Forgot Password. Pero para ponerse en marcha rápidamente, solo siga los pasos a continuación.

  1. Descargue o clone el código del tutorial de Angular 10 desde https://github.com/cornflourblue/angular-10-signup-verification-boilerplate
  2. Instale todos los paquetes npm necesarios ejecutando npm install desde la línea de comando en la carpeta raíz del proyecto (donde se encuentra el package.json).
  3. Elimine o comente la línea debajo del comentario // provider used to create fake backend ubicado en el archivo /src/app/app.module.ts.
  4. Inicie la aplicación ejecutando npm start desde la línea de comando en la carpeta raíz del proyecto, esto iniciará un navegador que muestra la aplicación y debe estar conectado con la API de .NET 6 Boilerplate que usted ya se está ejecutando.


Ejecute una aplicación React con la API estándar de .NET 6.0

Para obtener detalles completos sobre la aplicación estándar React, consulte la publicación React Boilerplate - Email Sign Up with Verification, Authentication & Forgot Password. Pero para ponerse en marcha rápidamente, solo siga los pasos a continuación.

  1. Descargue o clone el código del tutorial de React desde https://github.com/cornflourblue/react-signup-verification-boilerplate
  2. Instale todos los paquetes npm necesarios ejecutando npm install o npm i desde la línea de comandos en la carpeta raíz del proyecto (donde se encuentra el package.json).
  3. Elimine o comente las 2 líneas debajo del comentario // setup fake backend ubicado en el archivo /src/index.jsx.
  4. Inicie la aplicación ejecutando npm start desde la línea de comando en la carpeta raíz del proyecto, esto iniciará un navegador que muestra la aplicación y debe estar conectado con la API de .NET 6 Boilerplate que usted ya se está ejecutando.


Pruebe la API estándar de .NET 6.0 con Postman

Postman es una gran herramienta para probar API, puede descargarla en https://www.postman.com/descargas.

A continuación, encontrará instrucciones sobre cómo usar Postman para realizar las siguientes acciones:


Cómo registrar una nueva cuenta con Postman

Para registrar una nueva cuenta con la API repetitiva, siga estos pasos:

  1. Abra una nueva pestaña de solicitud haciendo clic en el botón más (+) al final de las pestañas.
  2. Cambie el método HTTP a POST con el selector desplegable a la izquierda del campo de entrada de URL.
  3. En el campo URL ingrese la dirección de la ruta de registro de su API local - http://localhost:4000/accounts/register
  4. Seleccione la pestaña Body debajo del campo URL, cambie el botón de opción de tipo de cuerpo a raw y cambie el selector desplegable de formato a JSON .
  5. Ingrese un objeto JSON que contenga las propiedades de cuenta requeridas en el área de texto Body, por ejemplo:
    {
        "title": "Mr",
        "firstName": "George",
        "lastName": "Costanza",
        "email": "[email protected]",
        "password": "george-likes-spicy-chicken",
        "confirmPassword": "george-likes-spicy-chicken",
        "acceptTerms": true
    }
  6. Haga clic en el botón Send, debería recibir "200 OK" respuesta con "registration successful" mensaje en el cuerpo de la respuesta.

Esta es una captura de pantalla de Postman después de enviar la solicitud y registrar la cuenta:

Y esta es una captura de pantalla del correo electrónico de verificación recibido con el token para verificar la cuenta:

Volver al principio


Cómo verificar una cuenta con Postman

Para verificar una cuenta con la API de .NET 6, siga estos pasos:

  1. Abra una nueva pestaña de solicitud haciendo clic en el botón más (+) al final de las pestañas.
  2. Cambie el método HTTP a POST con el selector desplegable a la izquierda del campo de entrada de URL.
  3. En el campo URL ingrese la dirección de la ruta de autenticación de su API local - http://localhost:4000/accounts/verify-email
  4. Seleccione la pestaña Body debajo del campo URL, cambie el botón de opción de tipo de cuerpo a raw y cambie el selector desplegable de formato a JSON .
  5. Ingrese un objeto JSON que contenga el token recibido en el correo electrónico de verificación (en el paso anterior) en el área de texto Body, p.ej:
    {
        "token": "REEMPLAZA ESTO CON TU TOKEN"
    }
  6. Haga clic en el botón Send, debería recibir "200 OK" respuesta con "verification successful" mensaje en el cuerpo de la respuesta.

Esta es una captura de pantalla de Postman después de enviar la solicitud y autenticar la cuenta:

Volver al principio


Cómo acceder a una cuenta si olvidó la contraseña

Para volver a habilitar el acceso a una cuenta con una contraseña olvidada, debe enviar la dirección de correo electrónico de la cuenta a la ruta /accounts/forgot-password, la ruta luego enviará un token a el correo electrónico que le permitirá restablecer la contraseña de la cuenta en el siguiente paso.

Siga estos pasos en Postman si olvidó la contraseña de su cuenta:

  1. Abra una nueva pestaña de solicitud haciendo clic en el botón más (+) al final de las pestañas.
  2. Cambie el método HTTP a POST con el selector desplegable a la izquierda del campo de entrada de URL.
  3. En el campo URL ingrese la dirección de la ruta de autenticación de su API local - http://localhost:4000/accounts/forgot-password
  4. Seleccione la pestaña Body debajo del campo URL, cambie el botón de opción de tipo de cuerpo a raw y cambie el selector desplegable de formato a JSON .
  5. Ingrese un objeto JSON que contenga el correo electrónico de la cuenta con la contraseña olvidada en el área de texto Body, por ejemplo:
    {
        "email": "[email protected]"
    }
  6. Haga clic en el botón Send, debería recibir "200 OK" respuesta con el mensaje "Revise su correo electrónico para obtener instrucciones para restablecer la contraseña" en el cuerpo de la respuesta.

Esta es una captura de pantalla de Postman después de enviar la solicitud y enviar el correo electrónico:

Y esta es una captura de pantalla del correo electrónico recibido con el token para restablecer la contraseña de la cuenta:

Volver al principio


Cómo restablecer la contraseña de una cuenta con Postman

Para restablecer la contraseña de una cuenta con la API sigue estos pasos:

  1. Abra una nueva pestaña de solicitud haciendo clic en el botón más (+) al final de las pestañas.
  2. Cambie el método HTTP a POST con el selector desplegable a la izquierda del campo de entrada de URL.
  3. En el campo URL ingrese la dirección de la ruta de autenticación de su API local - http://localhost:4000/accounts/reset-password
  4. Seleccione la pestaña Body debajo del campo URL, cambie el botón de opción de tipo de cuerpo a raw y cambie el selector desplegable de formato a JSON .
  5. Ingrese un objeto JSON que contenga el token de restablecimiento de contraseña recibido en el correo electrónico del paso contraseña olvidada, junto con una nueva contraseña y confirmPassword coincidente, en el Body área de texto, por ejemplo:
    {
        "token": "REEMPLAZA ESTO CON TU TOKEN",
        "password": "george-is-gettin-upset!",
        "confirmPassword": "george-is-gettin-upset!"
    }
  6. Haga clic en el botón Send, debería recibir "200 OK" respuesta con un "restablecimiento de contraseña exitoso" mensaje en el cuerpo de la respuesta.

Esta es una captura de pantalla de Postman después de enviar la solicitud y restablecer la contraseña de la cuenta:

Volver al principio


Cómo autenticarse con Postman

Para autenticar una cuenta con la API y obtener un token JWT, siga estos pasos:

  1. Abra una nueva pestaña de solicitud haciendo clic en el botón más (+) al final de las pestañas.
  2. Cambie el método HTTP a POST con el selector desplegable a la izquierda del campo de entrada de URL.
  3. En el campo URL ingrese la dirección de la ruta de autenticación de su API local - http://localhost:4000/accounts/authenticate
  4. Seleccione la pestaña Body debajo del campo URL, cambie el botón de opción de tipo de cuerpo a raw y cambie el selector desplegable de formato a JSON .
  5. Ingrese un objeto JSON que contenga el correo electrónico y la contraseña de la cuenta en el área de texto Body:
    {
        "email": "[email protected]",
        "password": "george-is-gettin-upset!"
    }
  6. Haga clic en el botón Send, debería recibir "200 OK" respuesta con los detalles de la cuenta, incluido un token JWT en el cuerpo de la respuesta y un token de actualización en las cookies de respuesta.
  7. Copie el valor del token JWT porque lo usaremos en los próximos pasos para realizar solicitudes autenticadas.

Esta es una captura de pantalla de Postman después de enviar la solicitud y autenticar la cuenta:

Y esto muestra la pestaña de cookies con el token de actualización en la respuesta:

Volver al principio


Cómo obtener una lista de todas las cuentas con Postman

Esta es una solicitud segura que requiere un token de autenticación JWT del paso autenticar. La ruta API está restringida al rol Admin.

Para obtener una lista de todas las cuentas de la API repetitiva de .NET 6, siga estos pasos:

  1. Abra una nueva pestaña de solicitud haciendo clic en el botón más (+) al final de las pestañas.
  2. Cambie el método HTTP a GET con el selector desplegable a la izquierda del campo de entrada de URL.
  3. En el campo URL ingrese la dirección a la ruta de cuentas de su API local - http://localhost:4000/accounts
  4. Seleccione la pestaña Authorization debajo del campo URL, cambie el tipo a Bearer Token en el selector desplegable de tipos y pegue el token JWT del paso de autenticación anterior en el Campo Token.
  5. Haga clic en el botón Send, debería recibir "200 OK" respuesta que contiene una matriz JSON con todos los registros de cuenta en el sistema.

Esta es una captura de pantalla de Postman después de realizar una solicitud autenticada para obtener todas las cuentas:

Volver al principio


Cómo actualizar una cuenta con Postman

Esta es una solicitud segura que requiere un token de autenticación JWT del paso autenticar. Las cuentas de Admin pueden actualizar cualquier cuenta, incluido su rol, las cuentas de User están restringidas a su propia cuenta y no pueden actualizar el rol. Las propiedades omitidas o vacías se ignoran y no se actualizan.

Para actualizar una cuenta con la API, siga estos pasos:

  1. Abra una nueva pestaña de solicitud haciendo clic en el botón más (+) al final de las pestañas.
  2. Cambie el método HTTP a PUT con el selector desplegable a la izquierda del campo de entrada de URL.
  3. En el campo URL ingrese la dirección a la ruta /accounts/{id} con la identificación de la cuenta que desea actualizar, por ejemplo, http://localhost:4000/accounts/1
  4. Seleccione la pestaña Authorization debajo del campo URL, cambie el tipo a Bearer Token en el selector desplegable de tipos y pegue el token JWT del paso de autenticación anterior en el Campo Token.
  5. Seleccione la pestaña Body debajo del campo URL, cambie el botón de opción de tipo de cuerpo a raw y cambie el selector desplegable de formato a JSON .
  6. Ingrese un objeto JSON en el área de texto Body que contenga las propiedades que desea actualizar, por ejemplo, para actualizar el nombre y el apellido:
    {
        "firstName": "Art",
        "lastName": "Vandelay"
    }
  7. Haga clic en el botón Send, debería recibir "200 OK" respuesta con los detalles actualizados de la cuenta en el cuerpo de la respuesta.

Esta es una captura de pantalla de Postman después de enviar la solicitud y actualizar la cuenta:

Volver al principio


Cómo usar un token de actualización para obtener un nuevo token JWT

Este paso solo se puede realizar después del paso autenticar porque se requiere una cookie de token de actualización válida.

Para usar una cookie de token de actualización para obtener un nuevo token JWT y un nuevo token de actualización, siga estos pasos:

  1. Abra una nueva pestaña de solicitud haciendo clic en el botón más (+) al final de las pestañas.
  2. Cambie el método HTTP a POST con el selector desplegable a la izquierda del campo de entrada de URL.
  3. En el campo URL, ingrese la dirección de la ruta del token de actualización de su API local - http://localhost:4000/accounts/refresh-token
  4. Haga clic en el botón Send, debería recibir "200 OK" respuesta con los detalles de la cuenta, incluido un nuevo token JWT en el cuerpo de la respuesta y un nuevo token de actualización en las cookies de respuesta.
  5. Copie el valor del token JWT porque lo usaremos en los próximos pasos para realizar solicitudes autenticadas.

Esta es una captura de pantalla de Postman después de enviar la solicitud y actualizar el token:

Y esto muestra la pestaña de cookies con el token de actualización en la respuesta:

Volver al principio


Cómo revocar un token de actualización con Postman

Esta es una solicitud segura que requiere un token de autenticación JWT del paso autenticar (o token de actualización). Las cuentas Admin pueden revocar los tokens de cualquier cuenta, las cuentas User solo pueden revocar sus propios tokens.

Para revocar un token de actualización para que ya no pueda usarse para generar tokens JWT, siga estos pasos:

  1. Abra una nueva pestaña de solicitud haciendo clic en el botón más (+) al final de las pestañas.
  2. Cambie el método HTTP a POST con el selector desplegable a la izquierda del campo de entrada de URL.
  3. En el campo URL ingrese la dirección de la ruta de autenticación de su API local - http://localhost:4000/accounts/revoke-token
  4. Seleccione la pestaña Authorization debajo del campo URL, cambie el tipo a Bearer Token en el selector desplegable de tipos y pegue el token JWT de la autenticación anterior (o actualice token) acceda al campo Token.
  5. Seleccione la pestaña Body debajo del campo URL, cambie el botón de opción de tipo de cuerpo a raw y cambie el selector desplegable de formato a JSON .
  6. Ingrese un objeto JSON que contenga el token de actualización activo del paso anterior en el área de texto Body, por ejemplo:
    {
        "token": "INTRODUZCA AQUÍ EL TOKEN DE REFRESCO ACTIVO"
    }
  7. Haga clic en el botón Send, debería recibir "200 OK" respuesta con el mensaje Token revoked.

Nota: También puede revocar el token en la cookie refreshToken con la ruta /accounts/revoke-token, para revocar la cookie del token de actualización simplemente envíe la misma solicitud con un cuerpo vacío.

Esta es una captura de pantalla de Postman después de realizar la solicitud y de que se haya revocado el token:

Volver al principio


Cómo eliminar una cuenta con Postman

Esta es una solicitud segura que requiere un token de autenticación JWT del paso autenticar. Las cuentas de Admin pueden eliminar cualquier cuenta, las cuentas de User están restringidas a su propia cuenta.

Para eliminar una cuenta con la API, siga estos pasos:

  1. Abra una nueva pestaña de solicitud haciendo clic en el botón más (+) al final de las pestañas.
  2. Cambie el método HTTP a ELIMINAR con el selector desplegable a la izquierda del campo de entrada de URL.
  3. En el campo URL ingrese la dirección a la ruta /accounts/{id} con la identificación de la cuenta que desea eliminar, por ejemplo, http ://localhost:4000/accounts/1
  4. Seleccione la pestaña Authorization debajo del campo URL, cambie el tipo a Bearer Token en el selector desplegable de tipos y pegue el token JWT del paso de autenticación anterior en el Campo Token.
  5. Haga clic en el botón Send, debería recibir "200 OK" respuesta con el mensaje "Account deleted successfully" en el cuerpo de la respuesta.

Esta es una captura de pantalla de Postman después de enviar la solicitud y eliminar la cuenta:

Volver al principio


Estructura de proyecto estándar de .NET 6.0

El proyecto de tutorial de .NET está organizado en las siguientes carpetas:

Authorization
Contiene las clases responsables de implementar la autenticación y autorización JWT personalizada en la API.

Controllers
Defina los puntos finales/rutas para la API, los métodos de acción del controlador son los puntos de entrada a la API para aplicaciones cliente a través de solicitudes HTTP.

Migrations
Archivos de migración de bases de datos basados ​​en las clases de la carpeta /Entities que se utilizan para crear y actualizar automáticamente la base de datos para la API. Las migraciones se generan con Entity Framework Core Tools para .NET CLI (dotnet ef), las migraciones de este ejemplo se generaron con el comando dotnet ef migrations add InitialCreate.

Models
Representar modelos de solicitud y respuesta para métodos de acción del controlador. Los modelos de solicitud definen los parámetros para las solicitudes entrantes y los modelos de respuesta definen los datos que se devuelven.

Services
Contiene lógica de negocios, validación y código de acceso a la base de datos.

Entities
Representa los datos de la aplicación que se almacenan en la base de datos.
Entity Framework Core (EF Core) asigna datos relacionales de la base de datos a instancias de objetos de entidad de C# en la aplicación para la gestión de datos y operaciones CRUD.

Helpers
Cualquier cosa que no encaje en las carpetas anteriores.

Haga clic en los enlaces a continuación para saltar a una descripción de cada archivo junto con su código:

 

Atributo AllowAnonymous de .NET

Ruta: /Authorization/AllowAnonymousAttribute.cs

El atributo personalizado [AllowAnonymous] se usa para permitir el acceso público a métodos de acción específicos cuando una clase de controlador está decorada con el atributo [Authorize]. Se utiliza en el accounts controller para permitir el acceso anónimo a varios métodos, incluidos Authenticate, Register y ForgotPassword.

La lógica para permitir el acceso público se encuentra en el atributo de autorización personalizado a continuación, la autorización se omite si el método de acción está decorado con [AllowAnonymous].

La razón por la que creé un atributo personalizado AllowAnonymous en lugar de usar el del marco .NET Core (Microsoft.AspNetCore.Authorization) es por coherencia con el otro atributo personalizado. clases de autenticación en el proyecto y para evitar errores de referencia ambiguos entre espacios de nombres.

namespace WebApi.Authorization;

[AttributeUsage(AttributeTargets.Method)]
public class AllowAnonymousAttribute : Attribute
{ }
 

Atributo Authorize personalizado de .NET

Ruta: /Authorization/AuthorizeAttribute.cs

El atributo personalizado [Authorize] se agrega a las clases de controlador o métodos de acción que requieren que el usuario esté autenticado y, opcionalmente, tenga un rol específico. Si se especifica un rol (p. ej., [Authorize(Role.Admin)]), la ruta está restringida a los usuarios en ese rol; de lo contrario, la ruta está restringida a todos los usuarios autenticados, independientemente del rol.

Cuando una clase de controlador está decorada con el atributo [Authorize], todos los métodos de acción en el controlador están restringidos a solicitudes autorizadas, excepto los métodos decorados con el atributo [AllowAnonymous] personalizado anterior.

La autorización se realiza mediante el método OnAuthorization que comprueba si hay un usuario autenticado adjunto a la solicitud actual (context.HttpContext.Items["User"]) y que el usuario tiene un rol autorizado (si se especifica).

En caso de autorización exitosa, no se realiza ninguna acción y la solicitud se pasa al método de acción del controlador; si la autorización falla, se devuelve una respuesta 401 Unauthorized.

namespace WebApi.Authorization;

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using WebApi.Entities;

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class AuthorizeAttribute : Attribute, IAuthorizationFilter
{
    private readonly IList<Role> _roles;

    public AuthorizeAttribute(params Role[] roles)
    {
        _roles = roles ?? new Role[] { };
    }

    public void OnAuthorization(AuthorizationFilterContext context)
    {
        // skip authorization if action is decorated with [AllowAnonymous] attribute
        var allowAnonymous = context.ActionDescriptor.EndpointMetadata.OfType<AllowAnonymousAttribute>().Any();
        if (allowAnonymous)
            return;

        // authorization
        var account = (Account)context.HttpContext.Items["Account"];
        if (account == null || (_roles.Any() && !_roles.Contains(account.Role)))
        {
            // not logged in or role not authorized
            context.Result = new JsonResult(new { message = "Unauthorized" }) { StatusCode = StatusCodes.Status401Unauthorized };
        }
    }
}
 

Middleware JWT personalizado de .NET

Ruta: /Authorization/JwtMiddleware.cs

El middleware JWT personalizado extrae el token JWT del encabezado de solicitud Authorization (si lo hay) y lo valida con el método jwtUtils.ValidateToken(). Si la validación es exitosa, se devuelve la identificación de cuenta del token y el objeto de cuenta autenticado se agrega a la colección HttpContext.Items, lo que lo hace accesible a otras clases dentro del alcance de la solicitud actual.

Si la validación del token falla (o no hay token), la solicitud es anónima y solo se permite acceder a rutas públicas porque no hay un objeto de cuenta autenticado adjunto al contexto HTTP. La lógica de autorización que comprueba el objeto de la cuenta se encuentra en el atributo de autorización personalizado anterior. Cuando se envía una solicitud anónima/no autorizada a una ruta segura, el atributo de autorización devuelve una respuesta HTTP 401 Unauthorized.

namespace WebApi.Authorization;

using Microsoft.Extensions.Options;
using WebApi.Helpers;

public class JwtMiddleware
{
    private readonly RequestDelegate _next;
    private readonly AppSettings _appSettings;

    public JwtMiddleware(RequestDelegate next, IOptions<AppSettings> appSettings)
    {
        _next = next;
        _appSettings = appSettings.Value;
    }

    public async Task Invoke(HttpContext context, DataContext dataContext, IJwtUtils jwtUtils)
    {
        var token = context.Request.Headers["Authorization"].FirstOrDefault()?.Split(" ").Last();
        var accountId = jwtUtils.ValidateJwtToken(token);
        if (accountId != null)
        {
            // attach account to context on successful jwt validation
            context.Items["Account"] = await dataContext.Accounts.FindAsync(accountId.Value);
        }

        await _next(context);
    }
}
 

Utilidades JWT de .NET

Ruta: /Authorization/JwtUtils.cs

La clase JWTUtils contiene métodos para generar y validar tokens JWT y generar tokens de actualización.

El método GenerateJwtToken() devuelve un token JWT de corta duración que caduca después de 15 minutos, contiene la identificación de la account especificada como "id", lo que significa que la carga del token contendrá la propiedad "id": <accountId> (p. ej., "id": 1). El token se crea con la clase JwtSecurityTokenHandler y se firma digitalmente con la clave secreta almacenada en el archivo app settings.

El método ValidateJwtToken() intenta validar el token JWT proporcionado y devolver la identificación de la cuenta ("id") de las reclamaciones del token. Si la validación falla, se devuelve nulo.

El método GenerateRefreshToken() devuelve un nuevo token de actualización que caduca después de 7 días. La fecha de creación y la dirección IP de la solicitud se guardan con el token para crear un registro de auditoría y ayudar a identificar cualquier actividad inusual. El método devuelve una cadena de token de actualización única garantizada al verificar que aún no existe en la base de datos y llamarse recursivamente a sí mismo si existe.

namespace WebApi.Authorization;

using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Security.Cryptography;
using System.Text;
using WebApi.Entities;
using WebApi.Helpers;

public interface IJwtUtils
{
    public string GenerateJwtToken(Account account);
    public int? ValidateJwtToken(string token);
    public RefreshToken GenerateRefreshToken(string ipAddress);
}

public class JwtUtils : IJwtUtils
{
    private readonly DataContext _context;
    private readonly AppSettings _appSettings;

    public JwtUtils(
        DataContext context,
        IOptions<AppSettings> appSettings)
    {
        _context = context;
        _appSettings = appSettings.Value;
    }

    public string GenerateJwtToken(Account account)
    {
        // generate token that is valid for 15 minutes
        var tokenHandler = new JwtSecurityTokenHandler();
        var key = Encoding.ASCII.GetBytes(_appSettings.Secret);
        var tokenDescriptor = new SecurityTokenDescriptor
        {
            Subject = new ClaimsIdentity(new[] { new Claim("id", account.Id.ToString()) }),
            Expires = DateTime.UtcNow.AddMinutes(15),
            SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature)
        };
        var token = tokenHandler.CreateToken(tokenDescriptor);
        return tokenHandler.WriteToken(token);
    }

    public int? ValidateJwtToken(string token)
    {
        if (token == null)
            return null;

        var tokenHandler = new JwtSecurityTokenHandler();
        var key = Encoding.ASCII.GetBytes(_appSettings.Secret);
        try
        {
            tokenHandler.ValidateToken(token, new TokenValidationParameters
            {
                ValidateIssuerSigningKey = true,
                IssuerSigningKey = new SymmetricSecurityKey(key),
                ValidateIssuer = false,
                ValidateAudience = false,
                // set clockskew to zero so tokens expire exactly at token expiration time (instead of 5 minutes later)
                ClockSkew = TimeSpan.Zero
            }, out SecurityToken validatedToken);

            var jwtToken = (JwtSecurityToken)validatedToken;
            var accountId = int.Parse(jwtToken.Claims.First(x => x.Type == "id").Value);

            // return account id from JWT token if validation successful
            return accountId;
        }
        catch
        {
            // return null if validation fails
            return null;
        }
    }

    public RefreshToken GenerateRefreshToken(string ipAddress)
    {
        var refreshToken = new RefreshToken
        {
            // token is a cryptographically strong random sequence of values
            Token = Convert.ToHexString(RandomNumberGenerator.GetBytes(64)),
            // token is valid for 7 days
            Expires = DateTime.UtcNow.AddDays(7),
            Created = DateTime.UtcNow,
            CreatedByIp = ipAddress
        };

        // ensure token is unique by checking against db
        var tokenIsUnique = !_context.Accounts.Any(a => a.RefreshTokens.Any(t => t.Token == refreshToken.Token));

        if (!tokenIsUnique)
            return GenerateRefreshToken(ipAddress);

        return refreshToken;
    }
}
 

Controlador Accounts de .NET

Ruta: /Controllers/AccountsController.cs

El controlador de cuentas define y maneja todas las rutas/puntos finales para la API que se relacionan con las cuentas, incluidas las de registro y amp; verificación, autenticación y amp; olvidé la contraseña, actualizando & revocación de tokens y operaciones de administración de cuentas (CRUD). Dentro de cada método de ruta, el controlador llama al servicio de cuenta para realizar la acción requerida, esto permite que el controlador se mantenga 'ajustado' y completamente separada de la lógica de negocio y el código de acceso a datos de cada acción.

Los métodos/rutas del controlador son seguros de manera predeterminada con el atributo [Authorize] en la clase, los métodos que están restringidos a un rol específico están decorados con el atributo Authorize y el rol (p. ej., [Authorize(Role.Admin)]). Varios métodos están decorados con el atributo [AllowAnonymous] que anula el atributo [Authorize] de nivel de clase para hacerlos accesibles públicamente. Elegí este enfoque para evitar que se utilicen métodos nuevos. hecho público accidentalmente. La lógica de autenticación se encuentra en el atributo de autorización personalizado.

Los métodos de ruta RevokeToken, GetById, Update y Delete incluyen una verificación de autorización personalizada adicional para evitar cuentas que no son de administrador para acceder a otras cuentas que no sean las suyas. Las cuentas User tienen acceso CRUD a su propia cuenta pero no a otras, las cuentas Admin tienen acceso CRUD completo a todas las cuentas.

El método auxiliar setTokenCookie() agrega una cookie HTTP Only que contiene el token de actualización a la respuesta para mayor seguridad. Las cookies HTTP solo no son accesibles para javascript del lado del cliente, lo que evita XSS (cross site scripting), y el token de actualización solo se puede usar para obtener un nuevo token de la ruta /accounts/refresh-token que previene CSRF (cross site request forgery).

namespace WebApi.Controllers;

using Microsoft.AspNetCore.Mvc;
using WebApi.Authorization;
using WebApi.Entities;
using WebApi.Models.Accounts;
using WebApi.Services;

[Authorize]
[ApiController]
[Route("[controller]")]
public class AccountsController : BaseController
{
    private readonly IAccountService _accountService;

    public AccountsController(IAccountService accountService)
    {
        _accountService = accountService;
    }

    [AllowAnonymous]
    [HttpPost("authenticate")]
    public ActionResult<AuthenticateResponse> Authenticate(AuthenticateRequest model)
    {
        var response = _accountService.Authenticate(model, ipAddress());
        setTokenCookie(response.RefreshToken);
        return Ok(response);
    }

    [AllowAnonymous]
    [HttpPost("refresh-token")]
    public ActionResult<AuthenticateResponse> RefreshToken()
    {
        var refreshToken = Request.Cookies["refreshToken"];
        var response = _accountService.RefreshToken(refreshToken, ipAddress());
        setTokenCookie(response.RefreshToken);
        return Ok(response);
    }

    [HttpPost("revoke-token")]
    public IActionResult RevokeToken(RevokeTokenRequest model)
    {
        // accept token from request body or cookie
        var token = model.Token ?? Request.Cookies["refreshToken"];

        if (string.IsNullOrEmpty(token))
            return BadRequest(new { message = "Token is required" });

        // users can revoke their own tokens and admins can revoke any tokens
        if (!Account.OwnsToken(token) && Account.Role != Role.Admin)
            return Unauthorized(new { message = "Unauthorized" });

        _accountService.RevokeToken(token, ipAddress());
        return Ok(new { message = "Token revoked" });
    }

    [AllowAnonymous]
    [HttpPost("register")]
    public IActionResult Register(RegisterRequest model)
    {
        _accountService.Register(model, Request.Headers["origin"]);
        return Ok(new { message = "Registration successful, please check your email for verification instructions" });
    }

    [AllowAnonymous]
    [HttpPost("verify-email")]
    public IActionResult VerifyEmail(VerifyEmailRequest model)
    {
        _accountService.VerifyEmail(model.Token);
        return Ok(new { message = "Verification successful, you can now login" });
    }

    [AllowAnonymous]
    [HttpPost("forgot-password")]
    public IActionResult ForgotPassword(ForgotPasswordRequest model)
    {
        _accountService.ForgotPassword(model, Request.Headers["origin"]);
        return Ok(new { message = "Please check your email for password reset instructions" });
    }

    [AllowAnonymous]
    [HttpPost("validate-reset-token")]
    public IActionResult ValidateResetToken(ValidateResetTokenRequest model)
    {
        _accountService.ValidateResetToken(model);
        return Ok(new { message = "Token is valid" });
    }

    [AllowAnonymous]
    [HttpPost("reset-password")]
    public IActionResult ResetPassword(ResetPasswordRequest model)
    {
        _accountService.ResetPassword(model);
        return Ok(new { message = "Password reset successful, you can now login" });
    }

    [Authorize(Role.Admin)]
    [HttpGet]
    public ActionResult<IEnumerable<AccountResponse>> GetAll()
    {
        var accounts = _accountService.GetAll();
        return Ok(accounts);
    }

    [HttpGet("{id:int}")]
    public ActionResult<AccountResponse> GetById(int id)
    {
        // users can get their own account and admins can get any account
        if (id != Account.Id && Account.Role != Role.Admin)
            return Unauthorized(new { message = "Unauthorized" });

        var account = _accountService.GetById(id);
        return Ok(account);
    }

    [Authorize(Role.Admin)]
    [HttpPost]
    public ActionResult<AccountResponse> Create(CreateRequest model)
    {
        var account = _accountService.Create(model);
        return Ok(account);
    }

    [HttpPut("{id:int}")]
    public ActionResult<AccountResponse> Update(int id, UpdateRequest model)
    {
        // users can update their own account and admins can update any account
        if (id != Account.Id && Account.Role != Role.Admin)
            return Unauthorized(new { message = "Unauthorized" });

        // only admins can update role
        if (Account.Role != Role.Admin)
            model.Role = null;

        var account = _accountService.Update(id, model);
        return Ok(account);
    }

    [HttpDelete("{id:int}")]
    public IActionResult Delete(int id)
    {
        // users can delete their own account and admins can delete any account
        if (id != Account.Id && Account.Role != Role.Admin)
            return Unauthorized(new { message = "Unauthorized" });

        _accountService.Delete(id);
        return Ok(new { message = "Account deleted successfully" });
    }

    // helper methods

    private void setTokenCookie(string token)
    {
        var cookieOptions = new CookieOptions
        {
            HttpOnly = true,
            Expires = DateTime.UtcNow.AddDays(7)
        };
        Response.Cookies.Append("refreshToken", token, cookieOptions);
    }

    private string ipAddress()
    {
        if (Request.Headers.ContainsKey("X-Forwarded-For"))
            return Request.Headers["X-Forwarded-For"];
        else
            return HttpContext.Connection.RemoteIpAddress.MapToIPv4().ToString();
    }
}
 

Controlador Base de .NET

Ruta: /Controllers/BaseController.cs

El controlador base es heredado por todos los demás controladores en la API repetitiva e incluye propiedades y métodos comunes a los que pueden acceder todos los controladores.

La propiedad Account devuelve la cuenta autenticada actual para la solicitud de la colección HttpContext.Items, o devuelve null si la solicitud no es autenticado La cuenta actual se agrega a la colección HttpContext.Items mediante el middleware jwt personalizado cuando la solicitud contiene un token JWT válido en el encabezado de autorización.

namespace WebApi.Controllers;

using Microsoft.AspNetCore.Mvc;
using WebApi.Entities;

[Controller]
public abstract class BaseController : ControllerBase
{
    // returns the current authenticated account (null if not logged in)
    public Account Account => (Account)HttpContext.Items["Account"];
}
 

Entidad Account de .NET

Ruta: /Entities/Account.cs

La clase de entidad de cuenta representa los datos de una cuenta en la aplicación.

La propiedad IsVerified devuelve verdadero si la fecha Verified o PasswordReset tiene un valor, esto es para habilitar la verificación de la cuenta después del registro a través de los pasos de contraseña olvidada + restablecer contraseña.

El método OwnsToken es un método conveniente que devuelve verdadero si el token de actualización especificado pertenece a la cuenta, se usa en el método RevokeToken del controlador de cuentas para mejorar la legibilidad del código haciéndolo más revelador de intenciones.

namespace WebApi.Entities;

public class Account
{
    public int Id { get; set; }
    public string Title { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string Email { get; set; }
    public string PasswordHash { get; set; }
    public bool AcceptTerms { get; set; }
    public Role Role { get; set; }
    public string VerificationToken { get; set; }
    public DateTime? Verified { get; set; }
    public bool IsVerified => Verified.HasValue || PasswordReset.HasValue;
    public string ResetToken { get; set; }
    public DateTime? ResetTokenExpires { get; set; }
    public DateTime? PasswordReset { get; set; }
    public DateTime Created { get; set; }
    public DateTime? Updated { get; set; }
    public List<RefreshToken> RefreshTokens { get; set; }

    public bool OwnsToken(string token) 
    {
        return this.RefreshTokens?.Find(x => x.Token == token) != null;
    }
}
 

Entidad Refresh Token de .NET

Ruta: /Entities/RefreshToken.cs

La clase de entidad del token de actualización representa los datos para un token de actualización en la aplicación.

El atributo [Owned] marca la clase de token de actualización como un tipo de entidad de propiedad, lo que significa que solo puede existir como elemento secundario o dependiente de otra clase de entidad. En este ejemplo, un token de actualización siempre es propiedad de una entidad de cuenta.

El atributo [Key] establece explícitamente el campo id como clave principal en la tabla de la base de datos. EF Core convierte automáticamente las propiedades con el nombre Id en claves principales; sin embargo, en el caso de entidades Owned, EF Core crea una clave principal compuesta que consiste en el ID y el ID del propietario. lo que puede causar errores con los campos de identificación generados automáticamente. Marcar explícitamente la identificación con el atributo [Key] le dice a EF Core que solo haga que el campo de identificación sea la clave principal en la tabla db.

namespace WebApi.Entities;

using Microsoft.EntityFrameworkCore;
using System.ComponentModel.DataAnnotations;

[Owned]
public class RefreshToken
{
    [Key]
    public int Id { get; set; }
    public Account Account { get; set; }
    public string Token { get; set; }
    public DateTime Expires { get; set; }
    public DateTime Created { get; set; }
    public string CreatedByIp { get; set; }
    public DateTime? Revoked { get; set; }
    public string RevokedByIp { get; set; }
    public string ReplacedByToken { get; set; }
    public string ReasonRevoked { get; set; }
    public bool IsExpired => DateTime.UtcNow >= Expires;
    public bool IsRevoked => Revoked != null;
    public bool IsActive => Revoked == null && !IsExpired;
}
 

Enumeración de Role de .NET

Ruta: /Entities/Role.cs

La enumeración de roles define todos los roles disponibles en la API repetitiva de .NET. Lo creé para evitar pasar roles como cadenas, así que en lugar de 'Admin' podemos usar Role.Admin.

namespace WebApi.Entities;

public enum Role
{
    Admin,
    User
}
 

Excepción App de .NET

Ruta: /Helpers/AppException.cs

La excepción de aplicación es una clase de excepción personalizada que se utiliza para diferenciar entre excepciones controladas y no controladas. Las excepciones controladas son las generadas por la aplicación y se utilizan para mostrar mensajes de error amigables al cliente, por ejemplo, lógica de negocios o excepciones de validación causadas por una entrada incorrecta del usuario. Las excepciones no controladas son generadas por .NET Framework y pueden deberse a errores en el código de la aplicación.

Consulte el servicio de cuentas para ver ejemplos de excepciones de aplicaciones que se generan. Vea cómo se manejan los diferentes tipos de excepciones en el middleware del controlador de errores.

namespace WebApi.Helpers;

using System.Globalization;

// custom exception class for throwing application specific exceptions 
// that can be caught and handled within the application
public class AppException : Exception
{
    public AppException() : base() {}

    public AppException(string message) : base(message) { }

    public AppException(string message, params object[] args) 
        : base(String.Format(CultureInfo.CurrentCulture, message, args))
    {
    }
}
 

Clase App Settings de .NET

Ruta: /Helpers/AppSettings.cs

La clase de configuración de la aplicación contiene propiedades definidas en el archivo appsettings.json y se usa para acceder a la configuración de la aplicación a través de objetos que se inyectan en las clases mediante la extensión . NET integrado en el sistema de inyección de dependencia (DI). Por ejemplo, el servicio de cuenta accede a la configuración de la aplicación a través de un IOptions<AppSettings> appSettings objeto que se inyecta en el constructor.

La asignación de secciones de configuración a clases se realiza al inicio en el archivo Program.cs.

namespace WebApi.Helpers;

public class AppSettings
{
    public string Secret { get; set; }

    // refresh token time to live (in days), inactive tokens are
    // automatically deleted from the database after this time
    public int RefreshTokenTTL { get; set; }

    public string EmailFrom { get; set; }
    public string SmtpHost { get; set; }
    public int SmtpPort { get; set; }
    public string SmtpUser { get; set; }
    public string SmtpPass { get; set; }
}
 

Perfil de .NET AutoMapper

Ruta: /Helpers/AutoMapperProfile.cs

El perfil de automapper contiene la configuración de mapeo utilizada por la aplicación .NET, AutoMapper es un paquete disponible en Nuget que permite el mapeo automático de valores de propiedad entre diferentes tipos de clase en función de los nombres de propiedad. En el ejemplo, lo usamos para mapear entre entidades Account y algunos tipos diferentes de modelos de solicitud y respuesta.

La asignación de UpdateRequest a Account incluye alguna configuración personalizada para ignorar las propiedades vacías en el modelo de solicitud cuando se asigna a una entidad de cuenta, esto es para que los campos sean opcionales al actualizar una cuenta.

namespace WebApi.Helpers;

using AutoMapper;
using WebApi.Entities;
using WebApi.Models.Accounts;

public class AutoMapperProfile : Profile
{
    // mappings between model and entity objects
    public AutoMapperProfile()
    {
        CreateMap<Account, AccountResponse>();

        CreateMap<Account, AuthenticateResponse>();

        CreateMap<RegisterRequest, Account>();

        CreateMap<CreateRequest, Account>();

        CreateMap<UpdateRequest, Account>()
            .ForAllMembers(x => x.Condition(
                (src, dest, prop) =>
                {
                    // ignore null & empty string properties
                    if (prop == null) return false;
                    if (prop.GetType() == typeof(string) && string.IsNullOrEmpty((string)prop)) return false;

                    // ignore null role
                    if (x.DestinationMember.Name == "Role" && src.Role == null) return false;

                    return true;
                }
            ));
    }
}
 

Contexto de datos .NET

Ruta: /Helpers/DataContext.cs

La clase de contexto de datos se usa para acceder a los datos de la aplicación a través de Entity Framework Core y está configurada para conectarse a una base de datos SQLite. Se deriva de la clase DbContext de EF Core y tiene una propiedad Accounts pública para acceder y administrar los datos de la cuenta. Los servicios utilizan el contexto de datos para manejar todas las operaciones de datos de bajo nivel.

Para usar una base de datos diferente (p. ej., SQL Server, MySql, PostgreSQL), agregue el paquete del proveedor de base de datos de NuGet (p. ej., Microsoft.EntityFrameworkCore.SqlServer para SQL Server), actualice OnConfiguring para usar el nuevo proveedor de base de datos, luego elimine las migraciones de base de datos en la carpeta /Migrations y vuelva a generarlas para la nueva base de datos con el comando dotnet ef migrations add InitialCreate .

namespace WebApi.Helpers;

using Microsoft.EntityFrameworkCore;
using WebApi.Entities;

public class DataContext : DbContext
{
    public DbSet<Account> Accounts { get; set; }
    
    private readonly IConfiguration Configuration;

    public DataContext(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    protected override void OnConfiguring(DbContextOptionsBuilder options)
    {
        // connect to sqlite database
        options.UseSqlite(Configuration.GetConnectionString("WebApiDatabase"));
    }
}
 

Middleware del controlador de errores globales .NET

Ruta: /Helpers/ErrorHandlerMiddleware.cs

El controlador de errores global se utiliza para detectar todos los errores y eliminar la necesidad de duplicar el código de manejo de errores en toda la aplicación repetitiva de .NET. Está configurado como middleware en el archivo Program.cs.

Los errores de tipo AppException se tratan como errores personalizados (específicos de la aplicación) que devuelven una respuesta 400 Bad Request, la clase KeyNotFoundException incorporada de .NET se usa para devolver respuestas 404 Not Found, todas las demás excepciones no se manejan y devuelven una respuesta 500 Internal Server Error además de registrarse en la consola.

Consulte el servicio de cuentas para ver ejemplos de errores personalizados y errores no encontrados generados por la API.

namespace WebApi.Helpers;

using System.Net;
using System.Text.Json;

public class ErrorHandlerMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger _logger;

    public ErrorHandlerMiddleware(RequestDelegate next, ILogger<ErrorHandlerMiddleware> logger)
    {
        _next = next;
        _logger = logger;
    }

    public async Task Invoke(HttpContext context)
    {
        try
        {
            await _next(context);
        }
        catch (Exception error)
        {
            var response = context.Response;
            response.ContentType = "application/json";

            switch (error)
            {
                case AppException e:
                    // custom application error
                    response.StatusCode = (int)HttpStatusCode.BadRequest;
                    break;
                case KeyNotFoundException e:
                    // not found error
                    response.StatusCode = (int)HttpStatusCode.NotFound;
                    break;
                default:
                    // unhandled error
                    _logger.LogError(error, error.Message);
                    response.StatusCode = (int)HttpStatusCode.InternalServerError;
                    break;
            }

            var result = JsonSerializer.Serialize(new { message = error?.Message });
            await response.WriteAsync(result);
        }
    }
}
 

Modelo de respuesta de cuenta .NET

Ruta: /Models/Accounts/AccountResponse.cs

El modelo de respuesta de la cuenta define los datos de la cuenta devueltos por los métodos GetAll, GetById, Create y Update del controlador de cuentas y servicio de cuentas. Incluye detalles básicos de la cuenta y excluye datos confidenciales como tokens y contraseñas cifradas.

namespace WebApi.Models.Accounts;

public class AccountResponse
{
    public int Id { get; set; }
    public string Title { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string Email { get; set; }
    public string Role { get; set; }
    public DateTime Created { get; set; }
    public DateTime? Updated { get; set; }
    public bool IsVerified { get; set; }
}
 

Modelo de solicitud de autenticación de .NET

Ruta: /Models/Accounts/AuthenticateRequest.cs

El modelo de solicitud de autenticación define los parámetros para las solicitudes POST entrantes a la ruta /accounts/authenticate, se adjunta a la ruta configurándola como parámetro para Authenticate método de acción del controlador de cuentas. Cuando la ruta recibe una solicitud HTTP POST, los datos del cuerpo se vinculan a una instancia de la clase AuthenticateRequest, se validan y pasan al método.

Las anotaciones de datos .NET se utilizan para manejar automáticamente la validación del modelo, el atributo [Required] establece tanto el correo electrónico como la contraseña como campos obligatorios, por lo que si falta alguno, la API devuelve un mensaje de error de validación. Del mismo modo, el atributo [EmailAddress] valida que la propiedad de correo electrónico contiene una dirección de correo electrónico válida.

namespace WebApi.Models.Accounts;

using System.ComponentModel.DataAnnotations;

public class AuthenticateRequest
{
    [Required]
    [EmailAddress]
    public string Email { get; set; }

    [Required]
    public string Password { get; set; }
}
 

Modelo de respuesta de autenticación de .NET

Ruta: /Models/Accounts/AuthenticateResponse.cs

El modelo de respuesta de autenticación define los datos devueltos por los métodos Authenticate y RefreshToken del controlador de cuentas y servicio de cuenta. Incluye detalles básicos de la cuenta, un token jwt y un token de actualización.

La propiedad del token de actualización está decorada con el atributo [JsonIgnore] que evita que la propiedad se devuelva en el cuerpo de respuesta de la API. Esto se debe a que el token de actualización se devuelve como una cookie HTTP Only en lugar de en el cuerpo.

namespace WebApi.Models.Accounts;

using System.Text.Json.Serialization;

public class AuthenticateResponse
{
    public int Id { get; set; }
    public string Title { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string Email { get; set; }
    public string Role { get; set; }
    public DateTime Created { get; set; }
    public DateTime? Updated { get; set; }
    public bool IsVerified { get; set; }
    public string JwtToken { get; set; }

    [JsonIgnore] // refresh token is returned in http only cookie
    public string RefreshToken { get; set; }
}
 

Modelo de solicitud de creación de .NET

Ruta: /Models/Accounts/CreateRequest.cs

El modelo de solicitud de creación define los parámetros para las solicitudes POST entrantes a la ruta /accounts, se adjunta a la ruta configurándola como el parámetro de la acción Create método del controlador de cuentas. Cuando la ruta recibe una solicitud HTTP POST, los datos del cuerpo se vinculan a una instancia de la clase CreateRequest, se validan y se pasan al método.

Las anotaciones de datos .NET se utilizan para manejar automáticamente la validación del modelo, [Required] hace que todas las propiedades sean obligatorias, [EmailAddress] valida que la propiedad de correo electrónico contenga una dirección de correo electrónico válida , [EnumDataType(typeof(Role))] valida que la propiedad del rol coincida con uno de los roles de la API (Admin o User), [MinLength(6)] valida que la contraseña contenga al menos seis caracteres, y [Compare("Password")] valida que la propiedad de confirmación de contraseña coincida con la propiedad de contraseña.

namespace WebApi.Models.Accounts;

using System.ComponentModel.DataAnnotations;
using WebApi.Entities;

public class CreateRequest
{
    [Required]
    public string Title { get; set; }

    [Required]
    public string FirstName { get; set; }

    [Required]
    public string LastName { get; set; }

    [Required]
    [EnumDataType(typeof(Role))]
    public string Role { get; set; }

    [Required]
    [EmailAddress]
    public string Email { get; set; }

    [Required]
    [MinLength(6)]
    public string Password { get; set; }

    [Required]
    [Compare("Password")]
    public string ConfirmPassword { get; set; }
}
 

Modelo de solicitud de contraseña olvidada de .NET

Ruta: /Models/Accounts/ForgotPasswordRequest.cs

El modelo de solicitud de contraseña olvidada define los parámetros para las solicitudes POST entrantes a la ruta /accounts/forgot-password de la API repetitiva, se adjunta a la ruta configurándola como el parámetro de la Método de acción ForgotPassword del controlador de cuentas. Cuando la ruta recibe una solicitud HTTP POST, los datos del cuerpo se vinculan a una instancia de la clase ForgotPasswordRequest, se validan y pasan al método.

Las anotaciones de datos .NET se utilizan para manejar automáticamente la validación del modelo, [Required] hace que el correo electrónico sea obligatorio y [EmailAddress] valida que contiene una dirección de correo electrónico válida.

namespace WebApi.Models.Accounts;

using System.ComponentModel.DataAnnotations;

public class ForgotPasswordRequest
{
    [Required]
    [EmailAddress]
    public string Email { get; set; }
}
 

Modelo de solicitud de registro .NET

Ruta: /Models/Accounts/RegisterRequest.cs

El modelo de solicitud de registro define los parámetros para las solicitudes POST entrantes a la ruta /accounts/register, se adjunta a la ruta configurándola como el parámetro para Register método de acción del controlador de cuentas. Cuando la ruta recibe una solicitud HTTP POST, los datos del cuerpo se vinculan a una instancia de la clase RegisterRequest, se validan y se pasan al método.

Las anotaciones de datos .NET se utilizan para manejar automáticamente la validación del modelo, [Required] hace que todas las propiedades sean obligatorias, [EmailAddress] valida que la propiedad de correo electrónico contenga una dirección de correo electrónico válida , [MinLength(6)] valida que la contraseña contenga al menos seis caracteres, [Compare("Password")] valida que la propiedad de confirmación de contraseña coincida con la propiedad de contraseña y [Range(typeof(bool), "true", "true")] valida que la propiedad accept terms contiene true.

namespace WebApi.Models.Accounts;

using System.ComponentModel.DataAnnotations;

public class RegisterRequest
{
    [Required]
    public string Title { get; set; }

    [Required]
    public string FirstName { get; set; }

    [Required]
    public string LastName { get; set; }

    [Required]
    [EmailAddress]
    public string Email { get; set; }

    [Required]
    [MinLength(6)]
    public string Password { get; set; }

    [Required]
    [Compare("Password")]
    public string ConfirmPassword { get; set; }

    [Range(typeof(bool), "true", "true")]
    public bool AcceptTerms { get; set; }
}
 

Modelo de solicitud de restablecimiento de contraseña de .NET

Ruta: /Models/Accounts/ResetPasswordRequest.cs

El modelo de solicitud de restablecimiento de contraseña define los parámetros para las solicitudes POST entrantes a la ruta /accounts/reset-password, se adjunta a la ruta configurándola como el parámetro para ResetPassword método de acción del controlador de cuentas. Cuando la ruta recibe una solicitud HTTP POST, los datos del cuerpo se vinculan a una instancia de la clase ResetPassword, se validan y pasan al método.

Las anotaciones de datos .NET se utilizan para manejar automáticamente la validación del modelo, [Required] hace que todas las propiedades sean obligatorias, [MinLength(6)] valida que la contraseña contenga al menos seis caracteres, y [Compare("Password")] valida que la propiedad de confirmación de contraseña coincida con la propiedad de contraseña.

namespace WebApi.Models.Accounts;

using System.ComponentModel.DataAnnotations;

public class ResetPasswordRequest
{
    [Required]
    public string Token { get; set; }

    [Required]
    [MinLength(6)]
    public string Password { get; set; }

    [Required]
    [Compare("Password")]
    public string ConfirmPassword { get; set; }
}
 

Modelo de solicitud de token de revocación de .NET

Ruta: /Models/Accounts/RevokeTokenRequest.cs

El modelo de solicitud de token de revocación define los parámetros para las solicitudes POST entrantes a la ruta /accounts/revoke-token de la API repetitiva, se adjunta a la ruta configurándola como el parámetro para el Método de acción RevokeToken del controlador de cuentas. Cuando la ruta recibe una solicitud HTTP POST, los datos del cuerpo se vinculan a una instancia de la clase RevokeToken, se validan y se pasan al método.

El campo Token es opcional en el cuerpo de la solicitud porque también se puede pasar en la cookie refreshToken; consulte al controlador de cuentas para obtener más detalles.

namespace WebApi.Models.Accounts;

public class RevokeTokenRequest
{
    public string Token { get; set; }
}
 

Modelo de solicitud de actualización de .NET

Ruta: /Models/Accounts/UpdateRequest.cs

El modelo de solicitud de actualización define los parámetros para las solicitudes PUT entrantes a la ruta /accounts/{id:int}, se adjunta a la ruta configurándola como el parámetro de Update método de acción del controlador de cuentas. Cuando la ruta recibe una solicitud HTTP PUT, los datos del cuerpo se vinculan a una instancia de la clase UpdateRequest, se validan y pasan al método.

Las anotaciones de datos .NET se utilizan para manejar automáticamente la validación del modelo, [EnumDataType(typeof(Role))] valida que la propiedad del rol coincida con uno de los roles de la API (Admin o User), [EmailAddress] valida que la propiedad de correo electrónico contiene una dirección de correo electrónico válida, [MinLength(6)] valida que la contraseña contiene al menos seis caracteres, y [Compare("Password")] valida que la propiedad de confirmación de contraseña coincida con la propiedad de contraseña.

Ninguna de las propiedades tiene el atributo [Required], lo que las convierte en opcionales, y los campos omitidos no se actualizan en la base de datos.

Algunos atributos de validación no manejan bien las cadenas vacías, por lo que las propiedades con atributos de validación reemplazan las cadenas vacías con null en set para garantizar que los valores de cadenas vacías sean ignorado.

namespace WebApi.Models.Accounts;

using System.ComponentModel.DataAnnotations;
using WebApi.Entities;

public class UpdateRequest
{
    private string _password;
    private string _confirmPassword;
    private string _role;
    private string _email;
    
    public string Title { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }

    [EnumDataType(typeof(Role))]
    public string Role
    {
        get => _role;
        set => _role = replaceEmptyWithNull(value);
    }

    [EmailAddress]
    public string Email
    {
        get => _email;
        set => _email = replaceEmptyWithNull(value);
    }

    [MinLength(6)]
    public string Password
    {
        get => _password;
        set => _password = replaceEmptyWithNull(value);
    }

    [Compare("Password")]
    public string ConfirmPassword 
    {
        get => _confirmPassword;
        set => _confirmPassword = replaceEmptyWithNull(value);
    }

    // helpers

    private string replaceEmptyWithNull(string value)
    {
        // replace empty string with null to make field optional
        return string.IsNullOrEmpty(value) ? null : value;
    }
}
 

Modelo de solicitud de token de restablecimiento de validación de .NET

Ruta: /Models/Accounts/ValidateResetTokenRequest.cs

El modelo de solicitud de validación de token de reinicio define los parámetros para las solicitudes POST entrantes a la ruta /accounts/validate-reset-token, se adjunta a la ruta configurándola como el parámetro para ValidateResetToken método de acción del controlador de cuentas. Cuando la ruta recibe una solicitud HTTP POST, los datos del cuerpo se vinculan a una instancia de la clase ValidateResetToken, se validan y se pasan al método.

Las anotaciones de datos .NET se utilizan para manejar automáticamente la validación del modelo, [Required] hace que el token sea obligatorio.

namespace WebApi.Models.Accounts;

using System.ComponentModel.DataAnnotations;

public class ValidateResetTokenRequest
{
    [Required]
    public string Token { get; set; }
}
 

Modelo de solicitud de verificación de correo electrónico de .NET

Ruta: /Models/Accounts/VerifyEmailRequest.cs

El modelo de solicitud de verificación de correo electrónico define los parámetros para las solicitudes POST entrantes a la ruta /accounts/verify-email de la API repetitiva, se adjunta a la ruta configurándola como el parámetro para el Método de acción VerifyEmail del controlador de cuentas. Cuando la ruta recibe una solicitud HTTP POST, los datos del cuerpo se vinculan a una instancia de la clase VerifyEmail, se validan y pasan al método.

Las anotaciones de datos .NET se utilizan para manejar automáticamente la validación del modelo, [Required] hace que el token sea obligatorio.

namespace WebApi.Models.Accounts;

using System.ComponentModel.DataAnnotations;

public class VerifyEmailRequest
{
    [Required]
    public string Token { get; set; }
}
 

Servicio de cuentas .NET

Ruta: /Services/AccountService.cs

El servicio de cuenta contiene la lógica de negocios central para el registro de cuenta y amp; verificación, autenticación con JWT & actualizar tokens, olvidé la contraseña & restablecer la función de contraseña, así como los métodos CRUD para administrar los datos de la cuenta. El servicio encapsula las interacciones con el contexto de datos de EF Core y expone un conjunto simple de métodos que utiliza el controlador de cuentas.

La parte superior del archivo contiene la interfaz IAccountService que define los métodos públicos para el servicio de cuentas, y debajo de la interfaz se encuentra la clase concreta AccountService que implementa la interfaz.

namespace WebApi.Services;

using AutoMapper;
using BCrypt.Net;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Security.Cryptography;
using System.Text;
using WebApi.Authorization;
using WebApi.Entities;
using WebApi.Helpers;
using WebApi.Models.Accounts;

public interface IAccountService
{
    AuthenticateResponse Authenticate(AuthenticateRequest model, string ipAddress);
    AuthenticateResponse RefreshToken(string token, string ipAddress);
    void RevokeToken(string token, string ipAddress);
    void Register(RegisterRequest model, string origin);
    void VerifyEmail(string token);
    void ForgotPassword(ForgotPasswordRequest model, string origin);
    void ValidateResetToken(ValidateResetTokenRequest model);
    void ResetPassword(ResetPasswordRequest model);
    IEnumerable<AccountResponse> GetAll();
    AccountResponse GetById(int id);
    AccountResponse Create(CreateRequest model);
    AccountResponse Update(int id, UpdateRequest model);
    void Delete(int id);
}

public class AccountService : IAccountService
{
    private readonly DataContext _context;
    private readonly IJwtUtils _jwtUtils;
    private readonly IMapper _mapper;
    private readonly AppSettings _appSettings;
    private readonly IEmailService _emailService;

    public AccountService(
        DataContext context,
        IJwtUtils jwtUtils,
        IMapper mapper,
        IOptions<AppSettings> appSettings,
        IEmailService emailService)
    {
        _context = context;
        _jwtUtils = jwtUtils;
        _mapper = mapper;
        _appSettings = appSettings.Value;
        _emailService = emailService;
    }

    public AuthenticateResponse Authenticate(AuthenticateRequest model, string ipAddress)
    {
        var account = _context.Accounts.SingleOrDefault(x => x.Email == model.Email);

        // validate
        if (account == null || !account.IsVerified || !BCrypt.Verify(model.Password, account.PasswordHash))
            throw new AppException("Email or password is incorrect");

        // authentication successful so generate jwt and refresh tokens
        var jwtToken = _jwtUtils.GenerateJwtToken(account);
        var refreshToken = _jwtUtils.GenerateRefreshToken(ipAddress);
        account.RefreshTokens.Add(refreshToken);

        // remove old refresh tokens from account
        removeOldRefreshTokens(account);

        // save changes to db
        _context.Update(account);
        _context.SaveChanges();

        var response = _mapper.Map<AuthenticateResponse>(account);
        response.JwtToken = jwtToken;
        response.RefreshToken = refreshToken.Token;
        return response;
    }

    public AuthenticateResponse RefreshToken(string token, string ipAddress)
    {
        var account = getAccountByRefreshToken(token);
        var refreshToken = account.RefreshTokens.Single(x => x.Token == token);

        if (refreshToken.IsRevoked)
        {
            // revoke all descendant tokens in case this token has been compromised
            revokeDescendantRefreshTokens(refreshToken, account, ipAddress, $"Attempted reuse of revoked ancestor token: {token}");
            _context.Update(account);
            _context.SaveChanges();
        }

        if (!refreshToken.IsActive)
            throw new AppException("Invalid token");

        // replace old refresh token with a new one (rotate token)
        var newRefreshToken = rotateRefreshToken(refreshToken, ipAddress);
        account.RefreshTokens.Add(newRefreshToken);


        // remove old refresh tokens from account
        removeOldRefreshTokens(account);

        // save changes to db
        _context.Update(account);
        _context.SaveChanges();

        // generate new jwt
        var jwtToken = _jwtUtils.GenerateJwtToken(account);

        // return data in authenticate response object
        var response = _mapper.Map<AuthenticateResponse>(account);
        response.JwtToken = jwtToken;
        response.RefreshToken = newRefreshToken.Token;
        return response;
    }

    public void RevokeToken(string token, string ipAddress)
    {
        var account = getAccountByRefreshToken(token);
        var refreshToken = account.RefreshTokens.Single(x => x.Token == token);

        if (!refreshToken.IsActive)
            throw new AppException("Invalid token");

        // revoke token and save
        revokeRefreshToken(refreshToken, ipAddress, "Revoked without replacement");
        _context.Update(account);
        _context.SaveChanges();
    }

    public void Register(RegisterRequest model, string origin)
    {
        // validate
        if (_context.Accounts.Any(x => x.Email == model.Email))
        {
            // send already registered error in email to prevent account enumeration
            sendAlreadyRegisteredEmail(model.Email, origin);
            return;
        }

        // map model to new account object
        var account = _mapper.Map<Account>(model);

        // first registered account is an admin
        var isFirstAccount = _context.Accounts.Count() == 0;
        account.Role = isFirstAccount ? Role.Admin : Role.User;
        account.Created = DateTime.UtcNow;
        account.VerificationToken = generateVerificationToken();

        // hash password
        account.PasswordHash = BCrypt.HashPassword(model.Password);

        // save account
        _context.Accounts.Add(account);
        _context.SaveChanges();

        // send email
        sendVerificationEmail(account, origin);
    }

    public void VerifyEmail(string token)
    {
        var account = _context.Accounts.SingleOrDefault(x => x.VerificationToken == token);

        if (account == null) 
            throw new AppException("Verification failed");

        account.Verified = DateTime.UtcNow;
        account.VerificationToken = null;

        _context.Accounts.Update(account);
        _context.SaveChanges();
    }

    public void ForgotPassword(ForgotPasswordRequest model, string origin)
    {
        var account = _context.Accounts.SingleOrDefault(x => x.Email == model.Email);

        // always return ok response to prevent email enumeration
        if (account == null) return;

        // create reset token that expires after 1 day
        account.ResetToken = generateResetToken();
        account.ResetTokenExpires = DateTime.UtcNow.AddDays(1);

        _context.Accounts.Update(account);
        _context.SaveChanges();

        // send email
        sendPasswordResetEmail(account, origin);
    }

    public void ValidateResetToken(ValidateResetTokenRequest model)
    {
        getAccountByResetToken(model.Token);
    }

    public void ResetPassword(ResetPasswordRequest model)
    {
        var account = getAccountByResetToken(model.Token);

        // update password and remove reset token
        account.PasswordHash = BCrypt.HashPassword(model.Password);
        account.PasswordReset = DateTime.UtcNow;
        account.ResetToken = null;
        account.ResetTokenExpires = null;

        _context.Accounts.Update(account);
        _context.SaveChanges();
    }

    public IEnumerable<AccountResponse> GetAll()
    {
        var accounts = _context.Accounts;
        return _mapper.Map<IList<AccountResponse>>(accounts);
    }

    public AccountResponse GetById(int id)
    {
        var account = getAccount(id);
        return _mapper.Map<AccountResponse>(account);
    }

    public AccountResponse Create(CreateRequest model)
    {
        // validate
        if (_context.Accounts.Any(x => x.Email == model.Email))
            throw new AppException($"Email '{model.Email}' is already registered");

        // map model to new account object
        var account = _mapper.Map<Account>(model);
        account.Created = DateTime.UtcNow;
        account.Verified = DateTime.UtcNow;

        // hash password
        account.PasswordHash = BCrypt.HashPassword(model.Password);

        // save account
        _context.Accounts.Add(account);
        _context.SaveChanges();

        return _mapper.Map<AccountResponse>(account);
    }

    public AccountResponse Update(int id, UpdateRequest model)
    {
        var account = getAccount(id);

        // validate
        if (account.Email != model.Email && _context.Accounts.Any(x => x.Email == model.Email))
            throw new AppException($"Email '{model.Email}' is already registered");

        // hash password if it was entered
        if (!string.IsNullOrEmpty(model.Password))
            account.PasswordHash = BCrypt.HashPassword(model.Password);

        // copy model to account and save
        _mapper.Map(model, account);
        account.Updated = DateTime.UtcNow;
        _context.Accounts.Update(account);
        _context.SaveChanges();

        return _mapper.Map<AccountResponse>(account);
    }

    public void Delete(int id)
    {
        var account = getAccount(id);
        _context.Accounts.Remove(account);
        _context.SaveChanges();
    }

    // helper methods

    private Account getAccount(int id)
    {
        var account = _context.Accounts.Find(id);
        if (account == null) throw new KeyNotFoundException("Account not found");
        return account;
    }

    private Account getAccountByRefreshToken(string token)
    {
        var account = _context.Accounts.SingleOrDefault(u => u.RefreshTokens.Any(t => t.Token == token));
        if (account == null) throw new AppException("Invalid token");
        return account;
    }

    private Account getAccountByResetToken(string token)
    {
        var account = _context.Accounts.SingleOrDefault(x =>
            x.ResetToken == token && x.ResetTokenExpires > DateTime.UtcNow);
        if (account == null) throw new AppException("Invalid token");
        return account;
    }

    private string generateJwtToken(Account account)
    {
        var tokenHandler = new JwtSecurityTokenHandler();
        var key = Encoding.ASCII.GetBytes(_appSettings.Secret);
        var tokenDescriptor = new SecurityTokenDescriptor
        {
            Subject = new ClaimsIdentity(new[] { new Claim("id", account.Id.ToString()) }),
            Expires = DateTime.UtcNow.AddMinutes(15),
            SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature)
        };
        var token = tokenHandler.CreateToken(tokenDescriptor);
        return tokenHandler.WriteToken(token);
    }

    private string generateResetToken()
    {
        // token is a cryptographically strong random sequence of values
        var token = Convert.ToHexString(RandomNumberGenerator.GetBytes(64));

        // ensure token is unique by checking against db
        var tokenIsUnique = !_context.Accounts.Any(x => x.ResetToken == token);
        if (!tokenIsUnique)
            return generateResetToken();
        
        return token;
    }

    private string generateVerificationToken()
    {
        // token is a cryptographically strong random sequence of values
        var token = Convert.ToHexString(RandomNumberGenerator.GetBytes(64));

        // ensure token is unique by checking against db
        var tokenIsUnique = !_context.Accounts.Any(x => x.VerificationToken == token);
        if (!tokenIsUnique)
            return generateVerificationToken();
        
        return token;
    }

    private RefreshToken rotateRefreshToken(RefreshToken refreshToken, string ipAddress)
    {
        var newRefreshToken = _jwtUtils.GenerateRefreshToken(ipAddress);
        revokeRefreshToken(refreshToken, ipAddress, "Replaced by new token", newRefreshToken.Token);
        return newRefreshToken;
    }

    private void removeOldRefreshTokens(Account account)
    {
        account.RefreshTokens.RemoveAll(x => 
            !x.IsActive && 
            x.Created.AddDays(_appSettings.RefreshTokenTTL) <= DateTime.UtcNow);
    }

    private void revokeDescendantRefreshTokens(RefreshToken refreshToken, Account account, string ipAddress, string reason)
    {
        // recursively traverse the refresh token chain and ensure all descendants are revoked
        if (!string.IsNullOrEmpty(refreshToken.ReplacedByToken))
        {
            var childToken = account.RefreshTokens.SingleOrDefault(x => x.Token == refreshToken.ReplacedByToken);
            if (childToken.IsActive)
                revokeRefreshToken(childToken, ipAddress, reason);
            else
                revokeDescendantRefreshTokens(childToken, account, ipAddress, reason);
        }
    }

    private void revokeRefreshToken(RefreshToken token, string ipAddress, string reason = null, string replacedByToken = null)
    {
        token.Revoked = DateTime.UtcNow;
        token.RevokedByIp = ipAddress;
        token.ReasonRevoked = reason;
        token.ReplacedByToken = replacedByToken;
    }

    private void sendVerificationEmail(Account account, string origin)
    {
        string message;
        if (!string.IsNullOrEmpty(origin))
        {
            // origin exists if request sent from browser single page app (e.g. Angular or React)
            // so send link to verify via single page app
            var verifyUrl = $"{origin}/account/verify-email?token={account.VerificationToken}";
            message = [email protected]"<p>Please click the below link to verify your email address:</p>
                            <p><a href=""{verifyUrl}"">{verifyUrl}</a></p>";
        }
        else
        {
            // origin missing if request sent directly to api (e.g. from Postman)
            // so send instructions to verify directly with api
            message = [email protected]"<p>Please use the below token to verify your email address with the <code>/accounts/verify-email</code> api route:</p>
                            <p><code>{account.VerificationToken}</code></p>";
        }

        _emailService.Send(
            to: account.Email,
            subject: "Sign-up Verification API - Verify Email",
            html: [email protected]"<h4>Verify Email</h4>
                        <p>Thanks for registering!</p>
                        {message}"
        );
    }

    private void sendAlreadyRegisteredEmail(string email, string origin)
    {
        string message;
        if (!string.IsNullOrEmpty(origin))
            message = [email protected]"<p>If you don't know your password please visit the <a href=""{origin}/account/forgot-password"">forgot password</a> page.</p>";
        else
            message = "<p>If you don't know your password you can reset it via the <code>/accounts/forgot-password</code> api route.</p>";

        _emailService.Send(
            to: email,
            subject: "Sign-up Verification API - Email Already Registered",
            html: [email protected]"<h4>Email Already Registered</h4>
                        <p>Your email <strong>{email}</strong> is already registered.</p>
                        {message}"
        );
    }

    private void sendPasswordResetEmail(Account account, string origin)
    {
        string message;
        if (!string.IsNullOrEmpty(origin))
        {
            var resetUrl = $"{origin}/account/reset-password?token={account.ResetToken}";
            message = [email protected]"<p>Please click the below link to reset your password, the link will be valid for 1 day:</p>
                            <p><a href=""{resetUrl}"">{resetUrl}</a></p>";
        }
        else
        {
            message = [email protected]"<p>Please use the below token to reset your password with the <code>/accounts/reset-password</code> api route:</p>
                            <p><code>{account.ResetToken}</code></p>";
        }

        _emailService.Send(
            to: account.Email,
            subject: "Sign-up Verification API - Reset Password",
            html: [email protected]"<h4>Reset Password Email</h4>
                        {message}"
        );
    }
}
 

Servicio de correo electrónico .NET

Ruta: /Services/EmailService.cs

El servicio de correo electrónico es un contenedor ligero alrededor de la biblioteca de cliente de correo .NET MailKit para simplificar el envío de correos electrónicos desde cualquier lugar en la API repetitiva de .NET 6. Lo utiliza el servicio de cuentas para enviar correos electrónicos de verificación de cuenta y restablecimiento de contraseña.

Para obtener más información sobre MailKit, consulte https://github.com/jstedfast/MailKit.

namespace WebApi.Services;

using MailKit.Net.Smtp;
using MailKit.Security;
using Microsoft.Extensions.Options;
using MimeKit;
using MimeKit.Text;
using WebApi.Helpers;

public interface IEmailService
{
    void Send(string to, string subject, string html, string from = null);
}

public class EmailService : IEmailService
{
    private readonly AppSettings _appSettings;

    public EmailService(IOptions<AppSettings> appSettings)
    {
        _appSettings = appSettings.Value;
    }

    public void Send(string to, string subject, string html, string from = null)
    {
        // create message
        var email = new MimeMessage();
        email.From.Add(MailboxAddress.Parse(from ?? _appSettings.EmailFrom));
        email.To.Add(MailboxAddress.Parse(to));
        email.Subject = subject;
        email.Body = new TextPart(TextFormat.Html) { Text = html };

        // send email
        using var smtp = new SmtpClient();
        smtp.Connect(_appSettings.SmtpHost, _appSettings.SmtpPort, SecureSocketOptions.StartTls);
        smtp.Authenticate(_appSettings.SmtpUser, _appSettings.SmtpPass);
        smtp.Send(email);
        smtp.Disconnect(true);
    }
}
 

Configuración de la aplicación .NET 6 JSON

Ruta: /appsettings.json

El archivo appsettings.json es el archivo de configuración base en una aplicación .NET que contiene configuraciones para todos los entornos (p. ej., Development, Production). Puede anular valores para diferentes entornos mediante la creación de archivos appsettings específicos del entorno (p.ej. appsettings.Development.json, appsettings.Production.json).

Incluye la cadena de conexión WebApiDatabase a la base de datos SQLite, el Secret utilizado para firmar y verificar tokens JWT, el tiempo de vida del token de actualización (RefreshTokenTTL) que establece la cantidad de días para mantener inactivos los tokens de actualización en la base de datos, la dirección EmailFrom utilizada para enviar correos electrónicos y las opciones Smtp* utilizadas para conectarse y autenticarse con un servidor de correo electrónico.

Configure los ajustes de SMTP para el correo electrónico con las propiedades Smtp*. Para una prueba rápida, puede crear una bandeja de entrada temporal en https://ethereal.email/ y copiar el Opciones de configuración de SMTP.

IMPORTANTE: La propiedad "Secret" se usa para firmar y verificar tokens JWT para la autenticación, cámbielo a una cadena aleatoria para asegurarse de que nadie más pueda generar un JWT con el mismo secreto y obtener acceso no autorizado acceso a su API. Una forma rápida y fácil es unir un par de GUID para crear una cadena aleatoria larga (por ejemplo, desde https://www.guidgenerator.com/).

{
    "AppSettings": {
        "Secret": "THIS IS USED TO SIGN AND VERIFY JWT TOKENS, REPLACE IT WITH YOUR OWN SECRET, IT CAN BE ANY STRING",
        "RefreshTokenTTL": 2,
        "EmailFrom": "[email protected]",
        "SmtpHost": "[ENTER YOUR OWN SMTP OPTIONS OR CREATE FREE TEST ACCOUNT IN ONE CLICK AT https://ethereal.email/]",
        "SmtpPort": 587,
        "SmtpUser": "",
        "SmtpPass": ""
    },
    "ConnectionStrings": {
        "WebApiDatabase": "Data Source=WebApiDatabase.db"
    },
    "Logging": {
        "LogLevel": {
            "Default": "Information",
            "Microsoft.AspNetCore": "Warning"
        }
    }
}
 

Programa .NET 6

Ruta: /Program.cs

El archivo de programa .NET 6 contiene top-level statements que el nuevo compilador de C# 10 convierte en un método Main() y Program clase para el programa .NET. El método Main() es el punto de entrada para una aplicación .NET, cuando se inicia una aplicación busca el método Main() para comenzar la ejecución. Las declaraciones de nivel superior se pueden ubicar en cualquier parte del proyecto, pero generalmente se colocan en el archivo Program.cs, solo un archivo puede contener declaraciones de nivel superior dentro de una aplicación .NET.

La clase WebApplication maneja el inicio de la aplicación, la administración de por vida, la configuración del servidor web y más. Primero se crea un WebApplicationBuilder llamando al método estático WebApplication.CreateBuilder(args), el constructor se usa para configurar servicios para inyección de dependencia (DI), se crea una instancia de WebApplication llamando a builder.Build(), la instancia de la aplicación se usa para configurar la tubería de solicitud HTTP (middleware), luego la aplicación se inicia llamando a app.Run().

Envolví las secciones add services... y configure HTTP... entre llaves {} para agruparlas visualmente, los corchetes son completamente opcionales.

Internamente, la clase WebApplicationBuilder llama al método de extensión ConfigureWebHostDefaults() que configura el alojamiento para la aplicación web, incluida la configuración de Kestrel como servidor web, la adición de middleware de filtrado de host y la habilitación de IIS integración. Para obtener más información sobre la configuración predeterminada del generador, consulte https://docs.microsoft.com/aspnet/core/fundamentals/host/generic-host#default-builder-settings.

using Microsoft.EntityFrameworkCore;
using System.Text.Json.Serialization;
using WebApi.Authorization;
using WebApi.Helpers;
using WebApi.Services;

var builder = WebApplication.CreateBuilder(args);

// add services to DI container
{
    var services = builder.Services;
    var env = builder.Environment;
 
    services.AddDbContext<DataContext>();
    services.AddCors();
    services.AddControllers().AddJsonOptions(x => 
    {
        // serialize enums as strings in api responses (e.g. Role)
        x.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter());
    });
    services.AddAutoMapper(AppDomain.CurrentDomain.GetAssemblies());
    services.AddSwaggerGen();

    // configure strongly typed settings object
    services.Configure<AppSettings>(builder.Configuration.GetSection("AppSettings"));

    // configure DI for application services
    services.AddScoped<IJwtUtils, JwtUtils>();
    services.AddScoped<IAccountService, AccountService>();
    services.AddScoped<IEmailService, EmailService>();
}

var app = builder.Build();

// migrate any database changes on startup (includes initial db creation)
using (var scope = app.Services.CreateScope())
{
    var dataContext = scope.ServiceProvider.GetRequiredService<DataContext>();    
    dataContext.Database.Migrate();
}

// configure HTTP request pipeline
{
    // generated swagger json and swagger ui middleware
    app.UseSwagger();
    app.UseSwaggerUI(x => x.SwaggerEndpoint("/swagger/v1/swagger.json", ".NET Sign-up and Verification API"));

    // global cors policy
    app.UseCors(x => x
        .SetIsOriginAllowed(origin => true)
        .AllowAnyMethod()
        .AllowAnyHeader()
        .AllowCredentials());

    // global error handler
    app.UseMiddleware<ErrorHandlerMiddleware>();

    // custom jwt auth middleware
    app.UseMiddleware<JwtMiddleware>();

    app.MapControllers();
}

app.Run("http://localhost:4000");
 

Archivo de proyecto .NET 6 Web Api

Ruta: /WebApi.csproj

El csproj (proyecto C#) es un archivo basado en MSBuild que contiene el marco de destino y la información de dependencia del paquete NuGet para la aplicación. La función ImplicitUsings está habilitada, lo que le dice al compilador que genere automáticamente un conjunto de directivas de uso globales basadas en el tipo de proyecto, eliminando la necesidad de incluir muchas declaraciones de uso comunes en cada archivo de clase. Las instrucciones de uso global se generan automáticamente cuando crea el proyecto y se pueden encontrar en el archivo /obj/Debug/net6.0/WebApi.GlobalUsings.g.cs.

Para obtener más información sobre el archivo de proyecto de C#, consulte .NET + MSBuild - C# Project File (.csproj) in a Nutshell.

<Project Sdk="Microsoft.NET.Sdk.Web">
    <PropertyGroup>
        <TargetFramework>net6.0</TargetFramework>
        <ImplicitUsings>enable</ImplicitUsings>
    </PropertyGroup>
    <ItemGroup>
        <PackageReference Include="AutoMapper" Version="11.0.1" />
        <PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="11.0.0" />
        <PackageReference Include="BCrypt.Net-Next" Version="4.0.2" />
        <PackageReference Include="MailKit" Version="3.1.1" />
        <PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="6.0.2" />
        <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.2">
            <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
            <PrivateAssets>all</PrivateAssets>
        </PackageReference>
        <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.2" />
        <PackageReference Include="Swashbuckle.AspNetCore" Version="6.2.3" />
        <PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="6.16.0" />
    </ItemGroup>
</Project>

 


Suscríbete o Sígueme para actualizaciones

Suscríbete a mi canal de YouTube o sígueme en Twitter, Facebook o GitHub para recibir notificaciones cuando publique contenido nuevo.

Aparte de la codificación...

Actualmente estoy intentando viajar por Australia en motocicleta con mi esposa Tina en un par de Royal Enfield Himalayan. Puedes seguir nuestras aventuras en YouTube, Instagram y Facebook.


¿Necesita Ayuda .NET?

Buscar fiverr para encontrar ayuda rápidamente de desarrolladores .NET experimentados.