Firmado JWT con RSA (RS512) para Autenticación en Microservicios con Go y gRPC-Gateway V2

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.

Etiquetas: Go gRPC-Gateway JWT RSA RS512

Publicado el 6-21 02:08