Bienvenidos al mantenimiento 4.0
5 abril, 2021
Microsoft Ignite 2021: Novedades de Realidad Mixta y Azure Data
12 abril, 2021

En este post os vamos a ejemplificar como montar un sistema de cliente-api (protegido con JWT) y api-api (protegido con un servidor de autenticación IdentityServer)

Al lio!

Para empezar creamos un proyecto de tipo api que usaremos como api principal, otra como api de servicio, una api vacía que será el identity server y las pruebas de consumo de cliente las realizaremos desde postman ya que el cliente podría ser desde una aplicación móvil a una de consola.

Empezaremos añadiendo el nuget de IdentityServer4 al proyecto IdentityServer

Después crearemos un archivo al que llamaremos ConfiguracionIdentity.cs que usaremos para configurar las apis de las que se encargara identityserver.

En éste archivo tendremos un método estático GetApiScopes que es donde pondremos la api que queremos proteger con ClientCredentials.

También tendremos un método GetClients que es donde pondremos el cliente que va a poder atacar a esta api. Si este cliente (que en nuestro caso es una api) tuviese que atacar a distintos Scopes podríamos añadirlos en AllowedScopes.

En este método le damos un Id al cliente, decimos el tipo de credenciales que va a tener, establecemos una contraseña (si os da error es que es demasiado corta) y por último decimos a que scopes tiene acceso, es este caso ApiServicio (tiene que ser el mismo nombre que le hemos dado en GetApiScopes).

Con esto la configuración cliente – servicio queda hecha.

 

Ahora hay que configurar IdentityServer en el startup, si bien funciona, NO es la configuración ideal para producción. También decir que la maquina tiene que confiar en el certificado de desarrollo que le estamos dando (en nuestro caso, el que genera net core al arrancar una aplicación), esto es algo que programando en Linux me ha dado algún quebradero de cabeza ya que Linux no acepta el certificado de .net y hay que configurar kestrel para que use uno que nosotros hayamos aceptado manualmente.

Si estas en Windows o Mac con este comando de terminal debería solucionar cualquier problema de certificados a la hora del desarrollo: dotnet dev-certs https –trust desde una terminal en la raíz del proyecto.

 

En Startup ConfigureServices añadimos el servicio de identityServer y le decimos que utilice los Scopes y clientes que tenemos en nuestra clase de configuración.

En Configure le decimos a la aplicación que use IdentityServer

Hecho esto, si lanzamos el proyecto y nos dirigimos a esta url https://<localhost:puerto>/.well-known/openid-configuration nos devolverá un json donde en scopes_supported estará el scope que hemos configurado.

 

Pasamos a la api de Servicio.

Aquí añadiremos el nuget IdentityServer4.AccessTokenValidation y pasaremos a configurarlo en el startup, también crearemos un controlador que nos devolverá un texto para comprobar que todo funciona.

En el startup pondremos lo siguiente en el método ConfigureServices:

Con esto establecemos nuestro servidor de autoridad (el que hemos creado antes) le pasamos el esquema Bearer y le decimos que no valide la audiencia.

Además en Configure le diremos a la aplicación que use autenticación:

Ahora crearemos un controlador de prueba (recuerda que el controlador sea de tipo API y no de tipo MVC, a veces pasa)

Añade [Authorize] en las decoraciones de la clase para que todos los métodos tengan que ser comprobados, si quieres alguno que este abierto usa [AllowAnonimous] sobre él.

Y con esto, la api de servicio ya estaría.

Nota importante, como vamos a correrlo todo en nuestro local tendremos que cambiar los puertos donde se despliegan las apps, esto lo podemos hacer desde el archivo launchSettings que hay en Properties en cada proyecto.

También tenemos que decirle a Visual Studio que lance las aplicaciones desde consola (si usas el CLI de .Cet Core esto no te hace falta)

Básicamente, no selecciones IIS Express.

 

Yo voy a dejarlos de la siguiente manera:

App                        http        https

IdentityServer   5000      5001

ApiPrincipal       5002      5003

ApiServicio         5004      5005

 

Para lanzar las aplicaciones, click secundario en el proyecto y depurar:

Vamos a usar postman para hacer una petición a localhost:5005/api/authorized/test

 

Hacemos una nueva entrada para una petición Get y nos vamos a Authorization, ahí seleccionamos OAuth 2.0

Después configuramos el token que vamos a usar en Configure New Token (si no lo ves, es porque esta en una scrollbar y tienes que bajar)

Le damos a Get New Access Token y si todo va bien nos saldrá esta ventana:

Le damos a Use Token y nos lo pondrá como token seleccionado para hacer la petición

