OAuth 2.0 Grant Types

Muy buenos días y gracias por acompañarnos un martes más. La pregunta de hoy y por 25 pesetas la respuesta acertada: díganos tipos de concesión (Grant Types) permitidos por OAuth 2.0, como por ejemplo “password”. Un, dos, tres, responda otra vez.

[sonido ensordecedor de bocina]

Escuchemos a los super-tacañones:

Aunque un servicio OAuth es capaz de devolver JSON Web Tokens, JWT no es un tipo de concesión válido.

OAuth 2.0 es una especificación que describe diferentes formas (Grant Types o tipos de concesión) de solicitar un token de acceso (access_token) para un servicio HTTP. Se usa como base de la especificación Open Id Connect (OIDC) y también de los protocolos de autenticación implementados por las grandes empresas de internet (Twitter, Facebook, Microsoft… Google de hecho usa OIDC). De forma que, cuando hablemos de OAuth, nos referiremos a un protocolo de autenticación basado en OAuth 2.0.

Si bien es verdad que el RFC de OAuth no habla explícitamente de JWT como formato para los access_token, es la forma más común en la que lo podremos encontrar hoy en día.

Dentro de este contexto, OAuth propone varias formas de autenticarse (Grant Types), e incluso una forma de crear nuestro propios formatos, pero los más utilizados hoy por hoy son los que exponíamos anteriormente: Authorization Code, Authorization Code con PKCE, Client Credentials, Implicit, Password y Refresh Token.

Access Token

La respuesta de todos los métodos de autenticación al final tiene que ser un formato semejante: un JSON con los siguientes valores:

Authorization Code

El Authorization Code es uno de los flujos de autenticación que más beneficios ofrece. Se utiliza por lo general en páginas web. La idea es que inicialmente se solicita una autenticación con el siguiente formato:

GET /oauth/authorize
   ?client_id=example_client_id
   &response_type=code
   &redirect_uri=http%3A%2F%2Fexampledomain.com
   &state=string_as_status
   &scope=openid HTTP/1.1
Host: authorizationserver.com

Donde:

Entonces el servidor de autorización solicita un usuario y un password vía un formulario web. Al introducir datos correctos, el servidor nos redireccionará a la página que le pasamos en el parámetro redirect_uri:

http://exampledomain.com/
    ?code=examplecode
    &state=string_as_status

En esta respuesta el valor de state debe ser el mismo que pasamos en la petición. Y como parámetro code encontraremos un código, que normalmente es válido durante unos 60 segundos, a partir del que podremos realizar la petición del token:

POST /oauth/token HTTP/1.1
Host: authorizationserver.com
Accept: application/json
Authorization: Basic user_password_formula
Content-Type: application/x-www-form-urlencoded
Content-Length: ...

grant_type=authorization_code
&redirect_uri=http%3A%2F%2Fexampledomain.com
&code=example_code

Donde:

Como peculiaridad, la petición vendrá autenticada usando el esquema “Basic” cuyo contenido responde a la siguiente fórmula:

user_password_formula = base64(client_id + ":" + client_secret)

Donde:

Como respuesta tendremos el formato anteriormente descrito de Access Token:

HTTP/1.1 200 OK
Cache-Control: no-store
Pragma: no-cache
Content-Type: application/json
Content-Length: ...

{
    "access_token": "a_lot_of_characters_in_base_64",
    "token_type": "Bearer",
    "expires_in": 3600,
    "scope": "openid",
    "id_token": "a_lot_of_characters_in_base_64"
}

Authorization Code con PKCE

Se usa PKCE (Proof Key for Code Exchange) con el fin de tener una comunicación segura sin tener que usar valores de client_secret. Es la solución recomendada para aplicaciones móviles y Single Page Application (SPA).

El flujo es exactamente igual al anterior, salvo porque vamos a añadir dos parámetros nuevos:

De esta forma la petición inicial sería:

GET /oauth/authorize
   ?client_id=example_client_id
   &response_type=code
   &redirect_uri=http%3A%2F%2Fexampledomain.com
   &state=string_as_status
   &scope=openid
   &code_challenge_method=S256
   &code_challenge=example_code_challenge HTTP/1.1
Host: authorizationserver.com

Donde:

Entonces el servidor de autorización solicita un usuario y un password vía un formulario web. Al introducir datos correctos, el servidor nos redireccionará a la página que le pasamos en el parámetro redirect_uri:

http://exampledomain.com/
    ?code=examplecode
    &state=string_as_status

En esta respuesta el valor de state debe ser el mismo que pasamos en la petición. Y como parámetro code encontraremos un código, que normalmente es válido durante unos 60 segundos, a partir del que podremos realizar la petición del token:

POST /oauth/token HTTP/1.1
Host: authorizationserver.com
Accept: application/json
Content-Type: application/x-www-form-urlencoded
Content-Length: ...

grant_type=authorization_code
&redirect_uri=http%3A%2F%2Fexampledomain.com
&code=example_code
&code_verifier=example_code_verifier

Donde:

Al contrario que el Authorization Code simple, en este caso no se requiere la cabera de “Authorization”.

Como respuesta tendremos el Access Token:

HTTP/1.1 200 OK
Cache-Control: no-store
Pragma: no-cache
Content-Type: application/json
Content-Length: ...

{
    "access_token": "a_lot_of_characters_in_base_64",
    "token_type": "Bearer",
    "expires_in": 3600,
    "scope": "openid",
    "id_token": "a_lot_of_characters_in_base_64"
}

