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 40a67d8e54e0732830554336f7cec8c90828ce94
parent 5824a99abca1e279fbb09f9767ca7c89061b16cf
Author: Sebastian Mancke <s.mancke@tarent.de>
Date:   Sat, 19 Nov 2016 15:01:25 +0100

enhanced test coverage

Diffstat:
Mlogin/config.go | 101++++++++++++++++++++++++++++++++++++++++++++++---------------------------------
Mlogin/config_test.go | 47+++++++++++++++++++++++++++++++++++++++++++++++
Mlogin/handler.go | 14++++++++++++--
Mlogin/handler_test.go | 51++++++++++++++++++++++++++++++++++++++++++++++++---
Mlogin/login_form.go | 2+-
Mlogin/provider.go | 7-------
Mlogin/simple_backend.go | 7+++++++
7 files changed, 174 insertions(+), 55 deletions(-)

diff --git a/login/config.go b/login/config.go @@ -5,29 +5,25 @@ import ( "fmt" "github.com/caarlos0/env" "math/rand" + "os" "strings" "time" ) -const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" +var DefaultConfig Config func init() { rand.Seed(time.Now().UTC().UnixNano()) -} - -type BackendOptions []map[string]string - -func (bo *BackendOptions) String() string { - return fmt.Sprintf("%v", *bo) -} - -func (bo *BackendOptions) Set(value string) error { - optionMap, err := parseBackendOptions(value) - if err != nil { - return err + DefaultConfig = Config{ + Host: "localhost", + Port: "6789", + LogLevel: "info", + JwtSecret: randStringBytes(32), + SuccessUrl: "/", + CookieName: "jwt_token", + CookieHttpOnly: true, + Backends: BackendOptions{}, } - *bo = append(*bo, optionMap) - return nil } type Config struct { @@ -42,35 +38,39 @@ type Config struct { Backends BackendOptions } -func DefaultConfig() *Config { - return &Config{ - Host: "localhost", - Port: "6789", - LogLevel: "info", - JwtSecret: randStringBytes(32), - SuccessUrl: "/", - CookieName: "jwt_token", - CookieHttpOnly: true, - Backends: BackendOptions{}, +func ReadConfig() *Config { + c, err := readConfig(flag.CommandLine, os.Args[1:]) + if err != nil { + // should never happen, because of flag default policy ExitOnError + panic(err) } + return c } -func ReadConfig() *Config { - config := DefaultConfig() - - env.Parse(config) - - flag.StringVar(&config.Host, "host", config.Host, "The host to listen on") - flag.StringVar(&config.Port, "port", config.Port, "The port to listen on") - flag.StringVar(&config.LogLevel, "log-level", config.LogLevel, "The log level") - flag.BoolVar(&config.TextLogging, "text-logging", config.TextLogging, "Log in text format instead of json") - flag.StringVar(&config.JwtSecret, "jwt-secret", "random key", "The secret to sign the jwt token") - flag.StringVar(&config.CookieName, "cookie-name", config.CookieName, "The name of the jwt cookie") - flag.BoolVar(&config.CookieHttpOnly, "cookie-http-only", config.CookieHttpOnly, "Set the cookie with the http only flag") - flag.StringVar(&config.SuccessUrl, "success-url", config.SuccessUrl, "The url to redirect after login") - flag.Var(&config.Backends, "backend", "Backend configuration in form 'provider=name,key=val,key=...', can be declared multiple times") - - flag.Parse() - return config +func readConfig(f *flag.FlagSet, args []string) (*Config, error) { + config := DefaultConfig + + env.Parse(&config) + + f.StringVar(&config.Host, "host", config.Host, "The host to listen on") + f.StringVar(&config.Port, "port", config.Port, "The port to listen on") + f.StringVar(&config.LogLevel, "log-level", config.LogLevel, "The log level") + f.BoolVar(&config.TextLogging, "text-logging", config.TextLogging, "Log in text format instead of json") + f.StringVar(&config.JwtSecret, "jwt-secret", "random key", "The secret to sign the jwt token") + f.StringVar(&config.CookieName, "cookie-name", config.CookieName, "The name of the jwt cookie") + f.BoolVar(&config.CookieHttpOnly, "cookie-http-only", config.CookieHttpOnly, "Set the cookie with the http only flag") + f.StringVar(&config.SuccessUrl, "success-url", config.SuccessUrl, "The url to redirect after login") + f.Var(&config.Backends, "backend", "Backend configuration in form 'provider=name,key=val,key=...', can be declared multiple times") + + err := f.Parse(args) + if err != nil { + return nil, err + } + + if config.JwtSecret == "random key" { + config.JwtSecret = DefaultConfig.JwtSecret + } + + return &config, err } func parseBackendOptions(b string) (map[string]string, error) { @@ -89,6 +89,8 @@ func parseBackendOptions(b string) (map[string]string, error) { return opts, nil } +const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + func randStringBytes(n int) string { b := make([]byte, n) for i := range b { @@ -96,3 +98,18 @@ func randStringBytes(n int) string { } return string(b) } + +type BackendOptions []map[string]string + +func (bo *BackendOptions) String() string { + return fmt.Sprintf("%v", *bo) +} + +func (bo *BackendOptions) Set(value string) error { + optionMap, err := parseBackendOptions(value) + if err != nil { + return err + } + *bo = append(*bo, optionMap) + return nil +} diff --git a/login/config_test.go b/login/config_test.go @@ -1,11 +1,58 @@ package login import ( + "flag" "fmt" "github.com/stretchr/testify/assert" + "os" "testing" ) +func TestConfig_ReadConfigDefaults(t *testing.T) { + originalArgs := os.Args + defer func() { os.Args = originalArgs }() + + assert.Equal(t, &DefaultConfig, ReadConfig()) +} + +func TestConfig_ReadConfig(t *testing.T) { + input := []string{ + "--host=host", + "--port=port", + "--log-level=loglevel", + "--text-logging=true", + "--jwt-secret=jwtsecret", + "--success-url=successurl", + "--cookie-name=cookiename", + "--cookie-http-only=false", + "--backend=provider=simple", + "--backend=provider=foo", + } + + expected := &Config{ + Host: "host", + Port: "port", + LogLevel: "loglevel", + TextLogging: true, + JwtSecret: "jwtsecret", + SuccessUrl: "successurl", + CookieName: "cookiename", + CookieHttpOnly: false, + Backends: BackendOptions{ + map[string]string{ + "provider": "simple", + }, + map[string]string{ + "provider": "foo", + }, + }, + } + + cfg, err := readConfig(flag.NewFlagSet("", flag.ContinueOnError), input) + assert.NoError(t, err) + assert.Equal(t, expected, cfg) +} + func TestConfig_ParseBackendOptions(t *testing.T) { testCases := []struct { input []string diff --git a/login/handler.go b/login/handler.go @@ -11,6 +11,10 @@ import ( "strings" ) +const ContentTypeHtml = "text/html; charset=utf-8" +const ContentTypeJWT = "application/jwt" +const ContentTypePlain = "text/plain" + type Handler struct { backends []Backend config *Config @@ -123,18 +127,20 @@ func (h *Handler) respondAuthenticated(w http.ResponseWriter, r *http.Request, u return } - w.Header().Set("Content-Type", "application/jwt") + w.Header().Set("Content-Type", ContentTypeJWT) w.WriteHeader(200) fmt.Fprintf(w, "%s\n", token) } func (h *Handler) createToken(userInfo UserInfo) (string, error) { - token := jwt.NewWithClaims(jwt.SigningMethodHS256, userInfo) + token := jwt.NewWithClaims(jwt.SigningMethodHS512, userInfo) return token.SignedString([]byte(h.config.JwtSecret)) } func (h *Handler) respondError(w http.ResponseWriter, r *http.Request) { if wantHtml(r) { + w.Header().Set("Content-Type", ContentTypeHtml) + w.WriteHeader(500) username, _, _ := getCredentials(r) writeLoginForm(w, map[string]interface{}{ @@ -145,6 +151,7 @@ func (h *Handler) respondError(w http.ResponseWriter, r *http.Request) { }) return } + w.Header().Set("Content-Type", ContentTypePlain) w.WriteHeader(500) fmt.Fprintf(w, "Internal Server Error") } @@ -156,6 +163,8 @@ func (h *Handler) respondBadRequest(w http.ResponseWriter, r *http.Request) { func (h *Handler) respondAuthFailure(w http.ResponseWriter, r *http.Request) { if wantHtml(r) { + w.Header().Set("Content-Type", ContentTypeHtml) + w.WriteHeader(403) username, _, _ := getCredentials(r) writeLoginForm(w, map[string]interface{}{ @@ -167,6 +176,7 @@ func (h *Handler) respondAuthFailure(w http.ResponseWriter, r *http.Request) { }) return } + w.Header().Set("Content-Type", ContentTypePlain) w.WriteHeader(403) fmt.Fprintf(w, "Wrong credentials") } diff --git a/login/handler_test.go b/login/handler_test.go @@ -1,6 +1,7 @@ package login import ( + "errors" "fmt" "github.com/stretchr/testify/assert" "net/http" @@ -28,6 +29,12 @@ func TestHandler_NewFromConfig(t *testing.T) { }, // error cases { + // init error because no users are provided + &Config{Backends: BackendOptions{map[string]string{"provider": "simple"}}}, + 1, + true, + }, + { &Config{}, 0, true, @@ -96,21 +103,53 @@ func TestHandler_LoginWeb(t *testing.T) { assert.Contains(t, recorder.Header().Get("Set-Cookie"), "jwt_token=") assert.Equal(t, "/", recorder.Header().Get("Location")) - // show the login after error + // show the login form again after authentication failed recorder = call(req("POST", "/context/login", "username=bob&password=FOOBAR", TypeForm, AcceptHtml)) - assert.Equal(t, 200, recorder.Code) + assert.Equal(t, 403, recorder.Code) assert.Contains(t, recorder.Body.String(), "form") assert.Contains(t, recorder.Body.String(), `method="POST"`) assert.Contains(t, recorder.Body.String(), `action="/context/login"`) assert.Equal(t, recorder.Header().Get("Set-Cookie"), "") } +func TestHandler_LoginError(t *testing.T) { + h := testHandlerWithError() + + // backend returning an error with result type == jwt + request := req("POST", "/context/login", `{"username": "bob", "password": "secret"}`, TypeJson, AcceptJwt) + recorder := httptest.NewRecorder() + h.ServeHTTP(recorder, request) + + assert.Equal(t, 500, recorder.Code) + assert.Equal(t, recorder.Header().Get("Content-Type"), "text/plain") + assert.Equal(t, recorder.Body.String(), "Internal Server Error") + + // backend returning an error with result type == html + request = req("POST", "/context/login", `{"username": "bob", "password": "secret"}`, TypeJson, AcceptHtml) + recorder = httptest.NewRecorder() + h.ServeHTTP(recorder, request) + + assert.Equal(t, 500, recorder.Code) + assert.Contains(t, recorder.Header().Get("Content-Type"), "text/html") + assert.Contains(t, recorder.Body.String(), "form") + assert.Contains(t, recorder.Body.String(), "Internal Error") +} + func testHandler() *Handler { return &Handler{ backends: []Backend{ NewSimpleBackend(map[string]string{"bob": "secret"}), }, - config: DefaultConfig(), + config: &DefaultConfig, + } +} + +func testHandlerWithError() *Handler { + return &Handler{ + backends: []Backend{ + errorTestBackend("test error"), + }, + config: &DefaultConfig, } } @@ -132,3 +171,9 @@ func req(method string, url string, body string, header ...string) *http.Request } return r } + +type errorTestBackend string + +func (h errorTestBackend) Authenticate(username, password string) (bool, UserInfo, error) { + return false, UserInfo{}, errors.New(string(h)) +} diff --git a/login/login_form.go b/login/login_form.go @@ -23,7 +23,7 @@ const loginForm = `<!DOCTYPE html> <div class="panel panel-default"> <div class="panel-heading"> <h3 class="panel-title">Please sign in</h3> - {{ if .error}}Internal error. Please try again later{{end}} + {{ if .error}}Internal Error. Please try again later{{end}} {{ if .failure}}Wrong credentials{{end}} </div> <div class="panel-body"> diff --git a/login/provider.go b/login/provider.go @@ -18,10 +18,3 @@ func GetProvider(providerName string) (Provider, bool) { p, exist := provider[providerName] return p, exist } - -// GetProviderDescription returns the description of a registered provider by its name. -// The bool return parameter indicated, if there was such a provider. -func GetProviderDescription(providerName string) (*ProviderDescription, bool) { - pd, exist := providerDescription[providerName] - return pd, exist -} diff --git a/login/simple_backend.go b/login/simple_backend.go @@ -1,5 +1,9 @@ package login +import ( + "errors" +) + const SimpleProviderName = "simple" func init() { @@ -17,6 +21,9 @@ func SimpleBackendFactory(config map[string]string) (Backend, error) { userPassword[k] = v } } + if len(userPassword) == 0 { + return nil, errors.New("no users provided for simple backend") + } return NewSimpleBackend(userPassword), nil }