Ahora ya podemos lanzar la petición.

Y si todo va bien recibiremos nuestra respuesta:

Nota, Visual me estaba lanzando el proyecto ApiServicio en el puerto que configura IIS, para solucionarlo tenemos que cambiar la ejecución en la flecha verde de arriba, seleccionamos el proyecto ApiServicio y cambiamos la ejecución:

Haz esto también para ApiPrincipal

 

Bien, ahora que ya tenemos configurada la seguridad de la apiCliente pasemos a consumirla desde la ApiPrincipal.

Añadimos un nuevo controlador de tipo api y creamos una clase que llamaremos <nombreDelControlador>GetCredentials.cs

Con GetCredentials lo que vamos a hacer es un singleton que obtenga el token para llamar a la api de servicio y que lo almacene en una variable, de esta forma no tendremos que llamar a IdentityServer cada ver que tengamos que consumir la api de servicio.

Esta clase contará con cuatro métodos, GetInstance() que nos da la instancia del singleton, GetToken() que sirve para traerse el token de acceso desde IdentityServer y almacenarlo en la variable Token además de guardar el momento en el que va a expirar dicho token, CheckExpirationToken() que mira que la fecha y hora actuales sea inferior a las almacenadas por GetToken o que en caso de que la fecha de expiración almacenada sea null llama a GetToken y GetTokenValue() que retorna el valor de dicha variable.

 

Ahora en el controlador que hemos creado añadiremos un método Get para traernos la información de la ApiCliente

En el primer bloque instanciamos el singleton y comprobamos que estamos dentro de la fecha de expiración del token, si no tenemos token o si el token ha expirado pediremos uno nuevo y creamos una variable token a la que damos el valor que tenemos en el singleton.

Después creamos el cliente Http y hacemos la petición pasando el token que nos hemos traído.

Por ultimo devolvemos un Ok con el valor de la variable toReturn;

 

Hecho esto, ya podemos lanzar las tres aplicaciones: IdentityServer, ApiPrincipal y ApiServicio y comprobar que todo funciona haciendo una petición Get a api/<nombreDelControlador>/getFromOtherApi.

 

Si todo va bien obtendremos algo como esto:

 

 

Bien, hagamos un repaso.

Hasta el momento hemos creado un Servidor de autenticación que usa IdentityServer donde hemos configurado el acceso a una api de servicio desde una api principal, hemos configurado la api de servicio para que use autenticación de cliente con identityServer y hemos consumido dicha api desde la api principal.

Esto es algo que para un entorno de producción final no seria lo mas apropiado ya que estamos metiendo los Scopes y Clientes a capón en el código y si por lo que sea tuviésemos que meter un cliente nuevo, tendríamos que ir al código de IdentityServer, ponerlo y volver a publicar IdentityServer. Como digo, no es lo mas apropiado pero al tratarse de autenticación de api contra api, podría llegar a hacerse ya que no es algo que cambie con mucha frecuencia.

Sin embargo esto es algo que no nos podemos permitir con los usuarios ya que en el momento que un usuario cambiase su contraseña, habría que ponerlo en el código de Identity, es mas el usuario no podría cambiar su contraseña si no que tendriamos que hacerlo nosotros.

Por lo tanto la autenticación de usuarios que vamos a usar no va a ser a través de identity server, usaremos autenticación con JWT, esto nos permitira en un momento dado poner los usuarios en base de datos y realizar consultas ahí sin mayores complicaciones, aunque en este ejemplo lo dejaremos todo en local.

 

Empezaremos por crear la clave secret para montar los tokens en el archivo appConfig del proyecto de ApiPrincipal.

Tras esto, iremos a AppSettings para configurar el servicio que se encargará de realizar la autenticación.

Básicamente  lo  que  hacemos  aquí  es  coger  la  clave  que  hemos  puesto  en  AppSetings  y  almacenarla  en  la  variable  key.

Después  añadimos  la  autenticación  a  los  servicios  y  configuramos  la  autenticación,  vamos  a  utilizar  tokes  bearer  que  son  de  lo  más  común  que  hay.  Si  llevas  esto  a  producción  ten  en  cuenta  que  no  estamos  configurando  ni  Issuer ni  Audience,  solo  estamos  validando  mediante  la  clave  secreta.  Por ejemplo, si  estas  en  un  escenario  en  que  esta  api  solo  vaya  a  ser  consumida  desde  un   servicio  con  un  dominio  podrías  poner  ese  servicio  en  la  audiencia.

 

 

