loginsrv

Unnamed repository; edit this file 'description' to name the repository.
git clone git@jamesshield.xyz:repos/loginsrv.git
Log | Files | Refs | README | LICENSE

commit 25912cbf31688cd0e47c2adfbfaaa4b9f7e7fb07
parent 0d99e0462cb72464572e003bd3bd26cf2615d1c4
Author: Gregor Weckbecker <gregor@feiner-fug.net>
Date:   Fri, 16 Feb 2018 12:46:03 +0100

Merge pull request #67 from tarent/ec-support

support for elliptic curve signing methods
Diffstat:
MREADME.md | 1+
Mcaddy/setup_test.go | 6++++++
Mlogin/config.go | 3+++
Mlogin/config_test.go | 4++++
Mlogin/handler.go | 50+++++++++++++++++++++++++++++++++++++++++++-------
Mlogin/handler_test.go | 15+++++++++++++++
6 files changed, 72 insertions(+), 7 deletions(-)

diff --git a/README.md b/README.md @@ -55,6 +55,7 @@ _Note for Caddy users_: Not all parameters are available in Caddy. See the table | -htpasswd | value | | X | Htpasswd login backend opts: file=/path/to/pwdfile | | -jwt-expiry | go duration | 24h | X | The expiry duration for the jwt token, e.g. 2h or 3h30m | | -jwt-secret | string | "random key" | X | The secret to sign the jwt token | +| -jwt-algo | string | "HS512" | X | The singing algorithm to use (ES256, ES384, ES512, HS512, HS256, HS384, HS512) | | -log-level | string | "info" | - | The log level | | -login-path | string | "/login" | X | The path of the login resource | | -logout-url | string | | X | The url or path to redirect after logout | diff --git a/caddy/setup_test.go b/caddy/setup_test.go @@ -29,6 +29,7 @@ func TestSetup(t *testing.T) { shouldErr: false, config: login.Config{ JwtSecret: "jwtsecret", + JwtAlgo: "HS512", JwtExpiry: 24 * time.Hour, SuccessURL: "/", Redirect: true, @@ -49,6 +50,7 @@ func TestSetup(t *testing.T) { input: `login { success_url successurl jwt_expiry 42h + jwt_algo algo login_path /foo/bar redirect true redirect_query_parameter comingFrom @@ -64,6 +66,7 @@ func TestSetup(t *testing.T) { shouldErr: false, config: login.Config{ JwtSecret: "jwtsecret", + JwtAlgo: "algo", JwtExpiry: 42 * time.Hour, SuccessURL: "successurl", Redirect: true, @@ -99,6 +102,7 @@ func TestSetup(t *testing.T) { shouldErr: false, config: login.Config{ JwtSecret: "jwtsecret", + JwtAlgo: "HS512", JwtExpiry: 24 * time.Hour, SuccessURL: "/", Redirect: true, @@ -126,6 +130,7 @@ func TestSetup(t *testing.T) { shouldErr: false, config: login.Config{ JwtSecret: "jwtsecret", + JwtAlgo: "HS512", JwtExpiry: 24 * time.Hour, SuccessURL: "/", Redirect: true, @@ -151,6 +156,7 @@ func TestSetup(t *testing.T) { shouldErr: false, config: login.Config{ JwtSecret: "jwtsecret", + JwtAlgo: "HS512", JwtExpiry: 24 * time.Hour, SuccessURL: "/", Redirect: true, diff --git a/login/config.go b/login/config.go @@ -27,6 +27,7 @@ func DefaultConfig() *Config { Port: "6789", LogLevel: "info", JwtSecret: jwtDefaultSecret, + JwtAlgo: "HS512", JwtExpiry: 24 * time.Hour, JwtRefreshes: 0, SuccessURL: "/", @@ -53,6 +54,7 @@ type Config struct { LogLevel string TextLogging bool JwtSecret string + JwtAlgo string JwtExpiry time.Duration JwtRefreshes int SuccessURL string @@ -105,6 +107,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.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/config_test.go b/login/config_test.go @@ -27,6 +27,7 @@ func TestConfig_ReadConfig(t *testing.T) { "--log-level=loglevel", "--text-logging=true", "--jwt-secret=jwtsecret", + "--jwt-algo=algo", "--jwt-expiry=42h42m", "--success-url=successurl", "--redirect=false", @@ -52,6 +53,7 @@ func TestConfig_ReadConfig(t *testing.T) { LogLevel: "loglevel", TextLogging: true, JwtSecret: "jwtsecret", + JwtAlgo: "algo", JwtExpiry: 42*time.Hour + 42*time.Minute, SuccessURL: "successurl", Redirect: false, @@ -89,6 +91,7 @@ func TestConfig_ReadConfigFromEnv(t *testing.T) { NoError(t, os.Setenv("LOGINSRV_LOG_LEVEL", "loglevel")) NoError(t, os.Setenv("LOGINSRV_TEXT_LOGGING", "true")) NoError(t, os.Setenv("LOGINSRV_JWT_SECRET", "jwtsecret")) + NoError(t, os.Setenv("LOGINSRV_JWT_ALGO", "algo")) NoError(t, os.Setenv("LOGINSRV_JWT_EXPIRY", "42h42m")) NoError(t, os.Setenv("LOGINSRV_SUCCESS_URL", "successurl")) NoError(t, os.Setenv("LOGINSRV_REDIRECT", "false")) @@ -112,6 +115,7 @@ func TestConfig_ReadConfigFromEnv(t *testing.T) { LogLevel: "loglevel", TextLogging: true, JwtSecret: "jwtsecret", + JwtAlgo: "algo", JwtExpiry: 42*time.Hour + 42*time.Minute, SuccessURL: "successurl", Redirect: false, diff --git a/login/handler.go b/login/handler.go @@ -2,7 +2,6 @@ package login import ( "encoding/json" - "errors" "fmt" "io/ioutil" "net/http" @@ -10,6 +9,7 @@ import ( "time" "github.com/dgrijalva/jwt-go" + "github.com/pkg/errors" "github.com/tarent/loginsrv/logging" "github.com/tarent/loginsrv/model" "github.com/tarent/loginsrv/oauth2" @@ -22,9 +22,12 @@ const contentTypePlain = "text/plain" // Handler is the mail login handler. // It serves the login ressource and does the authentication against the backends or oauth provider. type Handler struct { - backends []Backend - oauth oauthManager - config *Config + backends []Backend + oauth oauthManager + config *Config + signingMethod jwt.SigningMethod + signingKey interface{} + signingVerifyKey interface{} } // NewHandler creates a login handler based on the supplied configuration. @@ -246,8 +249,12 @@ func (h *Handler) respondAuthenticated(w http.ResponseWriter, r *http.Request, u } func (h *Handler) createToken(userInfo jwt.Claims) (string, error) { - token := jwt.NewWithClaims(jwt.SigningMethodHS512, userInfo) - return token.SignedString([]byte(h.config.JwtSecret)) + signingMethod, key, _, err := h.signingInfo() + if err != nil { + return "", err + } + token := jwt.NewWithClaims(signingMethod, userInfo) + return token.SignedString(key) } func (h *Handler) GetToken(r *http.Request) (userInfo model.UserInfo, valid bool) { @@ -257,7 +264,8 @@ func (h *Handler) GetToken(r *http.Request) (userInfo model.UserInfo, valid bool } token, err := jwt.ParseWithClaims(c.Value, &model.UserInfo{}, func(*jwt.Token) (interface{}, error) { - return []byte(h.config.JwtSecret), nil + _, _, verifyKey, err := h.signingInfo() + return verifyKey, err }) if err != nil { return model.UserInfo{}, false @@ -271,6 +279,34 @@ func (h *Handler) GetToken(r *http.Request) (userInfo model.UserInfo, valid bool return *u, u.Valid() == nil } +func (h *Handler) signingInfo() (signingMethod jwt.SigningMethod, key, verifyKey interface{}, err error) { + if h.signingMethod == nil || h.signingKey == nil || h.signingVerifyKey == nil { + h.signingMethod = jwt.GetSigningMethod(h.config.JwtAlgo) + if h.signingMethod == nil { + return nil, nil, nil, errors.New("invalid signing method: " + h.config.JwtAlgo) + } + + keyString := h.config.JwtSecret + switch h.config.JwtAlgo { + case "ES256", "ES384", "ES512": + if !strings.Contains(string(keyString), "-----") { + keyString = "-----BEGIN EC PRIVATE KEY-----\n" + keyString + "\n-----END EC PRIVATE KEY-----" + } + + key, err := jwt.ParseECPrivateKeyFromPEM([]byte(keyString)) + if err != nil { + return nil, nil, nil, errors.Wrap(err, "can not parse PEM formated EC private key") + } + h.signingKey = key + h.signingVerifyKey = key.Public() + default: + h.signingKey = []byte(keyString) + h.signingVerifyKey = h.signingKey + } + } + return h.signingMethod, h.signingKey, h.signingVerifyKey, nil +} + func (h *Handler) respondError(w http.ResponseWriter, r *http.Request) { if wantHTML(r) { username, _, _ := getCredentials(r) diff --git a/login/handler_test.go b/login/handler_test.go @@ -399,6 +399,21 @@ func TestHandler_getToken_Valid(t *testing.T) { Equal(t, input, userInfo) } +func TestHandler_signAndVerify_ES256(t *testing.T) { + h := testHandler() + h.config.JwtAlgo = "ES256" + h.config.JwtSecret = "MHcCAQEEIJKMecdA9ASkZArOu9b+cPmSiVfQaaeErHcvkqG2gVIOoAoGCCqGSM49AwEHoUQDQgAE1gae9/zJDLHeuFteUkKgVhLrwJPoA43goNacgwldOucBvVUzD0EFAcpCR+0UcOfQ99CxUyKxWtnvr9xpDIXU0w==" + 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"}