Introducción a JWT y RS512
En el desarrollo de microservicios, JSON Web Tokens (JWT) se utilizan para la autenticación segura. Cuando se emplea el algoritmo RSA (RS512), se requiere un par de claves: una clave privada para firmar tokens y una clave pública para verificarlos. Esto garantiza que solo el servicio emisor pueda crear tokens válidos, y otros servicios puedan validar su integridad y caducidad sin accceso a la clave privada.
Un JWT se compone de tres partes: encabezado (header), carga útil (payload) y firma (signature), codificadas en Base64 y separadas por puntos. El encabezado especifica el algoritmo, la carga útil contiene los datos (como issuer, expiration y subject), y la firma se genera usando la clave privada para asegurar la autenticidad.
Configuración de Claves RSA
Para este ejemplo, se utilizarán claves de prueba proporcionadas por la documentación de JWT.io. La clave privada debe almacenarse de forma segura, por ejemplo, en un archivo private.key, y la clave pública en public.key. A continuación, se muestra un ejemplo de configuración para carga útil:
{
"exp": 1516246222,
"iat": 1516239022,
"iss": "servidor/auth",
"sub": "607266aa512e006d58b79d22"
}
Aquí, iss identifica al emisor (el servicio de autenticación), iat es el tiempo de emisión, exp el tiempo de expiración, y sub el identificador del usuario o cuenta.
Implementación en Go
Se define una interfaz para la generación de tokens, permitiendo desacoplar la lógica y facilitar pruebas o cambios en el algoritmo.
type GeneradorTokens interface {
CrearToken(idCuenta string, duracion time.Duration) (string, error)
}
El servicio de autenticación se modifica para incluir esta interfaz como dependencia, lo que hace la configuración más flexible:
type Servicio struct {
Mongo *dao.Mongo
Logger *zap.Logger
ResolvedorID ResolvedorID
GeneradorToken GeneradorTokens
TiempoExpira time.Duration
authpb.UnimplementedAuthServiceServer
}
Durante el inicio de sesión, se invoca al generador de tokens:
token, err := s.GeneradorToken.CrearToken(idCuenta, s.TiempoExpira)
if err != nil {
s.Logger.Error("fallo al generar token", zap.Error(err))
return nil, status.Error(codes.Internal, "")
}
return &authpb.LoginResponse{
AccessToken: token,
ExpiresIn: int32(s.TiempoExpira.Seconds()),
}, nil
Generador de Tokens JWT
Se implementa la interfaz GeneradorTokens usando JWT con RS512. La estructura incluye la clave privada, el issuer y una función para la hora actual (útil en pruebas).
type JWTGenerador struct {
clavePrivada *rsa.PrivateKey
emisor string
funcHora func() time.Time
}
func NuevoJWTGenerador(emisor string, clavePrivada *rsa.PrivateKey) *JWTGenerador {
return &JWTGenerador{
emisor: emisor,
funcHora: time.Now,
clavePrivada: clavePrivada,
}
}
func (g *JWTGenerador) CrearToken(idCuenta string, duracion time.Duration) (string, error) {
horaActual := g.funcHora().Unix()
token := jwt.NewWithClaims(jwt.SigningMethodRS512, jwt.StandardClaims{
Issuer: g.emisor,
IssuedAt: horaActual,
ExpiresAt: horaActual + int64(duracion.Seconds()),
Subject: idCuenta,
})
return token.SignedString(g.clavePrivada)
}
Pruebas Unitarias
Se verifica la correcta generación de tokens usando una hora fija para reproducibilidad. Las pruebas cargan la clave privada y comparan el token resultante con uno esperado.
func TestCrearToken(t *testing.T) {
archivoClave, err := os.Open("../private.key")
if err != nil {
logger.Fatal("error al abrir clave privada", zap.Error(err))
}
datosClave, err := ioutil.ReadAll(archivoClave)
if err != nil {
logger.Fatal("error al leer clave privada", zap.Error(err))
}
clave, err := jwt.ParseRSAPrivateKeyFromPEM(datosClave)
if err != nil {
logger.Fatal("error al parsear clave privada", zap.Error(err))
}
gen := NuevoJWTGenerador("servidor/auth", clave)
gen.funcHora = func() time.Time {
return time.Unix(1516239022, 0)
}
token, err := gen.CrearToken("607266aa512e006d58b79d22", 2*time.Hour)
if err != nil {
t.Errorf("error al generar token: %v", err)
}
esperado := "eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1MTYyNDYyMjIsImlhdCI6MTUxNjIzOTAyMiwiaXNzIjoic2VydmVyL2F1dGgiLCJzdWIiOiI2MDcyNjZhYTUxMmUwMDZkNThiNzlkMjIifQ.nwhaGZ0dozftexVfr9KM9ZVAzsPudhLs-n-yyrrjkbFTYA69rsEd35M0vc1gJ1DNMJk_v-1yUhkgRpxzP2Jiy1Lw8fqIlAk8l9EpDE77oJ9Dal6Rl26GERYZOkCvbq02fKSVj4drlSr75fIce9EnQq2xIVyvvNNty-QvHXTX29QQv-6c8vVYIrCFxtooARN9p8OSpg0hzc-YzsXo64lbUvbLIws27TJNwhctbqrOYQuX9XU3UhJ4Ik0Yt2cLc4LjuqI52Grvf89mJMmM5jnHQv0tKI2guvxNwlC3WN50dCIcuo1zjO-_eSje5OvqP7FKR1eSwnEcZiZQ8qwDDGi8pA"
if token != esperado {
t.Errorf("token incorrecto. esperado: %q, obtenido: %q", esperado, token)
}
}
Integración en el Servicio
Finalmente, se configura el servicio con los parámetros necesarios, como el tiempo de expiración y el generador de tokens, al iniciar el microservicio.
authpb.RegisterAuthServiceServer(s, &auth.Servicio{
ResolvedorID: &wechat.Servicio{
AppID: "tu-app-id",
AppSecret: "tu-app-secret",
},
Mongo: dao.NewMongo(mongoClient.Database("grpc-gateway-auth")),
Logger: logger,
TiempoExpira: 2 * time.Hour,
GeneradorToken: token.NuevoJWTGenerador("servidor/auth", clavePrivada),
})
Este enfoque permite una autenticación segura y escalable en entornos de microservicios, donde los tokens pueden ser validados por múltiples servicios usando la clave pública compartida.