Ahora  crearemos  los  modelos  que  usaremos  para  hacer  login,  uno  será  un  dto  que  contine  usuario  y  contraseña,  otro  será  el  modelo  de  usuario  que  almacenaremos  en  base  de  datos  y  otro  que  será  la  respuesta  del  usuario  y  el  token  que  usaremos  para  comprobar  que  funciona  bien.

En  este  ejemplo  no  voy  a  guardar  nada  en  base  de  datos  pero  la  diferencia  es  que  mientras  que  aquí  haremos  un  where  a  una  lista  de  usuarios  si  lo  llevamos  a  base  de  datos  habrá  que  hacer  una  consulta  y  asegurarse  de  que  las  contraseñas  se  guardan  por  lo  menos  encriptadas.

 

Crearemos  3  modelos

User

 

LoginModel

Y LoginResponse

Después crearemos la clase LoginService y la interfaz ILoginService

LoginService hereda de ILoginService y la declararemos en el startup para poder inyectarla.

En este archivo tendremos el método para autenticar un usuario  y para generar su token.

Falsearemos una base de datos usando una lista que será donde haremos las comparaciones para validar un login como correcto.

Si encontramos el usuario y contraseña en la lista el siguiente paso será crear un token con dicho usuario, para ello usaremos el método GenerateJwtToken al que pasaremos el usuario que acabamos de recuperar de nuestra “base de datos” y con el token resultante creamos un LoginResponse que es lo que le devolvemos al controller.

 

Y hablando de Controller, habrá que hacerlo también, pero antes declaremos el servicio en el startup para poder inyectarlo, que luego pasa lo que pasa. (Si tratas de hacer la llamada y revienta pero todo parece estar correcto, revisa siempre que hayas declarado lo que quiera que vayas a inyectar en el startup, es un error tonto pero ocurre mas de lo que me gustaría admitir).

La inyección se puede hacer de 3 formas: Scoped, Transient y Singleton, si se tercia algún día explicaré las diferencias, ante la duda, Scoped.

 

 

Ahora si, pasemos a hacer el controller.

Si estás en visual studio recuerda que pedes hacer scaffold de un api controller a base de clicks, si no, recuerda que un controlador de api hereda de ControllerBase y la clase lleva las decoraciones:

[Route(puedes/poner/algo/o/no/[controller])] [ApiController]

 

Otro archivo bastante sencillito, inyecctamos un ILoginService en el contructor y luego hacemos un método tipo HttpPost que recibe en el body un loginModel que mandaremos al metodo Authenticate del ILoginService.

Comprobamos el resultado y devolvemos la respuesta.

MUY IMPORTANTE.

En caso de que nos devuelva null hay que devolver un mensaje de error lo mas genérico posible tratandose de un login por lo tanto NUNCA digas si ha fallado el usuario o la contraseña sino que ha fallado la autentificación, usuario o contraseña incorrectos o cosas así, ya que si por ejemplo dices contraseña incorrecta estas diciendo que el correo o nombre de usuario introducidos existe en tu base de datos y eso compromete la seguridad de la plataforma y la privacidad del individuo.

 

Por último, para proteger un controlador con autorizacion por token solo tienes que añadir [Authorize] como decoración. En este ejemplo ponemos Authorize y además especificamos el esquema de autorizacion [Authorize(AuthenticationSchemes = Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerDefaults.AuthenticationScheme)]

Para comprobaar que todo funciona lanzamos las tres aplicaciones a la vez y con postman primero atacamos a api/login/hacerlogin y después con el token atacamos a api/consumo/getfromotherapi

 

Si os dice algo de que los certificados ssl son autofirmados le decís que no verifique esas cosas, en las versiones actuales avisa y lo puedes deshabilitar desde el aviso

Copiamos el valor del token ya que lo vamos a usar en la siguiente llamada

En la pestaña Authorization seleccionamos TYPE => Bearer Token y pegamos el token que acabamos de copiar

Y ahora lanzamos la consulta, si todo va bien y has seguido los pasos uno a uno deberías recibir la respuesta del servidor sin mayor problema

Y con esto ya estaría todo. Recuerda que para un escenario real como mínimo tendrás que meter los usuarios en una base de datos y con sus contraseñas cifradas. No es el sistema más perfecto pero si es un sistema funcional con el que podrás proteger las apis y tener un sistema de usuarios básico (aunque puedes complicarlo todo lo que quieras complicarte).

 

Espero que os haya resultado útil y que os pueda ayudar en vuestros futuros proyectos.

Aquí tenéis el código en github para que podaís bajaroslo y hacer con el lo que queráis.

https://github.com/alejandrosarsanposts/JwtAndIdentityServer

 

 

Compártelo: Share on FacebookTweet about this on TwitterShare on LinkedInPin on Pinterest

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

NEWSLETTER