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