Cuándo hablamos de añadir una capa de autenticación y autorización a nuestros servicios (por ejemplo, a una API), una de las alternativas más comunes es la de los JSON Web Token (JWT). Así que, no hay nada mejor que podamos hacer que ver una pequeña introducción sobre como usar esta técnica en nuestros proyectos Go. No sin antes comprender un poco cómo funcionan.
JSON Web Token
Los JSON Web Token (JWT) son un estándar abierto que los define como una forma compacta y autónoma para transmitir información de forma segura entre dos partes en forma de objeto JSON. Además, esta información puede ser verificada porque está firmada digitalmente.
¿Cuándo usar los JWT?
Ahora que ya hemos visto (por encima) qué son los JWT, veamos cuándo debemos hacer uso de ellos.
Dos de los escenarios más frecuentes son:
-
Autorización: con el fin de implementar la capa de autorización de nuestros servicios, lo que vamos a hacer es que, una vez que el usuario haya iniciado sesión, cada solicitud posterior del mismo incluirá su JWT, lo que nos permitirá identificarle y consecuentemente determinar si puede (o no) acceder a determinadas rutas, servicios y/o recursos.
Para éste propósito, lo que vamos a hacer será enviar una cabecera
Authorization
en nuestras peticiones HTTP con el valorBearer
seguido del token, por ejemplo:Bearer xxx.yyy.zzz
. -
Intercambio de información: como ya comentamos anteriormente, los JWT son una buena forma de transmitir información de forma segura entre las partes debido a que los JWT se pueden firmar, por ejemplo, utilizando claves públicas. De este modo podemos garantizar que los remitentes son quiénes dicen ser. Además, como la firma se calcula utilizando tanto las cabeceras como el contenido, también podemos verificar que éste último no haya sido alterado.
¿Qué pinta tienen los JWT?
Una vez convencidos de que queremos usar los JWT para alguno de los escenarios mostrados anteriormente, lo siguiente sería
conocer qué pinta tienen estos tokens. Bien, pues básicamente los JWT se componen de tres elementos separados por un
punto (.
):
- La cabecera (o header): que generalmente consta de dos partes: el tipo de token (JWT) y el algoritmo con el
que fue firmado (como HMAC SHA256 o RSA). Este contenido, como podemos deducir por su nombre, está en formato JSON
(
{"alg": "HS256", "type": "JWT"}
) y codificado en Base64Url. - El contenido (o payload): que contiene lo que se conoce como claims (generalmente, el usuario) y otros datos adicionales. También está codificado en Base64Url.
- La signatura (o signature): resultante de coger la cabecera y el contenido codificados, un secreto, el algoritmo de firma especificado en la cabecera y firmarlo. Ésta puede ser usada para verificar que el contenido no se modificó en el camino y, en el caso de los tokens firmados con una clave privada, también sirve para verificar que el remitente del JWT es quién dice ser.
De modo que, si analizamos uno de estos tokens podremos identificar la siguiente forma:
xxxxx.yyyyy.zzzzz
Y un ejemplo “real” podría ser:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
Finalmente, podemos usar esta herramienta para experimentar con algunos ejemplos.
JWT en Go
En lo que a JWT en Go se refiere, la librería más extendida es la dgrijalva/jwt-go
.
Esta nos proporciona todas las herramientas necesarias tanto para la construcción y la firma de los JWT así como su
posterior procesado y validación. Veamos algunos ejemplos.
Construcción y firma
Para poder usar los tokens, primero necesitamos construirlos. Para ello, vamos a usar el método
jwt.NewWithClaims
. Este método espera,
en primer lugar, el algoritmo de firmado, y
en segundo lugar, los claims que comentábamos anteriormente.
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"email": "hello@friendsofgo.tech",
"birthday": time.Date(2019, 01, 01, 0, 0, 0, 0, time.UTC).Unix(),
})
Como podéis ver, la propia librería nos proporciona varias constantes que hacen referencia a cada uno de los algoritmos
de firmado disponibles. Además, nos proporciona la estructura jwt.MapClaims
para que podamos definir los claims que queremos incluir en el payload del token en un formato clave - valor.
Adicionalmente, también nos proporciona la estructura jwt.StandardClaims
con los claims más comunes. O incluso nos podemos definir nuestra propia estructura de claims y / o “embeber” la anterior.
type MyCustomClaims struct {
Email string `json:"email"`
Birthday int64 `json:"birthday"`
jwt.StandardClaims
}
// Create the claims
claims := MyCustomClaims{
"hello@friendsofgo.tech",
time.Date(2019, 01, 01, 0, 0, 0, 0, time.UTC).Unix(),
jwt.StandardClaims{
ExpiresAt: 15000,
Issuer: "Friends of Go",
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
Una vez construido nuestro token solo nos quedará firmarlo mediante el método
jwt.SignedString
, el cuál recibe como
argumento el secreto con el que queremos firmar nuestro token y que nos devolverá el token firmado:
mySecret := "my-secret"
signedToken, err := token.SignedString(mySecret)
Parseo y verificación
Bien, ahora que ya somos capaces de construir nuestros propios JWT así que ha llegado el momento de que los usuarios de nuestro servicio empiecen a usarlos. Sin embargo, para ello deberemos ser capaces de parsearlos y verificarlos, de otro modo estaremos ante un potencial agujero de seguridad de nuestra aplicación.
Para parsear el token recibido vamos a hacer uso del método jwt.Parse
,
que espera como primer argumento el token y como segundo argumento una función de verificación, dónde comprobaremos
que tanto el algoritmo de firmado como el secreto obtenidos de parsear el token son los esperados (de otro modo sabremos
que el token ha sido alterado).
Como podéis ver en el ejemplo a continuación, la función jwt.Keyfunc
nos permitirá hacer las validaciones pertinentes sobre el token en cuestión. Dicha función será usada por el método
jwt.Parse
para detectar posibles alteraciones durante dicho parseo.
receivedToken := "xxxxx.yyyyy.zzzzz"
token, err := jwt.Parse(receivedToken, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"])
}
return mySecret, nil
})
Una vez parseado ya tendremos el jwt.Token
y
además ya habremos verificado que dicho token no fue alterado de forma malintencionada. En este momento solo nos
faltará hacer una type assertion al tipo de claims que hayamos usado para obtener los claims contenidos por el token.
if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
fmt.Println(claims["email"], claims["birthday"])
} else {
fmt.Println(err)
}
Y, de este modo tan sencillo, ya seremos capaces de usar los JSON Web Token (JWT) en nuestras aplicaciones Go.
Haciendo uso de un middleware JWT
Aunque a riesgo de salirnos un poco del alcance de este artículo, y sin perder el derecho a hacer un artículo práctico, específico sobre esta última aplicación, no podíamos cerrar el artículo sin comentar un último aspecto.
Y es que, tal y como hemos comentado anteriormente, la forma más común de utilizar los JSON Web Token (JWT) es mediante las cabeceras HTTP. Es por eso, que la mayoría de frameworks web o librerías de routing o middleware suelen proporcionar módulos que nos facilitan dicha tarea.
Por ejemplo, si usáis el framework gin-gonic
, tenéis el middleware
gin-jwt
que nos permite personalizar
todos los atributos de generación y uso de los tokens (algoritmo de firmado, claims, expiración, handlers, etc). O,
por ejemplo, si usáis negroni
tenéis este par (
go-jwt-middleware
y
go-jwtmiddleware
)
de middlewares que también os simplificarán la vida en relación al uso de los JWT.
Como siempre, estaremos encantados de recibir vuestro feedback en los comentarios del blog o vía Twitter.