Implementando Authorization Code Grant (PKCE)

Clase 25 de 39Curso de Autenticación con OAuth

Authorization Code Grant tiene algunos problemas de seguridad cuando se implementa en aplicaciones nativas. Por ejemplo, un atacante malicioso puede interceptar el authorization_code retornado por el Authorization Server y usarlo para obtener un Access Token.

La Proof Key for Code Exchange (PKCE), definida en https://tools.ietf.org/html/rfc7636, es una técnica usada para mitigar la intercepción del authorization_code.

Con PKCE la aplicación crea, por cada petición de autorización, una llave criptográfica aleatoria llamada code_verifier y su valor transformado llamado code_challenge el cual es enviado al Authorization Server para obtener un authorization_code y posteriormente enviarlo junto con el code_verifier para obtener un Access Token.

Conociendo el flujo

  1. La aplicación nativa inicia el flujo y redirecciona al usuario al Authorization Server enviando los parámetros code_challenge y code_challenge_method.
  2. El Authorization Server redirecciona el usuario a la aplicación nativa con un authorization_code en el query string.
  3. La aplicación nativa envía el authorization_code y el code_verifier junto con la url de redireccionamiento (redirect_uri) y el client_id al Authorization Server.
  4. El Authorization Server valida la información y retorna un Access Token.
  5. La aplicación nativa ahora puede usar el Access Token para llamar los recursos en nombre del usuario (API).

Detalles de implementación

Vamos a ver los detalles de implementación usando JavaScript/Node.js, ya que son operaciones y request básicos que puede implementar cualquier lenguaje nativo. Para ver más detalles de una implementación especifica en lenguajes como Java o Swift pueden consultar una guía aquí.

  1. Necesitamos generar y almacenar un code_verifier.
function base64URLEncode(str) { return str .toString("base64") .replace(/\+/g, "-") .replace(/\//g, "_") .replace(/=/g, ""); } const verifier = base64URLEncode(crypto.randomBytes(32));
  1. Mediante el code_verifier generamos un code_challenge que será enviando en el llamado de autorización.
function sha256(buffer) { return crypto .createHash("sha256") .update(buffer) .digest(); } const challenge = base64URLEncode(sha256(verifier));
  1. Para comenzar el Authorization Code Grant (PKCE), nuestra aplicación nativa deberá enviar primero el usuario a la url de autorización incluyendo el code_challenge y el método usado para su generación y así obtener el authorization_code.
https://<authorization-server>/authorize? audience=<your-audience>& scope=<your-scopes>& response_type=code& client_id=<your-cient-id>& code_challenge=<code-challenge>& code_challenge_method=S256& redirect_uri=<your-redirect-uri>
  1. Ahora que nuestra aplicación tiene el authorization_code lo debemos cambiar por un Access Token que puede ser usado para hacer llamados a los recursos del usuario. Para ello obtenemos el authorization_code (code) de nuestro paso anterior y hacemos un llamado tipo POST al endpoint de obtención de token enviando también el code_verifier.
const request = require("request"); const options = { method: "POST", url: "https://<authorization-server>/oauth/token", headers: { "content-type": "application/json" }, body: '{"grant_type":"authorization_code","client_id": "<your-client-id>","code_verifier": "<your-code-verifier>","code": "<your-authorization-code>","redirect_uri": "<your-redirect-uri>"}' }; request(options, function(error, response, body) { if (error) throw new Error(error); console.log(body); });

La respuesta tiene un JSON Web Token, generalmente de tipo Bearer:

{ "access_token": "eyJz93a...k4laUWw", "token_type": "Bearer" }
  1. Ya teniendo nuestro Access Token podemos hacer llamados a los recursos del usuario (API) en nombre de él.
const request = require("request"); const options = { method: "GET", url: "https://someapi.com/api", headers: { authorization: "Bearer <access-token>", "content-type": "application/json" } }; request(options, function(error, response, body) { if (error) throw new Error(error); console.log(body); });

Con esto tenemos los conocimientos necesarios para poder implementar este flujo. Recuerda seguir las recomendaciones para darle un uso adecuado.