Added -cookie-expiry, -cookie-domain, -jwt-expiry parameters

This commit is contained in:
Sebastian Mancke
2017-05-13 14:02:11 +02:00
parent dd9284bc07
commit 2c76f02671
6 changed files with 188 additions and 36 deletions
+23 -17
View File
@@ -38,23 +38,29 @@ For questions and support please use the [Gitter chat room](https://gitter.im/ta
## Configuration and Startup
### Config Options
| Parameter | Type | Description | Default |
|-|-|-|-|
| -cookie-http-only | boolean | Set the cookie with the http only flag | true |
| -cookie-name | string | The name of the jwt cookie | "jwt_token" |
| -github | value | Oauth config in the form: client_id=..,client_secret=..[,scope=..,][redirect_uri=..] | |
| -host | string | The host to listen on | "localhost" |
| -htpasswd | value | Htpasswd login backend opts: file=/path/to/pwdfile | |
| -jwt-secret | string | The secret to sign the jwt token | "random key" |
| -log-level | string | The log level | "info" |
| -login-path | string | The path of the login resource | "/login" |
| -logout-url | string | The url or path to redirect after logout | |
| -osiam | value | Osiam login backend opts: endpoint=..,client_id=..,client_secret=.. | |
| -port | string | The port to listen on | "6789" |
| -simple | value | Simple login backend opts: user1=password,user2=password,.. | |
| -success-url | string | The url to redirect after login | "/" |
| -template | string | An alternative template for the login form | |
| -text-logging | boolean | Log in text format instead of json | true |
_Note for caddy users_: Not all parameters are available in caddy. See the table for details. Incaddy, the parameter names can be also be used with `_` in the names, e.g. `cookie_http_only`.
| Parameter | Type | Default | Caddy | Description |
|-------------------|-------------|--------------|-------|--------------------------------------------------------------------------------------|
| -cookie-domain | string | | X | The optional domain parameter for the cookie |
| -cookie-expiry | string | session | X | The expiry duration for the cookie, e.g. 2h or 3h30m |
| -cookie-http-only | boolean | true | X | Set the cookie with the http only flag |
| -cookie-name | string | "jwt_token" | X | The name of the jwt cookie |
| -github | value | | X | Oauth config in the form: client_id=..,client_secret=..[,scope=..,][redirect_uri=..] |
| -host | string | "localhost" | - | The host to listen on |
| -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 |
| -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 |
| -osiam | value | | X | OSIAM login backend opts: endpoint=..,client_id=..,client_secret=.. |
| -port | string | "6789" | - | The port to listen on |
| -simple | value | | X | Simple login backend opts: user1=password,user2=password,.. |
| -success-url | string | "/" | X | The url to redirect after login |
| -template | string | | X | An alternative template for the login form |
| -text-logging | boolean | true | - | Log in text format instead of json |
### Environment Variables
All of the above Config Options can also be applied as environment variable, where the name is written in the way: `LOGINSRV_OPTION_NAME`.
+10
View File
@@ -8,6 +8,7 @@ import (
"io/ioutil"
"os"
"testing"
"time"
)
func TestSetup(t *testing.T) {
@@ -26,6 +27,7 @@ func TestSetup(t *testing.T) {
shouldErr: false,
config: login.Config{
JwtSecret: "jwtsecret",
JwtExpiry: 24 * time.Hour,
SuccessUrl: "/",
LoginPath: "/login",
CookieName: "jwt_token",
@@ -40,18 +42,24 @@ func TestSetup(t *testing.T) {
{
input: `loginsrv {
success_url successurl
jwt_expiry 42h
login_path /foo/bar
cookie_name cookiename
cookie_http_only false
cookie_domain example.com
cookie_expiry 23h23m
simple bob=secret
osiam endpoint=http://localhost:8080,client_id=example-client,client_secret=secret
}`,
shouldErr: false,
config: login.Config{
JwtSecret: "jwtsecret",
JwtExpiry: 42 * time.Hour,
SuccessUrl: "successurl",
LoginPath: "/foo/bar",
CookieName: "cookiename",
CookieDomain: "example.com",
CookieExpiry: 23*time.Hour + 23*time.Minute,
CookieHttpOnly: false,
Backends: login.Options{
"simple": map[string]string{
@@ -76,6 +84,7 @@ func TestSetup(t *testing.T) {
shouldErr: false,
config: login.Config{
JwtSecret: "jwtsecret",
JwtExpiry: 24 * time.Hour,
SuccessUrl: "/",
LoginPath: "/context/login",
CookieName: "cookiename",
@@ -98,6 +107,7 @@ func TestSetup(t *testing.T) {
shouldErr: false,
config: login.Config{
JwtSecret: "jwtsecret",
JwtExpiry: 24 * time.Hour,
SuccessUrl: "/",
LoginPath: "/login",
CookieName: "cookiename",
+7
View File
@@ -25,6 +25,7 @@ func DefaultConfig() *Config {
Port: "6789",
LogLevel: "info",
JwtSecret: jwtDefaultSecret,
JwtExpiry: 24 * time.Hour,
SuccessUrl: "/",
LogoutUrl: "",
LoginPath: "/login",
@@ -43,11 +44,14 @@ type Config struct {
LogLevel string
TextLogging bool
JwtSecret string
JwtExpiry time.Duration
SuccessUrl string
LogoutUrl string
Template string
LoginPath string
CookieName string
CookieExpiry time.Duration
CookieDomain string
CookieHttpOnly bool
Backends Options
Oauth Options
@@ -86,8 +90,11 @@ 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", "random key", "The secret to sign the jwt token")
f.DurationVar(&c.JwtExpiry, "jwt-expiry", c.JwtExpiry, "The expiry duration for the jwt token, e.g. 2h or 3h30m")
f.StringVar(&c.CookieName, "cookie-name", c.CookieName, "The name of the jwt cookie")
f.BoolVar(&c.CookieHttpOnly, "cookie-http-only", c.CookieHttpOnly, "Set the cookie with the http only flag")
f.DurationVar(&c.CookieExpiry, "cookie-expiry", c.CookieExpiry, "The expiry duration for the cookie, e.g. 2h or 3h30m. Default is browser session")
f.StringVar(&c.CookieDomain, "cookie-domain", c.CookieDomain, "The optional domain parameter for the cookie")
f.StringVar(&c.SuccessUrl, "success-url", c.SuccessUrl, "The url to redirect after login")
f.StringVar(&c.LogoutUrl, "logout-url", c.LogoutUrl, "The url or path to redirect after logout")
f.StringVar(&c.Template, "template", c.Template, "An alternative template for the login form")
+13
View File
@@ -5,6 +5,7 @@ import (
"github.com/stretchr/testify/assert"
"os"
"testing"
"time"
)
func TestConfig_ReadConfigDefaults(t *testing.T) {
@@ -25,11 +26,14 @@ func TestConfig_ReadConfig(t *testing.T) {
"--log-level=loglevel",
"--text-logging=true",
"--jwt-secret=jwtsecret",
"--jwt-expiry=42h42m",
"--success-url=successurl",
"--logout-url=logouturl",
"--template=template",
"--login-path=loginpath",
"--cookie-name=cookiename",
"--cookie-expiry=23m",
"--cookie-domain=*.example.com",
"--cookie-http-only=false",
"--backend=provider=simple",
"--backend=provider=foo",
@@ -42,11 +46,14 @@ func TestConfig_ReadConfig(t *testing.T) {
LogLevel: "loglevel",
TextLogging: true,
JwtSecret: "jwtsecret",
JwtExpiry: 42*time.Hour + 42*time.Minute,
SuccessUrl: "successurl",
LogoutUrl: "logouturl",
Template: "template",
LoginPath: "loginpath",
CookieName: "cookiename",
CookieExpiry: 23 * time.Minute,
CookieDomain: "*.example.com",
CookieHttpOnly: false,
Backends: Options{
"simple": map[string]string{},
@@ -71,11 +78,14 @@ func TestConfig_ReadConfigFromEnv(t *testing.T) {
assert.NoError(t, os.Setenv("LOGINSRV_LOG_LEVEL", "loglevel"))
assert.NoError(t, os.Setenv("LOGINSRV_TEXT_LOGGING", "true"))
assert.NoError(t, os.Setenv("LOGINSRV_JWT_SECRET", "jwtsecret"))
assert.NoError(t, os.Setenv("LOGINSRV_JWT_EXPIRY", "42h42m"))
assert.NoError(t, os.Setenv("LOGINSRV_SUCCESS_URL", "successurl"))
assert.NoError(t, os.Setenv("LOGINSRV_LOGOUT_URL", "logouturl"))
assert.NoError(t, os.Setenv("LOGINSRV_TEMPLATE", "template"))
assert.NoError(t, os.Setenv("LOGINSRV_LOGIN_PATH", "loginpath"))
assert.NoError(t, os.Setenv("LOGINSRV_COOKIE_NAME", "cookiename"))
assert.NoError(t, os.Setenv("LOGINSRV_COOKIE_EXPIRY", "23m"))
assert.NoError(t, os.Setenv("LOGINSRV_COOKIE_DOMAIN", "*.example.com"))
assert.NoError(t, os.Setenv("LOGINSRV_COOKIE_HTTP_ONLY", "false"))
assert.NoError(t, os.Setenv("LOGINSRV_SIMPLE", "foo=bar"))
assert.NoError(t, os.Setenv("LOGINSRV_GITHUB", "client_id=foo,client_secret=bar"))
@@ -86,11 +96,14 @@ func TestConfig_ReadConfigFromEnv(t *testing.T) {
LogLevel: "loglevel",
TextLogging: true,
JwtSecret: "jwtsecret",
JwtExpiry: 42*time.Hour + 42*time.Minute,
SuccessUrl: "successurl",
LogoutUrl: "logouturl",
Template: "template",
LoginPath: "loginpath",
CookieName: "cookiename",
CookieExpiry: 23 * time.Minute,
CookieDomain: "*.example.com",
CookieHttpOnly: false,
Backends: Options{
"simple": map[string]string{
+10 -1
View File
@@ -173,11 +173,14 @@ func (h *Handler) deleteToken(w http.ResponseWriter) {
Expires: time.Unix(0, 0),
Path: "/",
}
if h.config.CookieDomain != "" {
cookie.Domain = h.config.CookieDomain
}
http.SetCookie(w, cookie)
}
func (h *Handler) respondAuthenticated(w http.ResponseWriter, r *http.Request, userInfo model.UserInfo) {
userInfo.Expiry = time.Now().Add(time.Hour * 24).Unix()
userInfo.Expiry = time.Now().Add(h.config.JwtExpiry).Unix()
token, err := h.createToken(userInfo)
if err != nil {
logging.Application(r.Header).WithError(err).Error()
@@ -191,6 +194,12 @@ func (h *Handler) respondAuthenticated(w http.ResponseWriter, r *http.Request, u
HttpOnly: h.config.CookieHttpOnly,
Path: "/",
}
if h.config.CookieExpiry != 0 {
cookie.Expires = time.Now().Add(h.config.CookieExpiry)
}
if h.config.CookieDomain != "" {
cookie.Domain = h.config.CookieDomain
}
http.SetCookie(w, cookie)
w.Header().Set("Location", h.config.SuccessUrl)
w.WriteHeader(303)
+125 -18
View File
@@ -9,8 +9,10 @@ import (
"github.com/tarent/loginsrv/oauth2"
"net/http"
"net/http/httptest"
"strconv"
"strings"
"testing"
"time"
)
const TypeJson = "Content-Type: application/json"
@@ -18,6 +20,14 @@ const TypeForm = "Content-Type: application/x-www-form-urlencoded"
const AcceptHtml = "Accept: text/html"
const AcceptJwt = "Accept: application/jwt"
func testConfig() *Config {
testConfig := DefaultConfig()
testConfig.LoginPath = "/context/login"
testConfig.CookieDomain = "example.com"
testConfig.CookieExpiry = 23 * time.Hour
return testConfig
}
func TestHandler_NewFromConfig(t *testing.T) {
testCases := []struct {
@@ -121,7 +131,8 @@ func TestHandler_LoginJson(t *testing.T) {
// verify the token
claims, err := tokenAsMap(recorder.Body.String())
assert.NoError(t, err)
assert.Equal(t, map[string]interface{}{"sub": "bob"}, claims)
assert.Equal(t, "bob", claims["sub"])
assert.InDelta(t, time.Now().Add(DefaultConfig().JwtExpiry).Unix(), claims["exp"], 2)
// wrong credentials
recorder = call(req("POST", "/context/login", `{"username": "bob", "password": "FOOOBAR"}`, TypeJson, AcceptJwt))
@@ -203,14 +214,21 @@ func TestHandler_LoginWeb(t *testing.T) {
assert.Equal(t, "/", recorder.Header().Get("Location"))
// verify the token from the cookie
assert.Contains(t, recorder.Header().Get("Set-Cookie"), "jwt_token=")
headerParts := strings.SplitN(recorder.Header().Get("Set-Cookie"), "=", 2)
assert.Equal(t, 2, len(headerParts))
assert.Equal(t, headerParts[0], "jwt_token")
claims, err := tokenAsMap(strings.SplitN(headerParts[1], ";", 2)[0])
setCookieList := readSetCookies(recorder.Header())
assert.Equal(t, 1, len(setCookieList))
cookie := setCookieList[0]
assert.Equal(t, "jwt_token", cookie.Name)
assert.Equal(t, "/", cookie.Path)
assert.Equal(t, "example.com", cookie.Domain)
assert.InDelta(t, time.Now().Add(testConfig().CookieExpiry).Unix(), cookie.Expires.Unix(), 2)
assert.True(t, cookie.HttpOnly)
// check the token contens
claims, err := tokenAsMap(cookie.Value)
assert.NoError(t, err)
assert.Equal(t, map[string]interface{}{"sub": "bob"}, claims)
assert.Contains(t, headerParts[1]+";", "Path=/;")
assert.Equal(t, "bob", claims["sub"])
assert.InDelta(t, time.Now().Add(DefaultConfig().JwtExpiry).Unix(), claims["exp"], 2)
// show the login form again after authentication failed
recorder = call(req("POST", "/context/login", "username=bob&password=FOOBAR", TypeForm, AcceptHtml))
@@ -223,21 +241,32 @@ func TestHandler_Logout(t *testing.T) {
// DELETE
recorder := call(req("DELETE", "/context/login", ""))
assert.Equal(t, 200, recorder.Code)
assert.Contains(t, recorder.Header().Get("Set-Cookie"), "jwt_token=delete; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT;")
checkDeleteCookei(t, recorder.Header())
// GET + param
recorder = call(req("GET", "/context/login?logout=true", ""))
assert.Equal(t, 200, recorder.Code)
assert.Contains(t, recorder.Header().Get("Set-Cookie"), "jwt_token=delete; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT;")
checkDeleteCookei(t, recorder.Header())
// POST + param
recorder = call(req("POST", "/context/login", "logout=true", TypeForm))
assert.Equal(t, 200, recorder.Code)
assert.Contains(t, recorder.Header().Get("Set-Cookie"), "jwt_token=delete; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT;")
checkDeleteCookei(t, recorder.Header())
assert.Equal(t, "no-cache, no-store, must-revalidate", recorder.Header().Get("Cache-Control"))
}
func checkDeleteCookei(t *testing.T, h http.Header) {
setCookieList := readSetCookies(h)
assert.Equal(t, 1, len(setCookieList))
cookie := setCookieList[0]
assert.Equal(t, "jwt_token", cookie.Name)
assert.Equal(t, "/", cookie.Path)
assert.Equal(t, "example.com", cookie.Domain)
assert.Equal(t, int64(0), cookie.Expires.Unix())
}
func TestHandler_CustomLogoutUrl(t *testing.T) {
cfg := DefaultConfig()
cfg.LogoutUrl = "http://example.com"
@@ -322,26 +351,22 @@ func TestHandler_getToken_InvalidNoToken(t *testing.T) {
}
func testHandler() *Handler {
cfg := DefaultConfig()
cfg.LoginPath = "/context/login"
return &Handler{
backends: []Backend{
NewSimpleBackend(map[string]string{"bob": "secret"}),
},
oauth: oauth2.NewManager(),
config: cfg,
config: testConfig(),
}
}
func testHandlerWithError() *Handler {
cfg := DefaultConfig()
cfg.LoginPath = "/context/login"
return &Handler{
backends: []Backend{
errorTestBackend("test error"),
},
oauth: oauth2.NewManager(),
config: cfg,
config: testConfig(),
}
}
@@ -375,7 +400,7 @@ func tokenAsMap(tokenString string) (map[string]interface{}, error) {
if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
return map[string]interface{}(claims), nil
}
return nil, errors.New("token not valid")
}
@@ -408,3 +433,85 @@ func (m *oauth2ManagerMock) AddConfig(providerName string, opts map[string]strin
func (m *oauth2ManagerMock) GetConfigFromRequest(r *http.Request) (oauth2.Config, error) {
return m._GetConfigFromRequest(r)
}
// copied from golang: net/http/cookie.go
// with simple some simplification fro edge cases
// readSetCookies parses all "Set-Cookie" values from
// the header h and returns the successfully parsed Cookies.
func readSetCookies(h http.Header) []*http.Cookie {
cookieCount := len(h["Set-Cookie"])
if cookieCount == 0 {
return []*http.Cookie{}
}
cookies := make([]*http.Cookie, 0, cookieCount)
for _, line := range h["Set-Cookie"] {
parts := strings.Split(strings.TrimSpace(line), ";")
if len(parts) == 1 && parts[0] == "" {
continue
}
parts[0] = strings.TrimSpace(parts[0])
j := strings.Index(parts[0], "=")
if j < 0 {
continue
}
name, value := parts[0][:j], parts[0][j+1:]
c := &http.Cookie{
Name: name,
Value: value,
Raw: line,
}
for i := 1; i < len(parts); i++ {
parts[i] = strings.TrimSpace(parts[i])
if len(parts[i]) == 0 {
continue
}
attr, val := parts[i], ""
if j := strings.Index(attr, "="); j >= 0 {
attr, val = attr[:j], attr[j+1:]
}
lowerAttr := strings.ToLower(attr)
switch lowerAttr {
case "secure":
c.Secure = true
continue
case "httponly":
c.HttpOnly = true
continue
case "domain":
c.Domain = val
continue
case "max-age":
secs, err := strconv.Atoi(val)
if err != nil || secs != 0 && val[0] == '0' {
break
}
if secs <= 0 {
secs = -1
}
c.MaxAge = secs
continue
case "expires":
c.RawExpires = val
exptime, err := time.Parse(time.RFC1123, val)
if err != nil {
exptime, err = time.Parse("Mon, 02-Jan-2006 15:04:05 MST", val)
if err != nil {
c.Expires = time.Time{}
break
}
}
c.Expires = exptime.UTC()
continue
case "path":
c.Path = val
continue
}
c.Unparsed = append(c.Unparsed, parts[i])
}
cookies = append(cookies, c)
}
return cookies
}