Introducción
Una de las cosas más sencillas que uno puede hacer en esta vida es comprar pan. Es relativamente fácil; uno se acerca al comercio que toque, coge una barra de pan y paga en caja por la misma. Sencillo, ¿verdad?
Sin embargo, este sencillo acto incluye un elemento que casi siempre pasamos por alto, que no percibimos, pero que es algo en lo que reside buena parte del buen funcionamiento de la interactividad social en general: Tú quieres algo del comercio (el pan) y éste quiere algo de ti a cambio (riqueza) y tienes que intercambiar "algo", un poco de "riqueza" por tu parte, para que el comercio te lo cambie por su producto, el pan. El problema es que el comercio acepte "tu riqueza".
¿Cómo hacer para que el comercio, que te entrega su producto, el pan, acepte a cambio algo, y que sirva para ambas partes? En principio, deberíamos pagar con la unidad de riqueza establecida (antiguamente, el oro; hoy en día, el dólar) y sin embargo, nadie va por la calle con lingotes de oro o dólares en el bolsillo (y de llevar €uros en el bolsillo hablamos otro día). Se necesita algo más, algún elemento o mecanismo, que sustituya a eso de "llevar dólares encima" y que tenga el mismo propósito: validez en un intercambio entre pares.
Este fue el origen del "papel moneda" de curso legal, algo que todos conocemos bien, pues sirve como elemento válido de intercambio en una transacción entre el comercio y el cliente. Sin embargo, el elemento que proporciona dicha validez, en este caso, es un mero trozo de papel...
Si el lector se fija bien, en la imagen anterior se representa un billete de 20 €, de curso legal. Y decimos que "de curso legal" queriendo decir que dicho billete es perfectamente válido por cuanto lo "emite" una entidad de confianza: "El Banco Central Europeo", identificado en el billete por sus siglas, "BCE", la firma de su director (en ese momento), "Mario Draghi" y una serie de elementos con tecnología anti-fraude incorporados en el billete.
Así, como todos "nos fiamos" del BCE, pues no hay problema en intercambiar "papel moneda" a modo de "riqueza" para conseguir productos o servicios de otros.
Y esta es la base de todo esto: La confianza que, tanto el comprador como el vendedor, se tiene entre ellos, fundamentada en un elemento (un billete) proporcionado por un "tercero de confianza"; en este caso, el BCE.
Autenticación
La propia evolución de la tecnología, proceso al cual se ha sumado la pandemia de COVID19 sufrida últimamente, ha hecho que todos hayamos tenido la necesidad de "pasarnos al mundo digital" inexorablemente.
En este mundo digital en el que ya nos movemos, para poder efectuar ese "intercambio de riqueza" con un proveedor nos encontramos con que también es necesario identificarnos. Y ya no valen los mecanismos y protocolos que estaban evolucionando tranquilamente hasta 2020; la pandemia ha acelerado buena parte de estos, potenciando algunas tecnologías emergentes que, hasta ahora, estaban en una tranquila fase de desarrollo evolutivo.
Si bien estas tecnologías son "pre-pandemia", como decimos, la pandemia las ha potenciado. Así, la identificación del usuario "al otro lado" se ha convertido, casi de la noche a la mañana, en algo relativamente crítico y fundamental para muchas de las transacciones vía internet. Y de ahí, ha surgido también la necesidad de implementar prácticamente sin tiempo, soluciones a problemas asociados a estos procesos de identificación, como el "phishig", "password stealing", etc.
Esto no es algo que se haya sacado Google de la manga; surge a raiz de la directiva europea PSD2: La "Directiva Europea de Servicios de Pago". Esta directiva incorpora nuevos niveles de protección para con los clientes de entidades financieras vía internet y es la que ha originado la aparición de mecanismos como los de enviar un código al móvil del usuario para confirmar una operación.
A raíz de esta directiva, y de las posibilidades de verificación de quien está "al otro lado", ha surgido la necesidad de incorporar nuevos protocolos de autorización y autentificación, como decimos, especialmente diseñados para los intercambios vía Internet. Estos protocolos son los protocolos: "OAuth2" y "OpenID"; "quién eres, y qué puedes hacer". Y todo esto, de manera acelerada, propiciado por la COVID19, puesto que los protocolos, como tales, ya existían.
Autorización
Como decimos, la directiva PSD2 dió origen a un uso mucho más explícito de dos nuevas tecnologías de autenticación y autorización de clientes en internet: OpenID (OpenID Connect) y OAuth2. La conjunción de uso de estos dos protocolos sirve para "identificar" correctamente a la persona "al otro lado" y "autorizar" el acceso a funcionalidades específicas.
Y esto es así porque la directiva PSD2, además de requerir la identificación del cliente, también establece la necesidad de identificar qué operativas están disponibles para él y cuáles no. La otra pata importante de esta directiva tiene que ver con la gestión de las "autorizaciones" de acceso a funcionalidades; dicho de otra manera más mundana, es algo así como gestionar aquello tan viejo de: "te dejo pasar, pero 'a ver qué tocas', no sea que"...
IAM
Más adelante nos ocuparemos de los protocolos "OAuth2" y "OpenID"; pero, ahora, hagamos un breve apunte sobre algo colateral: IAM
.
Aunque identificar correctamente al usuario de una aplicación es algo relativamente importante, no es menos también identificar qué cosas puede y no puede hacer en la misma. El conjunto de la identificación ("Authentication") y las funcionalidades permitidas ("Authorization") se conoce como: IAM
.
Las siglas: IAM
significan: Identification and Access Management. Básicamente, IAM
incorpora diversas funcionalidades que permiten garantizar tanto la identidad de la persona que accede a un recurso, así como que tiene la autorización, los permisos adecuados, para poder acceder a dicho recurso. Adicionalmente, puede incorporar otras funcionalidades como la gestión de usuarios, de permisos o de "logging" de operativas.
Así, la suma de: "autenticación" y "autorización" es, en esencia, IAM
.
En internet se encuentran disponibles varias páginas que hablan sobre este particular, así como páginas de herramientas de fabricantes que dan solución a alto nivel a estas necesidades; en concreto, y a modo de introducción a IAM
, un buen punto de inicio puede ser este.
Autenticación e Identificación
Como hemos visto, IAM
resume tanto la "autenticación", la "identificación", del usuario "al otro lado" como la "autorización", la gestión de los "permisos de acceso", a los recursos del proveedor.
Si bien, ambos componentes son relevantes de cara a la operativa para cualquier tipo de aplicación de servicios financieros y no tan financieros, en esta entrada nos vamos a centrar en el primero de ellos: La Identificación.
En el pasado, hubo algún intento de centralizar la identificación de los usuarios; así, a principio de siglo, Microsoft propuso: "MS Password" como alternativa. Aun con buen propósito, no terminó de cuajar en su momento, pero al ser una buena idea, surgieron otras alternativas principalmente, en su momento lideradas tanto por Yahoo! como por el propio Microsoft con "Live".
Con el tiempo, se vio la necesidad de implementar una solución que fuera independiente del fabricante, un estándar abierto, y de ahí nació OAuth. OAuth fue el primer protocolo que permitía que diversos proveedores utilizaran "algo común" para gestionar el acceso de clientes a funcionalidades ("autorización"). Y, lo más importante, "cross"; esto es, daban un principio de solución al problema del "SSO", "Single Sign-On".
Esto significa una gran ventaja a la hora de acceder a múltiples aplicaciones web que requieran de identificación; por la experiencia de estos años, lo normal, es que cada aplicación requiera de nosotros que nos creemos una cuenta, un perfil, en dicha aplicación; este tipo de soluciones basadas en protocolos de identificación "estándares" y "externos", nos permitirán acceder a diversas aplicaciones sin más que disponer de una identificación (por ejemplo, Google). Aun así, la aplicación necesitará asociar dicha identificación a una cuenta "interna", pero eso ya lo hace internamente, ahorrándonos a nosotros todo ese procedimiento y ese registro interno.
De hecho, la Identificación OAuth2 nos sirve muy bien como mecanismo de identificación ("Autenticación") cross, o "SSO". Pero muy básico; lo que proporciona OAuth2 es únicamente la autorización de acceso a funcionalidades para unas determinadas credenciales de acceso. Pero no nos dice nada de quién es "la persona que está detrás"; de la "Identificación".
Se puede visitar la especificación en: OAuth.
Protocolos de Autenticación
Sin embargo, pronto se vio que era necesario ir más allá con la especificación para incorporar mejoras, nuevas funcionalidades y un mejor y uso más simple del protocolo. La revisión del estándar OAuth originó OAuth2.
Aunque buena parte de los expertos en el tema se afanan en proclamar que: "OAuth2 NO ES un protocolo de autenticación", en realidad sí que lo es; lo que no es, sin embargo, es un protocolo de identificación... completo. Es más, se le denomina: "protocolo de delegación de acceso".
Es decir; tanto OAuth como OAuth2 son protocolos que permiten identificar a un usuario mínimamente, pero simplemente eso; nos permiten saber si el usuario es un usuario válido o no, algo así como una identificación booleana que permite gestionar la autorización de acceso a una aplicación o a funcionalidades internas.
Pero, como decíamos anteriormente, lo que muchos proveedores de servicios necesitan es "algo más": Saber quién es la persona o entidad que está "al otro lado"; es decir, una identificación más o menos completa.
Y, de este requisito, de esta necesidad, surge OpenID, y más concretamente OIDC/OpenID Connect. OpenID Connect es un protocolo evolucionado a partir de la especificación OpenID 2.0 pero que utiliza OAuth2 por debajo e incorpora la capacidad de ser utilizado vía API por cualquier aplicación.
Así, a partir de una validación "positiva" de un usuario vía OAuth2, OpenID Connect resuelve el problema de la identificación permitiendo ir más allá, y proporcionando un mínimo detalle sobre la identidad de dicho usuario; una especie de "ficha de usuario" normalizada.
Llegados a este punto, podemos establecer que OpenID es un OAuth2 evolucionado y, para entender cómo funciona OpenID, necesitaremos pasar por OAuth2. Posteriormente, veremos cuáles son los cambios principales de dicha evolución.
Protocolo OAuth2
Introducción a OAuth2
Como hemos dicho anteriormente, OAuth2 se basa en el concepto de "tercero de confianza".
En realidad, no es un concepto novedoso, sino que más bien es algo que ya existe, por ejemplo, con el "Certificado Digital"; nosotros usamos un "Certificado Digital" que nos permite identificarnos y operar en algunas aplicaciones en internet porque tanto nosotros como los dueños de dichas aplicaciones "confiamos" en lo que dice una "Autoridad de Certificación" (en este caso, la FNMT).
Sin embargo, y a diferencia de un "Certificado Digital", que tiene un uso y una gestión a veces engorrosas, el protocolo "OAuth2" permite identificarnos ante aplicaciones web utilizando los servicios de empresas o entidades en quienes ya se "confía" plenamente. Así, si disponemos de una cuenta en Google, LinkedIn, Facebook, Github y otros, es posible utilizar nuestra identidad "en alguno de estos proveedores" para acceder a las funcionalidades de una aplicación de terceros. La solución viene del hecho de que dicha aplicación "acepta" la información de autenticación e identificación que proporcionan estos "Proveedores de Identidad".
Entonces, ¿qué es lo que hace diferente a OAuth2 frente a un certificado digital? Para un usuario común, es relativamente sencillo; basta con acceder a nuestro proveedor de confianza y decirle que confías en la aplicación de servicio que estás utilizando, nada más.
Con el certificado digital, también sería posible, pero eso implica que, cada vez que tengamos que acceder al proveedor del servicio, tendríamos que identificarnos y, por lo tanto, disponer del certificado en cualquier ocasión que fuera necesario. Estos dos puntos se solventan, como imaginamos, con un "Proveedor de Identidad" externo. Y online.
Registro y Credenciales de la Aplicación Cliente
Para que funcione el protocolo OAuth2 correctamente es necesario que la aplicación a la que vamos a intentar acceder esté previamente registrada, de alguna manera, en el "Proveedor de Identidad" que se vaya a utilizar.
Esto no significa que, nosotros, como "usuarios" o "clientes", tengamos que hacer alguna operativa en especial, no; esencialmente, este paso "previo" lo que quiere decir es que, quien sea que haya desarrollado la aplicación a la que nos vamos a conectar más adelante, ha de acceder al "Proveedor de Identidad" que quiera utilizar y registrar ahí la aplicación; no nosotros, sino el responsable de la aplicación.
Lógicamente, ese registro lo hará un usuario (físico o jurídico) dentro del "Proveedor de Identidad" y con una cuenta o credenciales oportunas. Básicamente, es decirle al "Proveedor de Identidad", Google por ejemplo, que hay una aplicación que quiere utilizar sus servicios de identificación.
Dentro de este registro hay que indicar una información muy importante: "LA URL de redirección". Este dato sirve para que, cuando un usuario se quiera autenticar contra esta "Aplicación Cliente", y lo haga positivamente, se le redirigirá a la dirección que el "dueño" de la misma desee. Esto sirve para centralizar el control del flujo en un único punto y las aplicaciones tengan una certeza de que las peticiones serán tratadas en un mismo sitio.
Una vez que el "dueño" de la aplicación la haya registrado en el "Proveedor de Identidad", éste le proporciona dos cosas:
client_id
: El identificador de la aplicación dentro del ecosistema de aplicaciones del "Proveedor de Identidad", yclient_secret
: La contraseña de acceso.
Estos dos datos son fundamentales para la operativa OAuth2, como veremos más adelante.
De momento, base decir que estas "credenciales" representan la "Aplicación Cliente" dentro del "Proveedor de Identidad".
NOTA: Si el lector lo desea, puede "dar de alta una aplicación cliente en GitHub siguiendo los pasos que se indican aquí
Token OAuth2
En realidad, los distintos protocolos de autenticación que estamos analizando se basan en dos cosas principales:
- Identificar positivamente al usuario, y
- Hacer accesible, a todos los participantes de la transacción, una pieza, algo, que represente dicha identificación.
A esta pieza que se intercambia, y que representará una identificación positiva del usuario/cliente de la aplicación del proveedor de servicio, se la denomina comúnmente: "Token".
Así, idealmente, cuando un usuario se identifica positivamente, el "Proveedor de Identidad" genera un Token para que el proveedor del servicio (así como otros elementos participantes de la transacción) pueda tener la constancia y la certeza de que el usuario "al otro lado" es quien dice ser. Por lo tanto, un "Token" no deja de ser un identificador (una larga cadena de caracteres) que permite identificar al usuario digitalmente, y con la capacidad de que dicha información pueda ser verificada en cualquier momento por el "Proveedor de Identidad" a petición de cualquiera de los participantes en la transacción.
Además de que un Token identifique a un usuario/cliente:
- Por un lado, el Token puede incorporar información adicional como la relativa a permisos de acceso, etc.
- Por otra parte, un Token vale para lo que las aplicaciones que lo usen estimen oportuno, independientemente de cómo se obtenga.
Obtención del Token
Pero, ¿cómo obtenemos un Token?
Como decíamos antes, básicamente son dos los pasos a seguir: "Identificar" y "obtener" un Token. Conceptualmente, como vemos, los dos pasos son sencillos de entender:
- [Acceso] Se solicita al "Proveedor de Identidad" que el usuario acepte que la aplicación cliente pueda usar su información de acceso (le "otorga permiso"), y
- [Token] Se solicita un token para operar.
Técnicamente hablando, para dar solución a estas dos operativas, y tal y como se indica en la Especificación OAuth2, el protocolo OAuth2 establece únicamente dos EndPoints obligatorios:
/authorize
: Este EndPoint sirve para obtener autorización de acceso por parte del usuario/cliente./oauth/token
: Este otro EndPoint sirve para, una vez que la aplicación cliente disponga de la conveniente autorización de acceso, el "Proveedor de Identidad" genere un Token a utilizar por la aplicación cliente para acceder a los recursos.
En la Especificación OAuth2 se pueden consultar los parámetros a utilizar en cada petición; los veremos y usaremos más adelante. Ahora nos centraremos en algunos conceptos importantes: los "scopes", los "roles", los "flujos" y los "grant_type's".
Roles OAuth2
Según la Especificación OAuth2, hay cuatro roles principales:
- Resource Owner: Representa la entidad que proporciona el permiso de acceso al recurso protegido. Normalmente, es el usuario final o cliente de una aplicación.
- Resource Server: Representa el servidor que posee el recurso protegido al cual deseamos acceder finalmente. Generalmente, es el
API
que gestiona los recursos. - Client: Es la aplicación cliente a la cual accede el usuario y que necesitará permisos de acceso al recurso protegido en nombre del usuario que lo solicita.
- Authorization Server: La aplicación que autentica al Resource Owner y genera los correspondientes tokens de acceso tras conceder los permisos oportunos. A este servidor se le conoce también como "Proveedor de Identidad", o "Servidor de Autenticación".
Para que podamos "poner cara" a cada uno de estos "participantes" de una transacción, imaginémonos que queremos acceder a la información de nuestra cuenta bancaria. Así:
- Resource Owner: Nosotros, el usuario.
- Resource Server: La aplicación de la entidad financiera que alberga la información de la cuenta bancaria.
- Client: La aplicación a la que nos conectamos. Normalmente, es otra aplicación, por delante, que permite analizar qué solicitamos y deriva a la aplicación del "BackEnd" apropiado.
- Authorization Server: Un "Servidor de Autenticación" que puede ser interno, de la entidad financiera, o externo (como Google, Facebook, etc.).
Esto lo vamos a ver más concretamente en un ejemplo, en la sección de: "Orquestación OAuth2".
Scopes OAuth2
Como estamos viendo, el protocolo "OAuth2" es un protocolo de "Identificación"; es decir, permite "identificar" a alguien. Pero no tiene nada que ver con la "Autorización", que es más tema de permisos.
La identificación, en sí, permite dar paso o no a un usuario en una aplicación; es una autorización de acceso. El problema es qué puede hacer dentro de la aplicación; los permisos de ejecución. Esto es lo que se conoce como: "scopes".
Para solventar esto, cada aplicación decide, mediante la implementación de la lógica oportuna y en función de las características del usuario logado, qué puede hacer o qué no.
No obstante, y de manera totalmente adicional a esto, el protocolo "OAuth2" permite gestionar "scopes", que, en cierta medida, representarían dichos "permisos de acceso" a funcionalidades, otorgados a cada uno de los usuarios directamente en el Servidor de Autenticación. Si éste nos lo permite, podríamos definir "scopes" genéricos y asociarlos a cada usuario para usarlos cuando sea oportuno.
Como se comenta en esta página, los "scopes" es lo más aproximado que "OAuth2" está en cuanto a permisos de autorización se refiere.
Los "scopes" son entidades que proporcionamos desde la "Aplicación Cliente" y que no tienen por qué tener un significado concreto; simplemente son "textos" (por así decirlo) que se asocian según unas reglas que se establezcan a los usuarios que se identifiquen en el "Servidor de Autenticación". Con posterioridad, cuando el usuario acceda a la "Aplicación Cliente", ésta podrá usarlos, si los recibe, para gestionar el acceso a las funcionalidades internas en función de las reglas que estime oportuno.
Por ejemplo; podríamos disponer de los siguientes "scopes":
"accounts:read": # Para poder "consultar" datos.
"accounts:write": # Para poder "operar" con dichas cuentas.
O cualesquiera otros "scopes" que queramos definir.
Así, en la aplicación de destino, si recibe dicha información, podrá gestionar, por ejemplo, la autorización para efectuar transacciones en función de estos "scopes", o permisos de acceso.
Orquestación OAuth2
Siguiendo con el ejemplo anterior, y antes de profundizar en los aspectos más técnicos de la especificación, detengámonos un momento a tratar de "visualizar" toda la orquestación de un flujo OAuth2 típico.
Flujo de Peticiones
A simple vista, este diagrama de flujo parece un poco complicado, la verdad; pero a ver si, con el ejemplo, conseguimos entender todos y cada uno de los pasos de este... "baile" de peticiones.
- Acceso a la "Aplicación Cliente".
- Normalmente, nosotros, el "usuario", abrimos un navegador y tecleamos la URL de la aplicación cliente a la que solemos acceder para visualizar la información que necesitamos.
- Permiso de Acceso.
- Generalmente, la aplicación carece de permiso de acceso a dicho recurso, la información de la cuenta del banco, así que la "Aplicación Cliente" redirecciona al navegador cliente al "Servidor de Autorización" para identificarse y que éste le permita acceder a la información, al "Resource Server".
- La "Aplicación Cliente", para redirigir convenientemente al navegador del usuario al "Servidor de Autorización", prepara una URL particular, en la que incluye información de sí misma; el
client_id
visto anteriormente. ¿Lo recuerda el lector? - Imaginemos que el "Servidor de Autorización" es Google, un "aplicación externa".
- El sistema del banco "desconoce" quienes somos, pero como confía en "Google", puede aceptar una identificación positiva por su parte sobre nosotros.
- Identificación del usuario final.
- Generalmente, el "Servidor de Autenticación", en este punto suele contestar con un: "¿Tú quién eres?".
- Esto, técnicamente, se resuelve con un mensaje algo así como: "A ver, identifícate primero".
- Así, el "Servidor de Autenticación" le envía una URL de identificación a la "Aplicación Cliente" que llega directamente al navegador del usuario, el "Resource Owner", para que se identifique en el "Servidor de Autenticación".
- Autorización de la aplicación.
- El usuario, el "Resource Owner", ha de identificarse y otorgar permiso de acceso a la aplicación.
- El usuario ve cómo le sale en la ventana del navegador un formulario de identificación para identificarse "en el Servidor de Autenticación" con sus credenciales.
- Normalmente, si estamos con "Google", por ejemplo, y hemos hecho login previamente, este paso no aparece, porque ya tenemos una sesión iniciada; pero a veces, esto no es así y el "Servidor de Autenticación" nos solicita que nos identifiquemos.
- Tras identificarnos, suele aparecer otra ventana, también desde el "Servidor de Autenticación" que ya nos ha reconocido, preguntándonos si conocemos a esta aplicación "que quiere actuar en tu nombre". Usualmente, contestamos afirmativamente porque queremos eso, precisamente.
- Adicionalmente a este paso, algunos servidores solicitan también que valides los permisos de acceso; hay que tener en cuenta que tu identificación se va a propagar a una aplicación externa al "Servidor de Autenticación" y, a veces, es conveniente explicitar "para qué" se le concede permiso a la "Aplicación Cliente".
- Autorización concedida.
- En este punto, el "Servidor de Autenticación" ya sabe quién somos y que queremos darle permisos a la "Aplicación Cliente" para que actúe "en nuestro nombre" (para según qué cosas, claro; esto lo veremos más adelante).
- El "Servidor de Autenticación" le remite a la "Aplicación Cliente" un código de autorización válido.
- Este "código" sirve para que, posteriormente, se pueda obtener un Token de operación.
- Básicamente, "Google", en este caso, le dice a la "Aplicación Cliente" que el usuario que está intentando acceder es conocido y que adelante con la operativa.
- Obtención del Token.
- Con dicho código de autorización, la "Aplicación Cliente", que ya se sabe autorizada por el usuario, vuelve a llamar al "Servidor de Autenticación", pero esta vez para solicitar un Token con el que poder operar.
- Emisión del Token.
- El "Servidor de Autenticación" valida el código y genera un Token para la "Aplicación Cliente".
- Acceso al Recurso Protegido.
- Y ya, finalmente con el Token de operación en la mano, la "Aplicación Cliente" llama al "Resource Server", solicitando, en nombre del usuario final (es decir, nosotros), la información que queremos.
- Validación del Token.
- Ahora, entra en juego "otra aplicación", distinta, que reside en otro servidor (el "Resource Server") y que, claro está, es quien posee la información, y que, además, hasta ahora, no había aparecido en toda esta operativa.
- Lógicamente, la aplicación en el "Resource Server" desconoce todo cuanto ha pasado anteriormente y ve únicamente que alguien, con un "Token", le pide una información concreta. Básicamente, es como si dijera: "¿Que me estás pidiendo qué? ¿En nombre de quién? ¿Y tú, quién eres?".
- Y claro, para identificar "quién llama" rebusca en la petición a ver si hay algo que sirva; y se encuentra con el famoso Token de identificación.
- Así que, el "Resource Server", coge dicho Token y le pregunta, a su vez, al "Servidor de Autenticación": "Oye, ¿esto que me han dado, vale?"
- Autorización de Operativa.
- El "Servidor de Autenticación" identifica el token, comprueba su validez, tanto en contenido como en tiempo (esto tiene que ver con la revocación de los Tokens, que veremos más adelante), y dice si dicho token vale o no vale.
- Normalmente, el token, salvo que haya expirado la validez temporal, suele ser válido, con lo que el "Servidor de Autenticación" le responde al "Resource Server" positivamente.
- Datos.
- El "Resource Server", con el OK por parte del "Servidor de Autenticación", recupera la información que la "Aplicación Cliente" ha solicitado y se la remite
- Respuesta.
- Finalmente, la "Aplicación Cliente" obtiene lo que necesitaba del "Resource Server" y se lo envía al navegador del usuario.
Este diagrama y los pasos explicados se corresponden con, quizás, el "flujo" más complejo, el "Authorization Code Grant
". Lo cierto es que no todos los "flujos" han de seguir todos estos pasos; algunos se recortan, pero esta descripción sirve muy bien como base para entender cómo funciona en sí "OAuth2".
Algunas consideraciones.
- El "Servidor de Autenticación" actúa en todo este proceso como el "tercero de confianza"; tanto la "Aplicación Cliente" como el "Resource Server", que son aplicaciones distintas, confían en él y en lo que dice.
- Por lo general, los pasos
#3
y#4
, que hacen que el usuario autorice que la aplicación pueda actuar "en su nombre" tan sólo son necesarios una vez; la primera. En posteriores ocasiones, estos pasos no serán necesarios por cuanto el usuario ya ha concedido el conveniente permiso de acceso. - No obstante, el usuario siempre puede acceder al "Servidor de Autenticación" y revocar dicha autorización a la "Aplicación Cliente", si así lo desea.
- En este caso, si en una posterior ocasión se vuelve a necesitar dicho permiso, el usuario verá cómo tiene que volver a pasar por los pasos
#3
y#4
. - En realidad, por parte del usuario, y obviando los pasos de autorización de acceso a la "Aplicación Cliente", por lo general sólo hace una única petición y es a la "Aplicación Cliente".
- La "Aplicación Cliente", por su parte, y también obviando los pasos de autorización, efectúa dos peticiones, las correspondientes a los pasos
#2
y#6
. - El paso
#2
se corresponde con el EndPoint: "/authorize
". - El paso
#6
se corresponde con el EndPoint: "/oauth/token
". - Esta "orquestación" es compleja, sí; pero es necesaria por cuanto, en todo este proceso, se citan cuatro actores distintos.
- A partir de este ejemplo, podemos identificar con claridad los cuatro roles OAuth2:
- Resource Owner: El usuario; es decir, nosotros.
- Resource Server: El
API
de la aplicación de banca. - Client: La aplicación cliente, donde accedemos. También puede ser un
API
, si así se configura. - Authorization Server: La aplicación que gestiona identidades.
- Este diagrama comentado representa lo que se conoce como el "Happy Path". Lógicamente, hay puntos de error que se han de tratar convenientemente, según la casuística.
Flujos OAuth2
Al final, como hemos visto, el objetivo de todo este proceso es poder disponer de un token válido con el que identificar al cliente/usuario y poder operar normalmente en su nombre. Pero no todo es tan sencillo...
Hay distintas situaciones, distintas maneras de obtener un Token en función del tipo de aplicación cliente que lo solicite. Y, en función de este tipo, la manera de obtener el Token es distinta. Esto es lo que se conoce como: "flujos".
Para trabajar con un Token OAuth2, la especificación permite diversas casuísticas (en la especificación OAuth2 se detallan todas las diversas casuísticas contempladas), de entre las cuales hacemos foco en las siguientes:
- Authorization Code Grant: Flujo de uso normal por aplicaciones cliente que se ejecutan en un servidor web. También es el normalmente utilizado por aplicaciones móviles mediante la incorporación de la técnica: "Proof Key for Code Exchange (PKCE)".
- Implicit Grant: El flujo normalmente usado por aplicaciones Javascript "de una sola página" ("Single-Page Applications").
- Password / Resource Owner Credentials Grant: Utilizado por aplicaciones verificadas, sobradamente conocidas.
- Client Credentials Grant: Usado para comunicarse entre máquinas.
- Refresh Token Grant: Usado para renovar el Token.
De hecho, en la web de la Especificación OAuth2, se dispone de una página que nos permite identificar el flujo, "grant-type", más apropiado según las características y necesidades de nuestra aplicación.
En "La Biblia de los Grants OAuth2" también se puede encontrar un detalle más exhaustivo sobre estos flujos.
Authorization Code Grant
El flujo, o "grant-type" de tipo "Authorization Code Grant" debería resultarnos muy familiar por cuanto se corresponde precisamente con el ejemplo que hemos usado previamente: Iniciar sesión en una aplicación utilizando los servicios de identificación de una cuenta tipo Facebook o Google.
La descripción general la podemos encontrar en la Especificación del Flujo OAuth2, mientras que el detalle técnico está disponible en la Documentación del EndPoint /authorize
.
Otra cosa a notar aquí es que el conocido Framework Java, "Spring (Boot)", en su módulo "Spring Security" ya nos advierte de que la implementación de esta especificación en dicho Framework, únicamente acepta este tipo de flujo: "Authorization Code Grant".
Solicitud de Autorización
Como veíamos en el ejemplo, cuando el usuario le "solicita" a la "Aplicación Cliente" acceso a una determinada información, lo primero que hay que hacer es identificarse. Para esto, la "Aplicación Cliente" prepara una URL
especial, que se le remite al navegador del usuario para que, automáticamente, se redirija al "Servidor de Autenticación" y se identifique.
A efectos prácticos, vamos a usar GitHub a modo de "Servidor de Autenticación OAuth2". Dentro de GitHub disponemos de una aplicación, ya registrada, que vamos a usar para ilustrar este flujo.
Este primer paso consiste pues en remitir una petición GET
a una URL
con el siguiente formato (todo en una única línea):
GET [#BASE_URL#]/authorize
?response_type=[#RESPONSE_TYPE#]
&client_id=[#CLIENT_ID#]
&redirect_uri=[#REDIRECT_URI#]
&scope=[#SCOPE#]
&state=[#STATE#]
donde #BASE_URL#
es la parte común de acceso a los servicios "OAuth2". En GitHub, por ejemplo, esta variable es: https://github.com/login/oauth
.
Veamos qué quieren decir estos parámetros.
response_type
: [OBLIGATORIO
] Normalmente, "code
", que indica que lo que queremos recibir como respuesta es un "código de autorización".client_id
: [OBLIGATORIO
] La "Aplicación Cliente", a la hora de generar la URL final, de este paso, ha de indicar su identidad. Aquí es donde indica "quién es"; "quien es"... la aplicación, no el usuario final.redirect_uri
: [OBLIGATORIO
] Una vez que el usuario se haya identificado positivamente, en este parámetro le indicamos al "Servidor de Autenticación" que le redirija a una página específica. Es algo así como decirle al usuario, vía el "Servidor de Autenticación", dónde tiene que dirigirse una vez que se haya identificado.scope
: Con este parámetro, opcional, podremos indicar para qué queremos utilizar la autorización que solicitamos. Esto lo proporciona la propia "Aplicación Cliente", en función de los "scopes" que el "Servidor de Autenticación" tenga definidos.state
: Este parámetro, altamente recomendado, es una cadena de texto, generada automáticamente, que sirve para prevenir ataques CSRF. Se recomienda validar este dato cada vez que se efectúe algún tipo de operativa en la "Aplicación Cliente".
( NOTA: Según la especificación, el resto de parámetros son opcionales y relevantes, únicamente, para la aplicación de destino. )
A modo de ejemplo, a continuación mostramos cómo sería esta solicitud contra GitHub (también, todo en una única línea):
GET https://github.com/login/oauth/authorize
?response_type=code
&client_id=a1dcb06fd36b5fae6089
&redirect_uri=http%3A%2F%2Fexample.com%2Foauth2%2Ftoken.php
&scope=user+public_repo
&state=7f563cf6554a2eff0a17d5062b126158
Podemos ver qué ocurre si ejecutamos esta petición. Una manera práctica es con curl; otra es visualizando la llamada en el navegador usando las DevTools, que se activan con la combinación de teclas: [CTRL]
+ [MAYUS]
+ [I]
.
A modo de ejemplo, si usamos curl
:
curl -v https://github.com/login/oauth/authorize
?response_type=code
&client_id=a1dcb06fd36b5fae6089
&redirect_uri=http%3A%2F%2Fexample.com%2Foauth2%2Ftoken.php
&scope=user+public_repo
&state=7f563cf6554a2eff0a17d5062b126158
obtenemos la respuesta:
> GET /login/oauth/authorize?response_type=code&client_id=a1dcb06fd36b5fae6089&redirect_uri=http%3A%2F%2Fexample.com%2Foauth2%2Ftoken.php&scope=user+public_repo&state=7f563cf6554a2eff0a17d5062b126158 HTTP/2
> Host: github.com
> User-Agent: curl/7.61.0
> Accept: */*
>
* Connection state changed (MAX_CONCURRENT_STREAMS == 100)!
< HTTP/2 302
< server: GitHub.com
< date: Tue, 05 Oct 2021 11:50:35 GMT
< content-type: text/html; charset=utf-8
< vary: X-PJAX, X-PJAX-Container, Accept-Encoding, Accept, X-Requested-With
< permissions-policy: interest-cohort=()
< location: https://github.com/login?client_id=a1dcb06fd36b5fae6089&return_to=%2Flogin%2Foauth%2Fauthorize%3Fclient_id%3Da1dcb06fd36b5fae6089%26redirect_uri%3Dhttp%253A%252F%252Fexample.com%252Foauth2%252Ftoken.php%26response_type%3Dcode%26scope%3Duser%2Bpublic_repo%26state%3D7f563cf6554a2eff0a17d5062b126158
< cache-control: no-cache
< strict-transport-security: max-age=31536000; includeSubdomains; preload
< x-frame-options: deny
< x-content-type-options: nosniff
< x-xss-protection: 0
< referrer-policy: origin-when-cross-origin, strict-origin-when-cross-origin
< expect-ct: max-age=2592000, report-uri="https://api.github.com/_private/browser/errors"
< content-security-policy: default-src 'none'; base-uri 'self'; block-all-mixed-content; child-src github.com/assets-cdn/worker/ gist.github.com/assets-cdn/worker/; connect-src 'self' uploads.github.com www.githubstatus.com collector.githubapp.com api.github.com github-cloud.s3.amazonaws.com github-production-repository-file-5c1aeb.s3.amazonaws.com github-production-upload-manifest-file-7fdce7.s3.amazonaws.com github-production-user-asset-6210df.s3.amazonaws.com cdn.optimizely.com logx.optimizely.com/v1/events translator.github.com wss://alive.github.com; font-src github.githubassets.com; form-action 'self' github.com gist.github.com; frame-ancestors 'none'; frame-src render.githubusercontent.com viewscreen.githubusercontent.com viewscreen-lab.githubusercontent.com; img-src 'self' data: github.githubassets.com identicons.github.com collector.githubapp.com github-cloud.s3.amazonaws.com secured-user-images.githubusercontent.com/ *.githubusercontent.com; manifest-src 'self'; media-src github.com user-images.githubusercontent.com/; script-src github.githubassets.com; style-src 'unsafe-inline' github.githubassets.com; worker-src github.com/assets-cdn/worker/ gist.github.com/assets-cdn/worker/ github.com/socket-worker-0af8a29d.js gist.github.com/socket-worker-0af8a29d.js
< set-cookie: _gh_sess=WNhZn6wMx0Meikp0CN4QXi1XW3jm3%2FBYjBUIPnY7ejUKBTgPs6gBh3f%2Bnpf3IAkrpsxPzGI2MLXlEcG%2BmS3CReXcAoF0cx%2BTN83Zdb1fstC5zngO09ToL88%2FZMacnHSZ1dQEseCP3ots5ojMAXxFQ5JjMtGOqY6bsVE2%2FSMfFPZaKOHavy3rWFGccyji3Jd7v4Y6I4ur2tVIwfOoilRxJXTVOyZESIrn%2BIGjAF43954rNWIhLL6xjN9AeF1LvoS5vqowp7Rt4Ytmny27Iamapw%3D%3D--vG2gZz%2F8KGd2QBtZ--0ClAm7WOWOSYa4F6gFP5MA%3D%3D; Path=/; HttpOnly; Secure; SameSite=Lax
< set-cookie: _octo=GH1.1.584601027.1633434687; Path=/; Domain=github.com; Expires=Wed, 05 Oct 2022 11:51:27 GMT; Secure; SameSite=Lax
Como vemos, GitHub nos ha pedido que nos identifiquemos mediante dos "headers": < HTTP/2 302...
y < location: https://github.com/login?client_id=a1dcb06fd36b5fae6089&return_to=...
.
Esto vendría a ser el paso #3
del flujo del ejemplo anterior, y la URL que nos proporciona nos lleva a una página como la que mostramos a continuación.
Lógicamente, habremos de identificarnos en GitHub, en el "Servidor de Autenticación" que estamos utilizando. Y, de usar curl
, tendríamos también que estar pendientes de enviar tanto los datos como la cookie de sesión. Por esto, recomendamos usar las "DevTools" del navegador para ir viendo las peticiones y las respuestas.
Tras dicha identificación, el paso #4
, GitHub recibiríamos, en nuestro navegador, un código HTTP 302
, de redirección y una nueva URL, como por ejemplo:
> ...
< ...
< HTTP/2 302
< ...
< location: http://example.com/oauth2/token.php?code=010026acae0fc1baa264&state=7f563cf6554a2eff0a17d5062b126158
< ...
Como vemos, la respuesta es muy escueta; únicamente devuelve tres cosas:
location
: El "Servidor de Autenticación" nos redirige (le dice al navegador: "Código 302; vete a"...) una URL concreta que resulta ser la misma que habíamos indicado en el parámetro:redirect_uri
.code
: Como habíamos solicitado un "código" como tipo de respuesta, mediante el parámetro:response_type
, aquí, GitHub nos proporciona uno.state
: Realmente, el código de autorización es lo único que nos debería devolver GitHub; pero, como vimos, además del código, GitHub nos devuelve el mismo código de estado que habíamos proporcionado.
Este mecanismo nos permite validar que dicho código se corresponde con la petición que hicimos nosotros, y que podemos verificar comparando los valores del parámetro: "state" que habíamos generado anteriormente con el que nos devuelve GitHub. Esto, como hemos visto anteriormente, nos proporciona un mecanismo de seguridad para evitar ataques CSRF y cuya comprobación se recomienda encarecidamente.
Sesión ya iniciada.
- "¿Y si, por lo que sea, ya nos hemos identificado previamente?"
Pues, en este caso, este paso no se producirá. Observemos, también, que el "Servidor de Autenticación", GitHub en este caso, a la hora de hacer "login" nos está indicando también que va a mantener la sesión activa e iniciada mediante el uso de una "cookie de sesión".
Este mecanismo le permitirá a GitHub identificarnos, como usuario suyo, a lo largo de cuantas peticiones hagamos, sin más que incluir esta cookie en cada petición. Afortunadamente para nosotros, tanto del valor de la misma como del hecho de estar pendiente de su envío son cosas que ya hacen los navegadores por nosotros.
Así, si ya estuviéramos identificados previamente, únicamente recibiríamos, la URL de redirección del punto anterior.
Obtención del Token (POST
)
Una vez identificados en el Servidor de Autenticación, y habiendo autorizado a la "Aplicación Cliente" a poder acceder a "Resource Servers" en nuestro nombre, resumido e la obtención de un "código de autorización", la aplicación cliente ha de solicitar un "Token" para poder operar.
El detalle técnico para esta parte está disponible en la Documentación del EndPoint /oauth/token
.
Ahora, en este segundo paso, la "Aplicación Cliente", utilizando el código de autorización que ha recibido, tendrá que solicitar el token. Para ello, como ya sabemos, tendrá que efectuar una petición HTTP pero, en este caso, será de tipo: POST
:
POST [#BASE_URL#]/login/oauth/access_token
Para poder enviar esta petición, hemos de indicar en el encabezado HTTP que el tipo de la misma es un formulario:
Host: github.com
Accept: application/json
Content-Type: application/x-www-form-urlencoded
A la petición le incorporamos los siguientes parámetros en el "BODY
":
grant_type=authorization_code
&code=[#CODE#]
&client_id=[#client_id#]
&client_secret=[#client_secret#]
&redirect_uri=[#REDIRECT_URI#]
donde todos los datos son obligatorios y significan:
grant_type
: Para este flujo, este dato es: "authorization_code
" e implica que solicitamos un Token.code
: El código recibido anteriormente, en la petición de autorización.client_id
: El identificador de la aplicación en el "Proveedor de Identidad".client_secret
: La contraseña de acceso de la aplicación en el "Proveedor de Identidad".redirect_uri
: La dirección URL que habíamos indicado en el "Proveedor de Identidad" a la hora de registrar la aplicación. Ambos datos han de ser idénticos.
Como vemos, esta petición, desde la "Aplicación Cliente", ya incluye las credenciales completas de la misma en el "Servidor de Autenticación". Esto es así porque es una llamada entre ambos servidores.
Siguiendo el ejemplo con GitHub, la petición sería:
POST https://github.com/login/oauth/access_token
Host: github.com
Accept: application/json
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code
&code=[#CODE#]
&client_id=[#client_id#]
&client_secret=[#client_secret#]
&redirect_uri=[#REDIRECT_URI#]
En caso afirmativo, esta petición devolverá algo en formato JSON
similar a lo siguiente:
{
"access_token":"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",
"token_type":"bearer",
"scope":"public_repo,user"
}
La "Aplicación Cliente" ya dispone de un Token válido con el que trabajar; paso #7
. A partir de aquí, salvo que el token deje de tener validez temporal (que caduque, vamos), podrá ejecutar cuantas operaciones necesite contra cualquier otra aplicación que utilice los servicios de identificación del "Proveedor de Identidad" con el que estemos trabajando.
Esta petición, además de estos datos de respuesta, podría incorporar más información; dependerá del servidor. Entre otros, se nos podría informar de la validez del token y del código para renovarlo:
expires_in
: Los segundos de validez del token.refresh_token
: Una cadena de texto que servirá para solicitar un nuevo Token, cuando expire la validez del Token actual.
Obtención del Token (Basic Auth
)
Otro mecanismo de autenticación, si el "Servidor de Autenticación" lo soporta, es enviar las credenciales en el encabezado de la petición HTTP, en vez de enviar "en plano" los datos de client_id
y client_secret
.
En este caso, hay que calcular el resultante de: authString = Base64( "[#client_id#]:[#client_secret#]" )
e incorporarlo como Header HTTP:
Host: github.com
Accept: application/json
Content-Type: application/x-www-form-urlencoded
Authorization: Basic [#authString#] <<<<< ***** NUEVO *****
con lo que, en el cuerpo de la petición, podríamos obviar esta información, con lo que nos quedaría:
grant_type=authorization_code
&code=[#CODE#]
&redirect_uri=[#REDIRECT_URI#]
Siguiendo el ejemplo con GitHub, la petición sería:
POST https://github.com/login/oauth/access_token
Host: github.com
Accept: application/json
Content-Type: application/x-www-form-urlencoded
Authorization: Basic [#authString#]
grant_type=authorization_code
&code=[#CODE#]
&redirect_uri=[#REDIRECT_URI#]
Peticiones al "Resource Server"
Con el Token obtenido, como decimos, la "Aplicación Cliente" podrá efectuar llamadas tipo REST
, por lo general, al API
existente en la aplicación en marcha en el "Resource Server".
Por ejemplo, para nuestro caso imaginario de ejemplo, podríamos solicitar la lista de cuentas bancarias del usuario, sin más que hacer una petición de tipo:
GET https://api.example.com/api/v2/accounts
Pero, para poder decirle al servidor de "Resource Server" quiénes somos, tendremos que pasarle alguna información adicional en los encabezados HTTP
:
Authorization: Bearer [#TOKEN#]
Content-Type: application/json
Del diagrama de flujo del ejemplo, observamos que el "Resource Server" recibe la petición, extrae el Token de la cabecera de la petición, contacta con el "Servidor de Autenticación" y solicita la validación del mismo para cerciorarse de que está proporcionando la información solicitada a quien realmente tiene que hacerlo.
Tras este paso intermedio, que implica una petición y una respuesta, como vemos, el "Resource Server" remitirá como respuesta, la información solicitada. Como ejemplo, si hemos solicitado al propio GitHub la lista de repositorios del usuario, nos devolverá una respuesta similar a lo siguiente.
{
"accountType": "DIRECT_DEBIT",
"accountToken": "MYCOMPANY-123456780",
"defaultAccount": true,
"accountName": "Dave's Bank Account",
"displayName": "Dave's Bank Account",
"currency": "AUD",
"bsb": "032-000",
"accountNumber": "123465",
"customerId" : 123456789,
"links": []
}, {
"accountType": "DIRECT_DEBIT",
"accountToken": "MYCOMPANY-234567890",
"defaultAccount": false,
"accountName": "Jane's Bank Account",
"displayName": "Jane's Bank Account",
"currency": "AUD",
"bsb": "049-000",
"accountNumber": "234657",
"customerId" : 123456789,
"links": []
}
A partir de aquí es cuestión ya de la "Aplicación Cliente" el tratamiento de esta información y su envío al usuario final.
Authorization Code Grant con PKCE
Otra casuística que puede darse con relativa frecuencia es aquella en la que la "Aplicación Cliente" está desarrollada en código nativo o es una aplicación web de tipo "Single-Page", como pudiera ser "GMail" o similar.
En estos casos, mantener las credenciales privadas de la aplicación para poder acceder al "Servidor de Autenticación" pudiera comprometer la seguridad, bien porque se pueda decompilar la aplicación nativa o porque se acceda al código HTML/JavaScript de la aplicación Web.
Esto se explica con más profundidad en este enlace de la especificación.
De manera resumida, el flujo "Authorization Code Grant con PKCE" es muy similar al "Authorization Code Grant". La diferencia es la incorporación de un nuevo elemento a intercambiar: Un par de claves: "code_verifier
"/"code_challenge
".
Veamos paso a paso, a partir del flujo de referencia anterior, cómo funciona esto. Imaginemos que estamos en una aplicación Web...
- El flujo base es idéntico al anterior.
- A la hora de generar la URL de autorización (la llamada al EndPoint: "
/authorize
") es necesario generar un "code_verifier
". - El "
code_verifier
" no es más que una cadena de texto, generada aleatoriamente, si queremos, de longitud entre 43 y 128 caracteres. - El "
code_challenge
" es el "code_verifier
" pero encriptado con alguno de los algoritmos de encriptación disponibles. - Generalmente, se suele usar:
SHA256
, pero bien se podría utilizar otro o, incluso, enviar en texto plano. - Agregar a la URL de autorización un par de campos más:
curl -v https://github.com/login/oauth/authorize
?response_type=code
&client_id=a1dcb06fd36b5fae6089
&redirect_uri=http%3A%2F%2Fexample.com%2Foauth2%2Ftoken.php
&scope=user+public_repo
&state=7f563cf6554a2eff0a17d5062b126158
&code_challenge=[#code_challenge#]
&code_challenge_method=S256
- "
S256
" es un alias para: "SHA256
". - El "Servidor de Autenticación" responde de la misma manera que en el flujo "Authorization Code Grant":
> ...
< ...
< HTTP/2 302
< ...
< location: http://example.com/oauth2/token.php?code=010026acae0fc1baa264&state=7f563cf6554a2eff0a17d5062b126158
< ...
- Extraemos el código de autorización para solicitar el Token.
- A la hora de generar la URL de solicitud de Token un campo más:
Host: github.com
Accept: application/json
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code
&code=[#CODE#]
&client_id=[#client_id#]
&client_secret=[#client_secret#]
&redirect_uri=[#REDIRECT_URI#]
&code_verifier=[#code_verifier#]
El resto de pasos y componentes son idénticos a los del flujo de referencia.
Información adicional
Ejemplo de uso en una aplicación Web
Refresh Token Grant
Como uno se puede imaginar, este "flujo" nos permitirá renovar el Token de operación, en el "Servidor de Autenticación", una vez que expire la validez temporal del mismo.
Para renovar un Token, ejecutaríamos una petición POST
al EndPoint de obtención de Tokens, como antes, pero con la siguiente configuración:
POST https://github.com/login/oauth/access_token
Host: github.com
Accept: application/json
Content-Type: application/x-www-form-urlencoded
Authorization: Basic [#authString#]
grant_type=authorization_code&code=[#CODE#]&refresh_token=[#refresh_token#]
Como vemos, es una petición similar a la de solicitud de Token, pero en este caso, incorporamos el parámetro: refresh_token
e introducimos el valor recibido, si nos lo ha proporcionado el "Servidor de Autenticación" previamente.
La respuesta es algo similar a la que obtenemos cuando solicitamos un token:
{
"access_token":"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",
"token_type":"bearer",
"scope":"public_repo,user"
}
Implicit Grant
El flujo "Implicit Grant" es muy similar al flujo "Authorization Code Grant" con dos diferencias importantes
- Está pensado, fundamentalmente, para aplicaciones tipo "Single-Page Web", en las que, como hemos visto, mantener las credenciales de la aplicación "a salvo" no es posible (en una aplicación web, todo el código de la misma es accesible).
- El EndPoint de autorización (
/authorize
), en vez de retornar un "código de autorización", como en el flujo "Authorization Code Grant" que posteriormente se intercambia para obtener un Token, permite devolver directamente dicho Token; en una única llamada.
Para que el EndPoint de autorización nos devuelva directamente un Token, basta con indicar que el tipo de respuesta que esperamos es eso: "token
", precisamente, en vez de: "code
":
curl -i https://github.com/login/oauth/authorize
?response_type=token
&client_id=a1dcb06fd36b5fae6089
&redirect_uri=http%3A%2F%2Fexample.com%2Foauth2%2Ftoken.php
&scope=user+public_repo
&state=7f563cf6554a2eff0a17d5062b126158
Una vez que el usuario efectúa los pasos oportunos, la respuesta del "" es directamente una redirección:
> ...
< ...
< HTTP/2 302
< ...
< location: http://example.com/redirect#access_token=g0ZGZmNj4mOWIjNTk2Pw1Tk4ZTYyZGI3&token_type=Bearer&expires_in=600&state=xcoVv98y2kd44vuqwye3kcq
< ...
Puesto mejor:
http://example.com/redirect
#access_token=g0ZGZmNj4mOWIjNTk2Pw1Tk4ZTYyZGI3
&token_type=Bearer
&expires_in=600
&state=xcoVv98y2kd44vuqwye3kcq
El principal problema de este flujo es que el Token es devuelto directamente en la URL, siendo por lo tanto visible para quien esté escuchando y almacenado en el historial del navegador.
Información adicional
NOTA: Se recomienda encarecidamente NO UTILIZAR este flujo/"grant type".
En su lugar, se recomienda utilizar el flujo: "Authorization Code Grant con PKCE"
Password / Resource Owner Credentials Grant
El flujo "Password Grant" es uno de los flujos OAuth2 más simples, e implica un único paso: la Autorización.
Este flujo está pensado para que la "Aplicación Cliente" reciba las credenciales de acceso del usuario al "Servidor de Autenticación" y las utilice. Como uno se puede imaginar, este mecanismo no es lo más seguro y, precisamente, OAuth2 (y OAuth en general) se diseñó para evitar este tipo de situaciones. Tampoco está recomendado su uso, como con el flujo: "Implicit Grant"; no obstante, vamos a explicar aquí cómo funciona.
Una vez que el usuario está logado en el "Servidor de Autenticación", basta con ejecutar una única llamada para obtener el Token: /oauth/token
.
Siguiendo con el ejemplo de GitHub, observamos que cambiamos el tipo de "grant" que solicitamos; de: authorization_code
pasamos a: password
:
POST https://github.com/login/oauth/access_token
Host: github.com
Accept: application/json
Content-Type: application/x-www-form-urlencoded
grant_type=password
&username=exampleuser
&password=1234luggage
&client_id=[#client_id#]
&client_secret=[#client_secret#]
&scope=user+public_repo
Los parámetros: client_secret
y scope
son opcionales.
Información adicional
Client Credentials Grant
El flujo "Implicit Grant" es básicamente el flujo "Authorization Code Grant" pero pensado para comunicaciones exclusivamente entre aplicaciones, entre servidores; es un flujo en el que el usuario final NO participa.
Como ocurre en otros flujos, aquí también se da el caso es que sólo es necesaria una invocación, por cuanto tan sólo se necesita disponer de un Token de aplicación, sin necesidad de que el usuario lo valide.
Por eso, aquí también se usa únicamente el EndPoint de: /oauth/token
:
POST https://github.com/login/oauth/access_token
Host: github.com
Accept: application/json
Content-Type: application/x-www-form-urlencoded
grant_type=client_credentials
&client_id=[#client_id#]
&client_secret=[#client_secret#]
&scope=user+public_repo
El parámetro: scope
es opcional.
Como vemos, con esta llamada al "Servidor de Autenticación" se solicita un Token a nombre únicamente de la "Aplicación Cliente", de ahí que viajen las credenciales.
Información adicional
Información del Usuario
Hasta aquí, cumplimos con el estándar de OAuth2, y hemos usado, según el "grant-type" que se necesite, los dos EndPoints que la especificación proporciona.
Observemos la información recibida del "Servidor de Autenticación" en, por ejemplo, las dos interacciones bajo el "grant-type": "Authorization Code Grant":
/authorize
:
[#REDIRECT_URL#]?code=40a96d1ed02c87b7383a&state=b9b8c95722f1af78e1f0d0466817ee2d|b9b8c95722f1af78e1f0d0466817ee2d
/oauth/token
:
{
"access_token" : "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",
"token_type" : "bearer",
"scope" : "public_repo,user"
}
Como se puede ver, ninguno de los datos que se reciben permite identificar al usuario final. Lo que recibimos usando el protocolo OAuth2, básicamente, y como ya hemos comentado anteriormente, es una mera confirmación de que el usuario tiene una "cuenta" en el "Servidor de Autenticación", ha accedido y ha autorizado a la "Aplicación Cliente" a operar en su nombre.
Pero no sabemos nada del usuario, carecemos de cualquier tipo de información sobre él.
De la respuesta del ejemplo, vemos que únicamente recibimos un Token, que en realidad es un identificador. Si necesitamos "algo más", tendremos que usar dicho "Token" para solicitar la información oportuna al "Servidor de Autenticación".
Durante la concepción del protocolo OAuth2 existió cierto debate sobre si incluir un tercer EndPoint para obtener la información del usuario, pero finalmente se descartó. No obstante, dado que muchas "Aplicaciones Cliente" necesitan disponer de un mínimo de información sobre el usuario final, algunos "Servidores de Autenticación" implementan este tercer EndPoint que permite acceder a un mínimo de información básica del usuario.
Así, por poner algunos ejemplos, disponemos de:
- GitHub UserInfo EndPoint: https://api.github.com/user
- Google UserInfo EndPoint: https://openidconnect.googleapis.com/v1/userinfo
- Microsoft UserInfo EndPoint: https://graph.microsoft.com/oidc/userinfo
- LinkedIn UserInfo EndPoint: https://api.linkedin.com/v2/me
- Facebook UserInfo EndPoint: https://graph.facebook.com/v12.0/{person-id}/
Pero como decimos, y recalcamos aquí, estos EndPoints son OPCIONALES y algunos "Servidores de Autenticación" no disponen de esta funcionalidad.
Protocolo OpenID
OIDC / OpenID Connect
Como dijimos casi arriba del todo en este artículo, estamos analizando dos (tres, mejor dicho) protocolos:
- OAuth / OAuth2: Un Estandar abierto ("Open Standard") para delegación de acceso.
- La entidad que gestiona este estandar es: OpenID Foundation.
- OpenID: Un Estandar abierto ("Open Standard") y descentralizado para autenticación.
- La entidad que gestiona este estandar es: OpenID Foundation.
- OIDC / OpenID Connect: Combina las funcionalidades de ambos protocolos, OAuth y OpenID; es decir, permite Autenticación y Autorización.
Se podría decir, pues, que "OIDC", "OpenID Connect", es la suma de "OAuth 2.0" y "OpenID 1.0".
En la siguiente imagen mostramos una idealización conceptual de la combinación de estos protocolos.
Podríamos decir que OAuth 2.0 vendría a ser como una tarjeta de acceso de un hotel, en la que únicamente reside un identificador, mientras que la combinación de OAuth 2.0 y OpenID, que resulta en el protocolo OIDC/OpenID Connect vendría a ser como una tarjeta de identificación de empresa; además del identificador que nos permite abrir puertas y acceder a determinadas salas, muestra también una foto del propietario y algunos datos sobre él.
Especificaciones OIDC
A continuación, indicamos la documentación de las especificaciones más relevantes:
- La especificación "de la industria" está disponible en la página web de OpenID NET.
CORE
: https://openid.net/specs/openid-connect-core-1_0.htmlDiscovery
: https://openid.net/specs/openid-connect-discovery-1_0.html- La especificación
OAuth 2.0 Authorization Framework
está disponible en: https://datatracker.ietf.org/doc/html/rfc6749 - La especificación
JSON Web Token (JWT)
está disponible en: https://datatracker.ietf.org/doc/html/rfc7519 - También disponemos de una página en la que se habla de JOSE
- La especificación final de OIDC/OpenID Connect aún no existe, aunque está en
DRAFT
.
Algunas consideraciones previas
Cuando hablamos de OIDC, al "Servidor de Autenticación" se le denomina concretamente: "Proveedor OpenID" (en inglés: OpenID Provider, "OP") puesto que hace las veces tanto de identificar al usuario como de proporcionar un conjunto de permisos de acceso conjuntamente.
También se le suele denominar: "Proveedor de Identidad" (en inglés: Identity Provider, "IDP").
De igual modo, a la "Aplicación Cliente" se le denomina: "Relying Party" ("RP").
OIDC se define, por lo tanto, como la combinación funcional (a alto nivel, claro) de:
- Gestión de autorización de acceso, proporcionada por OAuth 2.0, y
- Gestión de identidades, proporcionada por OpenID.
OIDC se ha diseñado, pues, tomando como base la especificación OAuth 2.0. Pero, como OAuth 2.0 no incluye, en su especificación, nada relacionado con la "Identificación" del usuario, que proporcionaría OpenID, se hace necesario agregar algún componente más a la especificación; algo que, además de proporcionar un Token de acceso, incorpore también información de Identidad sobre el usuario.
Sin embargo, con la pretensión de que el protocolo OIDC permita que un OP pueda actuar tanto como proveedor de identidad como únicamente de autorización de acceso, ambas características han de estar disponibles. Por esta razón, la especificación OIDC incorpora nuevos elementos que permiten flexibilizar ambas operativas:
- Acceso a toda la configuración de los EndPoints disponibles y los parámetros aceptados
- Cambios en la declaración y uso del parámetro: "
response_type
" - Ahora, se permite multiplicidad de peticiones.
- Incorporación de nuestos tipos:
id_token
ynone
- Declaración y determinación del flujo concreto a utilizar en función de lo demandado
- Nuevos "
scope
's": openid
: Obligatorio, si se pretende obtener información de identificación.- Otros scopes:
profile
,email
,address
yphone
- Nuevo componente de identidad: "
id_token
" en formato "JWT
", ("JSON Web Token"). - Uso de
JWT
/JWE
, ("JSON Web Token") y de "JOSE
"("Javascript Object Signing and Encryption (JOSE)")
A continuación, pasamos a examinar estos cambios.
Discovery EndPoint
Otra de las novedades de OIDC frente a OAuth 2.0 es la aparición de un "Discovery EndPoint".
Esto está definido en un documento específico en la Especificación OIDC Discovery EndPoint.
A medida que la especificación OIDC va incorporando más y más elementos (veremos algunos más adelante) se hace necesario disponer de un mecanismo al que cualquier "Relying Party" pueda invocar para recuperar información operativa. Es decir, un "OP"
debe proporcionar las funcionalidades "mínimas" que se requieren y, además, algunas opcionales, e incluso establecer las direcciones URL correctas, según su especificación. ¿Cómo podría un "Relying Party" saber cómo está configurado dicho "OP"
?
Para esto nació el "Discovery EndPoint".
El formato general de este nuevo EndPoint es:
https://YOUR_DOMAIN/.well-known/openid-configuration
A continuación, listamos algunos ejemplos de EndPoints de Configuración que implementan algunos proveedores OIDC:
Provider | Discovery OpenID (Well-Known) URL |
---|---|
https://accounts.google.com/.well-known/openid-configuration | |
Microsoft | https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration |
https://www.facebook.com/.well-known/openid-configuration |
Un vistazo a estas configuraciones nos permitirá ir teniendo una idea de para qué está este EndPoint y darnos cuenta de que cada proveedor establece las "URL de servicio" que estima oportuno. También podemos notar cómo hay algunos elementos que aparecen en todos ellos mientras que otros, no.
Google dispone de un poco más de información y uso de este recurso.
Response Types y Flujos
La especificación OAuth 2.0 indica que el campo: "response_type
" sólo puede ser: "code
", "token
" o alguno de los definidos más adelante en la especificación. Sin embargo, como hemos visto, se hace necesario incorporar la información de identidad, como elemento resultante en una petición, sin dejar de recibir también cualquier otro tipo de componente que ya se estuviera intercambiando bajo el protocolo "OAuth2".
Por esta razón, ahora, con OIDC no sólo se incorpora un nuevo tipo de respuesta esperada, "openid
"", sino que ya es posible combinar estos distintos tipos de "response_type
's", tal como indica la Especificación OIDC.
Además, en función de lo que se solicite, el tipo de "flujo" "OAuth2" bajo el cual se operará estará predeterminado de entre tres tipos de flujos: "Authorization Code", "Implicit" e "Hybrid" (nuevo).
Authorization Code
: Especificando:response_type
=code
Implicit
: Especificando:response_type
=id_token token
(los dos tipos simultáneamente) oresponse_type
=id_token
Hybrid
: Resto de combinaciones
En resumen:
"response_type " |
Flow |
---|---|
code |
Authorization Code Flow |
id_token |
Implicit Flow |
id_token token |
Implicit Flow |
code id_token |
Hybrid Flow |
code token |
Hybrid Flow |
code id_token |
token Hybrid Flow |
Esto está más profusamente explicado en esta página.
Scopes
Al incorporar información de identificación del usuario, el protocolo OIDC permite especificar qué tipo de información de identificación se necesita.
Esto se consigue indicando en el parámetro: scope
lo que se necesita. Obligatoriamente, habremos de indicar: "openid
", además de los scopes que ya necesitemos, tipo Auth2; en incluso podremos especificar alguno de los "scope's" OpenID nuevos predefinidos. Estos nuevos "scope's son: openid
, profile
, email
, address
y phone
.
Si no es indica como "scope": "openid
", el flujo seleccionado a ejecutar devolverá una información u otra.
Así, combinando el tipo de respuesta esperado con la aparición del tipo de scope "openid
"", tendríamos las siguientes combinaciones:
"response_type " |
Flow | scope "openid " |
EndPoint | Auth Code | Access Token | ID Token |
---|---|---|---|---|---|---|
code |
Authorization Code Flow | ✅ | Auth | ✅ | ⛔ | ⛔ |
Authorization Code Flow | ✅ | Token | ⛔ | ✅ | ✅ | |
Authorization Code Flow | ⛔ | Auth | ✅ | ⛔ | ⛔ | |
Authorization Code Flow | ⛔ | Token | ⛔ | ✅ | ⛔ | |
token |
Implicit Flow | <any> | Auth | ✅ | ✅ | ⛔ |
id_token |
Implicit Flow | <any> | Auth | ⛔ | ⛔ | ✅ |
id_token token |
Implicit Flow | <any> | Auth | ⛔ | ✅ | ✅ |
code id_token |
Hybrid Flow | <any> | Auth | ✅ | ⛔ | ✅ |
Hybrid Flow | <any> | Token | ⛔ | ✅ | ✅ | |
code token |
Hybrid Flow | ✅ | Auth | ✅ | ✅ | ⛔ |
Hybrid Flow | ✅ | Token | ⛔ | ✅ | ✅ | |
Hybrid Flow | ⛔ | Auth | ✅ | ✅ | ⛔ | |
Hybrid Flow | ⛔ | Token | ⛔ | ✅ | ⛔ | |
code id_token token |
Hybrid Flow | <any> | Auth | ✅ | ✅ | ✅ |
Hybrid Flow | <any> | Token | ⛔ | ✅ | ✅ |
ID Tokens
Habíamos hablado de que, con OIDC ahora ya es posible obtener, en un mismo flujo, distintas respuestas. Así, por ejemplo, podríamos solicitar un token de acceso (el típico de OAuth2) y otro de Identificación, el nuevo que proporciona OIDC.
Para esto, como hemos visto, hemos de usar alguna combinación de las anteriores que nos permita disponer de un "Access Token
" y de un "ID Token
", simultáneamente.
Al final, el "OP"
nos proporcionará una respuesta en la que probablemente incluya el ya conocido access_token
, conjuntamente con el nuevo id_token
. Pero, ¿qué es y qué aspecto tiene este nuevo "token"? Veamos un ejemplo.
Ejemplo de operativa
Supongamos que solicitamos dos tipos de respuestas: "id_token
" y "code
", con un scope determinado, "openid
" (además de los que ya se pudieran estar solicitando). Vamos a ver cómo sería todo este flujo pero utilizando los servicios de otro "OP"
: "OKTA
". Esto es porque, de momento, GitHub NO IMPLEMENTA OIDC. Por eso, usaremos este otro proveedor.
Para ello, primero, tendremos que crearnos un perfil y activar una aplicación de autenticación/autorización, siguiendo los pasos que ofrece el proveedor.
NOTA: OKTA ofrece una guía de configuración para actuar como "Proveedor de Identidad OIDC".
Por un lado, hemos de crear una cuenta en OKTA, que podemos hacer con un click, usando GitHub.
Por otra parte, podemos usar tanto esta página como los tres enlaces que aparecen en la parte final de la misma para configurar nuestro proveedor de identidad. Adicionalmente, disponemos también de la información que se proporciona en esta otra página.
Una vez efectuado esto, que dejamos como tarea al lector procedemos con las llamadas, incorporando este nuevo scope: "openid
":
NOTA: OKTA nos proporcionará un subdominio para nosotros, así como credenciales de usuario y de aplicación.
Para el propósito de esta práctica, necesitaremos:
[#SERVER_DOMAIN#]: Elhostname
del servidor OIDC que OKTA nos proporciona. Algo así como:dev-XXXXXXX.okta.com
[#CLIENT_ID#]: Elclient_id
de la aplicación OIDC en OKTA.
[#CLIENT_SECRET#]: Elclient_secret
de la aplicación OIDC en OKTA.
[#REDIRECT_URI#]: La URL a la cual deseamos que redirija la aplicación. Puede ser, como antes:http://example.com/oauth2/token.php
Discovery EndPoint
Lo primero que podemos hacer es usar este nuevo EndPoint de la especificación: El "Discovery EndPoint".
¿Cómo accedemos a él? Bueno, el "formato estándar" que proporciona este "OP"
es el siguiente:
https://[#SERVER_DOMAIN#]/oauth2/default/.well-known/openid-configuration
Se puede usar el navegador y nos dará una respuesta similar a la siguiente:
{
"issuer": "https://[#SERVER_DOMAIN#]/oauth2/default",
"authorization_endpoint": "https://[#SERVER_DOMAIN#]/oauth2/default/v1/authorize",
"token_endpoint": "https://[#SERVER_DOMAIN#]/oauth2/default/v1/token",
"userinfo_endpoint": "https://[#SERVER_DOMAIN#]/oauth2/default/v1/userinfo",
"registration_endpoint": "https://[#SERVER_DOMAIN#]/oauth2/v1/clients",
"jwks_uri": "https://[#SERVER_DOMAIN#]/oauth2/default/v1/keys",
"response_types_supported": [
"code",
"id_token",
"code id_token",
"code token",
"id_token token",
"code id_token token"
],
"response_modes_supported": [
"query",
"fragment",
"form_post",
"okta_post_message"
],
"grant_types_supported": [
"authorization_code",
"implicit",
"refresh_token",
"password"
],
"subject_types_supported": [
"public"
],
"id_token_signing_alg_values_supported": [
"RS256"
],
"scopes_supported": [
"openid",
"profile",
"email",
"address",
"phone",
"offline_access"
],
"token_endpoint_auth_methods_supported": [
"client_secret_basic",
"client_secret_post",
"client_secret_jwt",
"private_key_jwt",
"none"
],
"claims_supported": [
"iss",
"ver",
"sub",
"aud",
"iat",
"exp",
"jti",
"auth_time",
"amr",
"idp",
"nonce",
"name",
"nickname",
"preferred_username",
"given_name",
"middle_name",
"family_name",
"email",
"email_verified",
"profile",
"zoneinfo",
"locale",
"address",
"phone_number",
"picture",
"website",
"gender",
"birthdate",
"updated_at",
"at_hash",
"c_hash"
],
"code_challenge_methods_supported": [
"S256"
],
"introspection_endpoint": "https://[#SERVER_DOMAIN#]/oauth2/default/v1/introspect",
"introspection_endpoint_auth_methods_supported": [
"client_secret_basic",
"client_secret_post",
"client_secret_jwt",
"private_key_jwt",
"none"
],
"revocation_endpoint": "https://[#SERVER_DOMAIN#]/oauth2/default/v1/revoke",
"revocation_endpoint_auth_methods_supported": [
"client_secret_basic",
"client_secret_post",
"client_secret_jwt",
"private_key_jwt",
"none"
],
"end_session_endpoint": "https://[#SERVER_DOMAIN#]/oauth2/default/v1/logout",
"request_parameter_supported": true,
"request_object_signing_alg_values_supported": [
"HS256",
"HS384",
"HS512",
"RS256",
"RS384",
"RS512",
"ES256",
"ES384",
"ES512"
]
}
Creemos que es este JSON suficientemente autoexplicativo. De momento, lo que nos interesa son estos tres EndPoints:
{
...
"authorization_endpoint": "https://[#SERVER_DOMAIN#]/oauth2/default/v1/authorize",
"token_endpoint": "https://[#SERVER_DOMAIN#]/oauth2/default/v1/token",
"userinfo_endpoint": "https://[#SERVER_DOMAIN#]/oauth2/default/v1/userinfo",
...
}
REQUEST Authorization Code
Con la información ya preparada en nuestras manos, vamos a solicitar autorización a OKTA. Usaremos el "authorization_endpoint
".
Un ejemplo de petición sería el siguiente (incorporando los valores oportunos, claro):
curl -i https://[#SERVER_DOMAIN#]/oauth2/default/v1/authorize
?client_id=[#CLIENT_ID#]
&response_type=id_token%20code
&scope=openid%20profile%20email%20address%20phone
&redirect_uri=[#REDIRECT_URI#]
&state=WM6D
&nonce=YsG76jo
Como vemos, la petición incluye dos tipos de respuesta (id_token
y code
) y varios scopes, pero que incluyen, como mínimo el scope: openid
.
Podemos usar curl
o el propio navegador. Si usamos el navegador, debemos hacer dos cosas antes:
- Hacer login en OKTA, para que el proceso sea más directo.
- De no hacer login, como sabemos, previamente a la hora de solicitar la URL, el OP ("OKTA" en este caso) nos solicitará que nos loguemos y que autoricemos a la aplicación.
- Abrir las DevTools y limpiando las peticiones de red para que podamos ver toda la traza limpia.
Una vez hecho esto, copiamos la petición y la ponemos en el navegador y, tras pulsar, veremos que nos lleva a la página que hemos establecido como [#REDIRECT_URI#]
.
RESPONSE Authorization Code
Ahora, sin embargo, la dirección a la que nos redirige OKTA es ligeramente distinta del formato anteriormente visto. Es algo así como:
[#REDIRECT_URL#]#id_token=eyJraWQi.....NgVNdRmsneTLOJIl-dzdfw&code=XU_ToCq0QSbAlWs7s7TzxTRnd8RVhrTwum7U8Cz9JFk&state=WM6D
Puesto en modo más "visible":
#id_token=eyJraWQi.....NgVNdRmsneTLOJIl-dzdfw
&code=XU_ToCq0QSbAlWs7s7TzxTRnd8RVhrTwum7U8Cz9JFk
&state=WM6D
Como vemos, la petición de autorización incluye los dos parámetros que ya proporciona OAuth2 y, además, un "id_token
", tal como habíamos solicitado.
Este "ID Token", como sabemos, viene en formato: "JWT". Más adelante lo exploraremos; por ahora, simplemente saber que es así y usarlo.
NOTA: Este segundo "id_token
" es muy similar al anterior, como veremos, pero su contenido pudiera ser distinto del primero.
REQUEST Token
A continuación, y aunque hayamos obtenido ya el id_token
, vamos a solicitar el Token de autorización de acceso, utilizando el valor que se nos ha proporcionado como: code
. Ahora, usaremos "curl
" para llamar al "token_endpoint
".
ccurl -X POST
-H "Host: [#SERVER_DOMAIN#]"
-H "application/json"
-H "Content-Type: application/x-www-form-urlencoded"
-d "grant_type=authorization_code"
-d "code=[#CODE#]"
-d "client_id=[#CLIENT_ID#]"
-d "client_secret=[#CLIENT_SECRET#]"
-d "redirect_uri=[#REDIRECT_URI#]"
-d "scope=openid%20profile%20email%20address%20phone"
"https://[#SERVER_DOMAIN#]/oauth2/default/v1/token"
Esta petición es un "POST
", de tipo formulario, que esperamos un JSON como respuesta, y con unos campos de formulario que se indican con el parámetro: "-d
". Recordemos usar aquí el código de autorización proporcionado en la respuesta de la llamada anterior.
RESPONSE Token
Al ejecutar este comando en una consola, veremos la respuesta en la consola:
{
"token_type": "Bearer",
"expires_in": 3600,
"access_token": "eyJraWQiOiJCVUtsUE9...YBC-RJxK1SWnOF4ZjPg",
"scope": "phone profile email openid address",
"id_token": "eyJraWQiOiJCVUtsUE9...yWSnXTZNWc5Xi4BvNUsfzafzHFXkwe-wtvfoOyoIQ"
}
Como vemos, este "OP"
nos devuelve también otros dos "Tokens"; uno para la autorización, "access_token
" y otro para la identificación, "id_token
". ¡Y ambos en formato JWT!
En el ejemplo, el contenido de este campo ("id_token") está recortado (por temas de visualización) pero, a título de ejemplo, podría ser algo así como lo siguiente:
eyJ0eXAiOiJKV1QiLA0KICJhbGciOiJIUzI1NiJ9
.eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ
.dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk
En resumen, OIDC utiliza JSON Web Tokens simples (JWT) para devolver la información de "Identificación" del usuario. El problema es que, en este momento, es algo "críptico". El campo: "id_token
" es lo que se conoce como un "JWT, JSON Web Token".
UserInfo EndPoint
Finalmente, una última cosa por ver: El EndPoint de información del detalle del usuario. Ahora, usaremos "curl
" para llamar al "userinfo_endpoint
", el tercer EndPoint que nos quedaba. Aquí, tendremos que usar el "Token" de acceso que acabamos de recibir, el "access_token
".
ccurl -X GET
-H "Host: [#SERVER_DOMAIN#]"
-H "application/json"
-H "Authorization: Bearer [#ACCESS_TOKEN#]"
"https://[#SERVER_DOMAIN#]/oauth2/default/v1/userinfo"
Si ejecutamos este comando desde la consola, el resultado que nos devuelve el "OP"
es similar al siguiente:
{
"sub": "<El ID único del usuario en este OP>",
"name": "<El nombre completo del usuario>",
"locale": "<El idioma preferido>",
"email": "<La dirección de correo electrónico>",
"preferred_username": "<El alias>",
"given_name": "<El nombre>",
"family_name": "<Los apellidos>",
"zoneinfo": "<La zona horaria>",
"updated_at": "<La fecha de actualización de la información>",
"email_verified": "<Si el email está verificado o no>",
}
Como vemos, es un conjunto mínimo de información acerca del usuario registrado para que nuestra aplicación cliente, nuestro "RP"
, pueda disponer de una especie de "ficha de usuario" para lo que estime conveniente.
Entre esta información y la que se proporciona en los diversos "id_token
"'s previos, se dispone de suficiente información para completar, automáticamente, un perfil de usuario y operar convenientemente.
JWT / JSON Web Tokens
Examinemos ahora los "JWT". Como casi siempre, la mejor manera de ver qué es un JWT es... ¡viéndolo!
Copiamos en el portapapeles el último "id_token
" de ejemplo:
eyJ0eXAiOiJKV1QiLA0KICJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ.dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk
Vamos a la URL del JWT Debugger (https://jwt.io/#debugger-io
), limpiamos la caja: "Encoded" y pegamos ahí nuestro id_token
.
A la derecha, vemos la "decodificación" de dicho Token. La siguiente imagen muestra el resultado.
Como, en este ejemplo hemos obtenidos tres tokens JWT diferentes, podemos examinarlos con "JWT Debugger" y así explorar su contenido. Pero, ¿qué es cada uno de esos elementos?
En la página #5 de la especificación JSON Web Token (JWT) podemos ver las definiciones que se usan. Extractamos algunas de ellas.
The terms "JSON Web Signature (JWS)", "Base64url Encoding", "Header
Parameter", "JOSE Header", "JWS Compact Serialization", "JWS
Payload", "JWS Signature", and "Unsecured JWS" are defined by the JWS
specification [JWS].
The terms "JSON Web Encryption (JWE)", "Content Encryption Key
(CEK)", "JWE Compact Serialization", "JWE Encrypted Key", and "JWE
Initialization Vector" are defined by the JWE specification [JWE].
JSON Web Token (JWT)
A string representing a set of claims as a JSON object that is
encoded in a JWS or JWE, enabling the claims to be digitally
signed or MACed and/or encrypted.
JWT Claims Set
A JSON object that contains the claims conveyed by the JWT.
A JWT is represented as a sequence of URL-safe parts separated by
period ('.') characters. Each part contains a base64url-encoded
value. The number of parts in the JWT is dependent upon the
representation of the resulting JWS using the JWS Compact
Serialization or JWE using the JWE Compact Serialization.
Básicamente, un "JWT
" es un objeto que tiene tres partes, separadas entre sí por un caracter de ".", "punto". Cada una de esas tres partes contiene un valor (el que sea) codificado bajo "Base64
". En el caso de "OIDC", este "valor" va a ser un objeto "JSON".
Recuperemos el ejemplo anterior:
eyJ0eXAiOiJKV1QiLA0KICJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ.dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk
Esto, en JWT Debugger
, se nos convertía en:
{
"typ": "JWT",
"alg": "HS256"
}
.
{
"iss": "joe",
"exp": 1300819380,
"http://example.com/is_root": true
}
.
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
your-256-bit-secret
)
La primera parte de nuestro JWT, "eyJ0eXAiOiJKV1QiLA0KICJhbGciOiJIUzI1NiJ9
", queda "traducida" en: "{ "typ": "JWT", "alg": "HS256" }
"; la segunda, "eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ
" en: "{ "iss": "joe", "exp": 1300819380, "http://example.com/is_root": true }
" y la tercera en algo que parece un dato encriptado.
Estructura de un JWT
La estructura de un "JWT
" es una cadena de texto que se subdivide en tres partes donde, cada una de ellas tiene un significado especial. Así, de la especificación y con un poco de tranquilidad, podemos decir que un JWT se compone de:
- "JWT (JOSE) Header": Representa el "encabezado" del mensaje.
- "JWS Payload / JWT Claims Set": Es el "contenido" que la aplicación del
"OP"
desea trasladar al"RP"
. - "JWS Signature": Una "firma digital" del mensaje.
Para información más técnica, sugerimos visitar estos enlaces:
- https://auth0.com/docs/security/tokens
- https://auth0.com/docs/security/tokens/json-web-tokens
- https://auth0.com/docs/security/tokens/json-web-tokens/validate-json-web-tokens
- https://auth0.com/docs/security/tokens/json-web-tokens/json-web-token-structure
- https://auth0.com/docs/security/tokens/json-web-tokens/json-web-token-claims
- https://auth0.com/docs/security/tokens/json-web-tokens/json-web-key-sets
- https://auth0.com/docs/security/tokens/json-web-tokens/locate-json-web-key-sets
- https://auth0.com/docs/security/tokens/json-web-tokens/json-web-key-set-properties
JWT JOSE Header
El encabezado de un JWT es un objeto JSON que representa el tipo de contenido se intercambia y cómo se ha generado la firma digital de la tercera parte, la del "JWS Signature".
En esencia, un encabezado contiene la siguiente estructura de información:
{
"typ": "<el tipo de contenido del PAYLOAD>",
"alg": "<el algoritmo utilizado para cifrar el mensaje>"
}
El tipo de contenido del payload (typ
) suele ser: "JWT
", en MAYUSCULAS
, por cuestiones de compatibilidad.
Sobre el algoritmo de encriptación (alg
), decir que los que más se suelen usar son: "HMAC with SHA-256
" (HS256
) y "RSA signature with SHA-256
" (RS256
). Se podrían usar otros; de hecho, la especificación JSON Web Algorithms (JWA) incorpora algunos más, pero lo más común es encontrarnos con alguno de estos dos algoritmos.
JWT Payload
El Payload de un JWT también es un objeto JSON. En este caso, este objeto incorpora toda la información que el "OP"
desea intercambiar con el "RP"
.
Al contenido del Payload se le suele denominar "JWT Claims Set
"; es decir, es un conjunto de "claims". Un "claim" no es más que un par "clave/valor", donde el "nombre" sigue unas reglas especialmente definidas.
La especificación JWT nos indica que algunos "claims" ("nombres de claims", mejor dicho) están ya predefinidos. En esta otra página, de auth0.com
, también se listan estos "claims".
Pero un Payload no tiene por qué contener todos o parte de estos "claims"; como es de imaginar, la aplicación que hace las veces de "OP"
es quien decide qué información va a proporcionar en el JWT Payload. El "RP"
únicamente decide qué elementos de dicho Payload necesita utilizar.
JWS Signature
La tercera y última parte de un JWT representa la "firma digital" del contenido del mensaje. Para ello, se convierte a Base64 tanto el encabezado como el Payload, se concatenan con un caracter de "punto" (".") y se encripta con el algoritmo deseado. En el caso de usar SHA256, como vimos anteriormente, bastaría con codificar en nuestro código de "OP"
el JWT así
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
your-256-bit-secret
)
A modo de ejemplo ilustrativo, podemos echar un vistazo a cómo se haría esto con un ejemplo con Java Estándar: https://connect2id.com/products/nimbus-jose-jwt/examples/jwt-with-rsa-encryption
Conclusiones
En resumen, OIDC / OpenID Connect nos permite no sólo autorizar usuarios sino acceder, aunque sea mínimamente, a una autenticación básica. Esto puede ser muy útil para muchas aplicaciones Web que precisan de esa información básica del usuario para poder crear y gestionar tanto la cuenta como los accesos a la aplicación.
Pensemos que, de esta manera, y bien gestionado en la aplicación de destino, sería posible identificarse con un proveedor (por ejemplo, Google) y, a partir de dicha información, quizás sería posible autenticarse posteriormente con otro (por ejemplo, GitHub) si ambas informaciones de perfilado están disponibles y correlacionadas de alguna manera.
Dos apuntes más: Por un lado, al implementar OIDC bajo Spring disponemos, como viene siendo habitual, de prácticamente toda la implementación a falta de una mera configuración, con lo que incorporar OIDC a nuestros proyectos/aplicaciones es tremendamente sencillo, además de que ya existen diversas plataformas/servidores de identidad, y cada vez más, que soportan este estándar. Y, por otro lado, hacer notar aquí que la especificación OIDC, como extensión de OAuth2, incluye, bajo JWT, validación por certificación digital, un nivel más en lo que se refiere a la seguridad en las comunicaciones.
Recursos
A continuación, mostramos una serie de recursos encontrados por Internet que pueden servir de utilidad al lector.
NOTA: En ROJO los artículos que se consideran más relevantes.
Documentación General
Documentation General sobre OAuth
Site | Descripción | URL |
---|---|---|
Auth0 | Documentación OAuth2 | https://auth0.com/docs/get-started |
OAuth2 Grant Types | https://auth0.com/docs/authorization/flows | |
OAuth2 Protocolos: OAuth2.0 & OpenID Connect | https://auth0.com/docs/authorization/protocols | |
OAuth2 Security Tokens | https://auth0.com/docs/security/tokens | |
OAuth2 API de Autenticación | https://auth0.com/docs/api/authentication | |
OAuth.Net | Doc Root | https://oauth.net/ |
OAuth 1.0 | https://oauth.net/1/ | |
OAuth 2.0 | https://oauth.net/2/ | |
OAuth 2.0 Getting Started | https://oauth.net/getting-started/ |
Especificaciones
Documentation OAuth 2.0
Documentation en formato PDF
Descripción | URL |
---|---|
Autenticación con OpenID Connect a través de SimpleSAMLphp | https://www.rediris.es/jt/jt2018/programa/jt/ponencias/?id=jt2018-jt--a36b3c1.pdf |
OAuth2 and OpenID Connect: The Professional Guide | https://assets.ctfassets.net/2ntc334xpx65/4OlKBjXfM6ml9tDZi0JG1K/4ae8e1a264462638694cefb2762167c2/OAuth2-and-OpenID-Connect-The-Professional-Guide.pdf |
OpenID Connect Explained | https://connect2id.com/assets/oidc-explained.pdf |
Securing Microservices with OAuth 2.0 and OpenID Connect | https://owasp.org/www-pdf-archive//OAuth_2_and_OpenID_Connect_-_Andreas_Falk.pdf |
Documentation Técnica sobre OAuth 2.0 y OpenID Connect
Ejemplos Técnicos
Información Técnica
Site | Descripción | URL |
---|---|---|
Discovery EndPoint | https://www.facebook.com/.well-known/openid-configuration/ | |
UserInfo EndPoint | https://graph.facebook.com/v12.0/{person-id}/ | |
GitHub | GitHub UserInfo EndPoint | https://api.github.com/user |
Discovery EndPoint | https://accounts.google.com/.well-known/openid-configuration | |
UserInfo EndPoint | https://openidconnect.googleapis.com/v1/userinfo | |
LinkedIn UserInfo EndPoint | https://api.linkedin.com/v2/me | |
Microsoft | Discovery EndPoint | https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration |
UserInfo EndPoint | https://graph.microsoft.com/oidc/userinfo |
Utilidades
Descripción | URL |
---|---|
JWT | https://jwt.io/ |
JWT | https://jwt.ms/ |
Baeldung Twitter Account | https://twitter.com/Baeldung |
Spring Twitter Account | https://twitter.com/SpringFramework |
UNIX Epoch Converter | https://www.epochconverter.com/ |
BPMn.io | https://demo.bpmn.io/new |
SVG to PNG | https://svgtopng.com/es/ |
Swagger Editor | https://editor.swagger.io/ |
42Crunch | https://platform.42crunch.com/ |