From 9f91ab1b8a037e45dea8c9042a7718f8a636bf08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Gerdau?= Date: Wed, 13 Nov 2019 17:07:16 +0100 Subject: [PATCH] Add support for RSA signing keys --- login/config.go | 2 +- login/handler.go | 13 +++++++- login/handler_test.go | 70 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 83 insertions(+), 2 deletions(-) diff --git a/login/config.go b/login/config.go index bdd0b336..362f7913 100644 --- a/login/config.go +++ b/login/config.go @@ -117,7 +117,7 @@ func (c *Config) ConfigureFlagSet(f *flag.FlagSet) { f.StringVar(&c.LogLevel, "log-level", c.LogLevel, "The log level") f.BoolVar(&c.TextLogging, "text-logging", c.TextLogging, "Log in text format instead of json") f.StringVar(&c.JwtSecret, "jwt-secret", c.JwtSecret, "The secret to sign the jwt token") - f.StringVar(&c.JwtAlgo, "jwt-algo", c.JwtAlgo, "The singing algorithm to use (ES256, ES384, ES512, HS512, HS256, HS384, HS512)") + f.StringVar(&c.JwtAlgo, "jwt-algo", c.JwtAlgo, "The singing algorithm to use (ES256, ES384, ES512, RS256, RS384, RS512, HS256, HS384, HS512") f.DurationVar(&c.JwtExpiry, "jwt-expiry", c.JwtExpiry, "The expiry duration for the jwt token, e.g. 2h or 3h30m") f.IntVar(&c.JwtRefreshes, "jwt-refreshes", c.JwtRefreshes, "The maximum amount of jwt refreshes. 0 by Default") f.StringVar(&c.CookieName, "cookie-name", c.CookieName, "The name of the jwt cookie") diff --git a/login/handler.go b/login/handler.go index 42249347..400b895e 100644 --- a/login/handler.go +++ b/login/handler.go @@ -324,7 +324,7 @@ func (h *Handler) signingInfo() (signingMethod jwt.SigningMethod, key, verifyKey keyString := h.config.JwtSecret switch h.config.JwtAlgo { case "ES256", "ES384", "ES512": - if !strings.Contains(string(keyString), "-----") { + if !strings.Contains(keyString, "-----") { keyString = "-----BEGIN EC PRIVATE KEY-----\n" + keyString + "\n-----END EC PRIVATE KEY-----" } @@ -334,6 +334,17 @@ func (h *Handler) signingInfo() (signingMethod jwt.SigningMethod, key, verifyKey } h.signingKey = key h.signingVerifyKey = key.Public() + case "RS256", "RS384", "RS512": + if !strings.Contains(keyString, "-----") { + keyString = "-----BEGIN RSA PRIVATE KEY-----\n" + keyString + "\n-----END RSA PRIVATE KEY-----" + } + + key, err := jwt.ParseRSAPrivateKeyFromPEM([]byte(keyString)) + if err != nil { + return nil, nil, nil, errors.Wrap(err, "can not parse PEM formated RSA private key") + } + h.signingKey = key + h.signingVerifyKey = key.Public() default: h.signingKey = []byte(keyString) h.signingVerifyKey = h.signingKey diff --git a/login/handler_test.go b/login/handler_test.go index 06bed7e7..c2f0564d 100644 --- a/login/handler_test.go +++ b/login/handler_test.go @@ -1,7 +1,11 @@ package login import ( + "crypto/rand" + "crypto/rsa" + "crypto/x509" "encoding/json" + "encoding/pem" "errors" "fmt" "net/http" @@ -516,6 +520,72 @@ func TestHandler_signAndVerify_ES256(t *testing.T) { Equal(t, input, userInfo) } +func TestHandler_signAndVerify_RSA(t *testing.T) { + h := testHandler() + + tt := []int{ + 256, + 384, + 512, + } + for _, bits := range tt { + jwtAlgo := fmt.Sprintf("RS%d", bits) + t.Run(jwtAlgo, func(t *testing.T) { + t.Parallel() + + key, err := rsa.GenerateKey(rand.Reader, bits*2) + NoError(t, err) + + privateKey := &pem.Block{ + Type: "PRIVATE KEY", + Bytes: x509.MarshalPKCS1PrivateKey(key), + } + + h.config.JwtAlgo = jwtAlgo + h.config.JwtSecret = string(pem.EncodeToMemory(privateKey)) + + input := model.UserInfo{Sub: "marvin", Expiry: time.Now().Add(time.Second).Unix()} + token, err := h.createToken(input) + NoError(t, err) + r := &http.Request{ + Header: http.Header{"Cookie": {h.config.CookieName + "=" + token + ";"}}, + } + userInfo, valid := h.GetToken(r) + True(t, valid) + Equal(t, input, userInfo) + }) + } +} + +func TestHandler_signAndVerify_RSA_headerless(t *testing.T) { + h := testHandler() + h.config.JwtAlgo = "RS256" + h.config.JwtSecret = "" + + "MIICXAIBAAKBgQDSu7M1jiH06fGywhSw5jdjUdfX6b1yw8j2coVjAgT1oB44vU+S" + + "dgvak/tWoBkqG+Gdrn0m+3H/mRtGXWZDmh6VjQ5mnw91OGVJccL2UGdEbb4ub/9g" + + "4Bobn1ANUcbZvXWpmNP0kqyBwsXiaq6iL4TNW5iKdvnat7SwzLyIkGwPkQIDAQAB" + + "AoGAXpshs1Nh7z/v4F69R0WzbAVcL3SiNpmq6Ok09OP9MgB2UOa8iHYykCiLV7J8" + + "Wak2usGRMiUEYslrs0VPGd5hB9X94fDAh0SYC2wmBOJRBY2tU82pSkN5RjE8A3+f" + + "G6uwlZB2UtpYa/sihf7NkJCQh2ibT3YeelDUvEnfwALB6iECQQDck14kDckwi4mt" + + "LwWPqXTWAdKdTN1i6KGXDBt7Bi5lbVk3XFgQy/Z+GzRiBtjcWmcMw2VOUeFC9d/J" + + "WnRv8NklAkEA9JOsxEgzr7utqw3Zd1dDK5weDhAwXuaHiCIS+bDAsGor7pSgWOtU" + + "k+kpDdPe/TmtxJFhorJOsl+49VtYVoy+/QJAWJnlhcv31cUnL2ak8DkcUl53EHJw" + + "tytExVy6qScpedp7rM4uHckgITWiTAH+GD1ECY9vYQ9o0bHcC5CHFvQC9QJBAOwI" + + "2ONVCwy+A4zhgM472QdtU1QfK49qy8IFoGp4un2G+X720Qj/lFBq5MQDhWC9GYZr" + + "B98MVgavesDPtyFQE8ECQCRZaTDF4d5KBAvu5ogoqEATD5r21V4Zj5uZ/QSeI7+v" + + "UVncBYg6g4CIrczoqYpJ3aBF5MVJ0FEU9XCDO/iDvCU=" + + input := model.UserInfo{Sub: "marvin", Expiry: time.Now().Add(time.Second).Unix()} + token, err := h.createToken(input) + NoError(t, err) + r := &http.Request{ + Header: http.Header{"Cookie": {h.config.CookieName + "=" + token + ";"}}, + } + userInfo, valid := h.GetToken(r) + True(t, valid) + Equal(t, input, userInfo) +} + func TestHandler_getToken_InvalidSecret(t *testing.T) { h := testHandler() input := model.UserInfo{Sub: "marvin"}