Client Credentials

El modelo más sencillo de autenticarse de OAuth 2.0 es Client Credentials. Se usa para la autenticación de máquina a máquina, donde no se requiere el permiso de un usuario específico para acceder a los datos.

Su funcionamiento consiste en realizar una petición al servidor en el endpoint del generador de tokens:

POST /oauth/token HTTP/1.1
Host: authorization-server.com
Accept: application/json
Content-Type: application/x-www-form-urlencoded
Content-Length: ...

grant_type=client_credentials
&client_id=example_client_id
&client_secret=example_client_secret
&scope=user.read

Donde:

La respuesta directamente será el Access Token:

HTTP/1.1 200 OK
Cache-Control: no-store
Pragma: no-cache
Content-Type: application/json
Content-Length: ...

{
  "access_token": "a_lot_of_characters_in_base_64",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "a_lot_of_characters_in_base_64",
  "scope": "user.read"
}

Implicit

Cuando hablamos de un flujo de autenticación Implicit lo más probable es que estemos trabajando con páginas web SPA (Single Page Application). Generalmente, NO se recomienda usar este flujo, e incluso algunos servidores, prohíben su uso. Hoy en día se recomienda usar en su lugar el flujo de Authorization Code con PKCE.

De cualquier forma, podría ser que tengamos que usarlo, así que nunca sobra describirlo. Todo consiste en una petición simple al servidor:

GET /oauth/authorize
    ?client_id=example_client_id
    &response_type=token
    &redirect_uri=http%3A%2F%2Fexampledomain.com
    &state=string_as_status&scope=openid
    &scope=openid HTTP/1.1
Host: authorizationserver.com

Donde:

La respuesta de esta petición será una llamada a la URI que le pasamos en redirect_uri, con el siguiente formato:

http://exampledomain.com/
    #access_token=a_lot_of_characters_in_base_64
    &token_type=Bearer
    &expires_in=3600
    &state=string_as_status

De tal forma que podremos comparar el valor de state y sacar la información del Access Token del resto de parámetros.

Password

También conocido como Resource Owner Password Credentials, este flujo de autenticación es solo recomendable en entornos seguros, donde existe una relación de confianza entre el cliente y el servidor. Algo así como un servicio del sistema operativo o una aplicación que requiera permisos elevados. En resumen, este debería ser el último flujo que deberíamos usar, tan solo reservado para cuando no tenemos otra posibilidad.

Se parece mucho a Client Credentials, pero con la diferencia de que aquí vamos a autenticar a un usuario del sistema. De esta manera, crearemos una petición muy parecida al de ese modelo, pero añadiendo ciertos campos adicionales:

POST /oauth/token HTTP/1.1
Host: authorizationserver.com
Accept: application/json
Content-Type: application/x-www-form-urlencoded
Content-Length: ...

grant_type=password
&username=exampleuser
&password=examplepassword
&client_id=example_client_id
&client_secret=example_client_secret
&scope=user.read

Y la respuesta, será el Access Token:

HTTP/1.1 200 OK
Cache-Control: no-store
Pragma: no-cache
Content-Type: application/json
Content-Length: ...

{
  "access_token": "a_lot_of_characters_in_base_64",
  "token_type": "Bearer",
  "expires_in": 3600,
  "scope": "user.read"
}

Refresh Token

Con el fin de que no siempre se estén transmitiendo los mismos datos (algunos de ellos sensibles), otro de los flujos que se nos proponen es el de Refresh Token. Para ello necesitaremos haber obtenido un Access Token, y que este, a parte de expiración, tenga el campo refresh_token indicado.

Así pues, usando este campo, después de que el token anterior haya caducado, podremos obtener otro token nuevo. Eso sí, un refresh_token también tiene una caducidad y a su vez es de un solo uso. De esta forma no podremos generar todos los tokens nuevos que queramos, tan solo uno.

Este método es muy sencillo, realizaremos una petición simple al servidor OAuth como la siguiente:

POST /oauth/token HTTP/1.1
Host: authorizationserver.com
Accept: application/json
Content-Type: application/x-www-form-urlencoded
Content-Length: ...

grant_type=refresh_token
&client_id=example_client_id
&client_secret=example_client_secret
&refresh_token=a_lot_of_characters_in_base_64

Donde:

Y la respuesta vuelve a ser semejante a las demás:

HTTP/1.1 200 OK
Cache-Control: no-store
Pragma: no-cache
Content-Type: application/json
Content-Length: ...

{
    "access_token": "a_lot_of_characters_in_base_64",
    "token_type": "Bearer",
    "expires_in": 3600,
    "refresh_token": "a_lot_of_characters_in_base_64"
}

Conclusiones

Si en otro artículo os explicábamos el tema de JWT y cómo validarlo para poder proteger nuestras APIs, hoy nos hemos centrado en diferentes formas de autenticarnos en un servicio que soporte OAuth 2.0, y por supuesto, recibir ese JWT.

Estos no son todos los métodos, concesiones o Grant Types que existen, aunque sí los más usados.

Cuando escribo sobre temas de seguridad es muy posible que me pierda en repeticiones y referencia a RFCs. Me resulta difícil dar razones de peso o resultar útil sin usar ese formato. Quizá os resulte pesado. Pero lo cierto es que, no sé vosotros, pero dentro de varios meses, seguro que vuelvo a este artículo a buscar algún detalle que no recuerdo del todo…

… y hasta aquí puedo leer.