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 52893f34bc8b90be05685313ed4c509ebe346f2a
parent 69e9dc946b49ee9790dfa1e469139e47e4443d4d
Author: Sebastian Mancke <s.mancke@tarent.de>
Date:   Sat, 13 May 2017 22:26:53 +0200

Merge pull request #18 from tarent/expiry-time

Expiry time and additional config ops
Diffstat:
MREADME.md | 42++++++++++++++++++++++++------------------
Mcaddy/setup.go | 8+++++---
Mcaddy/setup_test.go | 79++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------------------
Mlogin/config.go | 7+++++++
Mlogin/config_test.go | 13+++++++++++++
Mlogin/handler.go | 21+++++++++++++++++----
Mlogin/handler_test.go | 145++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------
Mmodel/user_info.go | 9+++++++++
Amodel/user_info_test.go | 13+++++++++++++
9 files changed, 270 insertions(+), 67 deletions(-)

diff --git a/README.md b/README.md @@ -35,26 +35,32 @@ The following providers (login backends) are supported. For questions and support please use the [Gitter chat room](https://gitter.im/tarent/loginsrv). [![Join the chat at https://gitter.im/tarent/loginsrv](https://badges.gitter.im/tarent/loginsrv.svg)](https://gitter.im/tarent/loginsrv?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) - + ## 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`. diff --git a/caddy/setup.go b/caddy/setup.go @@ -85,10 +85,12 @@ func parseConfig(c *caddy.Controller) (*login.Config, error) { f := fs.Lookup(name) if f == nil { - c.ArgErr() - continue + return cfg, c.ArgErr() + } + err := f.Value.Set(value) + if err != nil { + return cfg, c.Err(err.Error()) } - f.Value.Set(value) } return cfg, nil diff --git a/caddy/setup_test.go b/caddy/setup_test.go @@ -1,6 +1,7 @@ package caddy import ( + "fmt" "github.com/mholt/caddy" "github.com/mholt/caddy/caddyhttp/httpserver" "github.com/stretchr/testify/assert" @@ -8,6 +9,7 @@ import ( "io/ioutil" "os" "testing" + "time" ) func TestSetup(t *testing.T) { @@ -20,12 +22,13 @@ func TestSetup(t *testing.T) { config login.Config }{ { //defaults - input: `loginsrv { + input: `login { simple bob=secret }`, shouldErr: false, config: login.Config{ JwtSecret: "jwtsecret", + JwtExpiry: 24 * time.Hour, SuccessUrl: "/", LoginPath: "/login", CookieName: "jwt_token", @@ -38,20 +41,26 @@ func TestSetup(t *testing.T) { Oauth: login.Options{}, }}, { - input: `loginsrv { + input: `login { 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 +85,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 +108,7 @@ func TestSetup(t *testing.T) { shouldErr: false, config: login.Config{ JwtSecret: "jwtsecret", + JwtExpiry: 24 * time.Hour, SuccessUrl: "/", LoginPath: "/login", CookieName: "cookiename", @@ -111,28 +122,50 @@ func TestSetup(t *testing.T) { }}, // error cases - {input: "loginsrv {\n}", shouldErr: true}, - {input: "loginsrv xx yy {\n}", shouldErr: true}, - {input: "loginsrv {\n cookie_http_only 42 \n simple bob=secret \n}", shouldErr: true}, - {input: "loginsrv {\n unknown property \n simple bob=secret \n}", shouldErr: true}, - {input: "loginsrv {\n backend \n}", shouldErr: true}, - {input: "loginsrv {\n backend provider=foo\n}", shouldErr: true}, - {input: "loginsrv {\n backend kk\n}", shouldErr: true}, + { // duration parse error + input: `login { + simple bob=secret + }`, + shouldErr: false, + config: login.Config{ + JwtSecret: "jwtsecret", + JwtExpiry: 24 * time.Hour, + SuccessUrl: "/", + LoginPath: "/login", + CookieName: "jwt_token", + CookieHttpOnly: true, + Backends: login.Options{ + "simple": map[string]string{ + "bob": "secret", + }, + }, + Oauth: login.Options{}, + }}, + {input: "login {\n}", shouldErr: true}, + {input: "login xx yy {\n}", shouldErr: true}, + {input: "login {\n cookie_http_only 42d \n simple bob=secret \n}", shouldErr: true}, + {input: "login {\n unknown property \n simple bob=secret \n}", shouldErr: true}, + {input: "login {\n backend \n}", shouldErr: true}, + {input: "login {\n backend provider=foo\n}", shouldErr: true}, + {input: "login {\n backend kk\n}", shouldErr: true}, } { - c := caddy.NewTestController("http", test.input) - err := setup(c) - if err != nil && !test.shouldErr { - t.Errorf("Test case #%d received an error of %v", j, err) - } else if test.shouldErr { - continue - } - mids := httpserver.GetConfig(c).Middleware() - if len(mids) == 0 { - t.Errorf("no middlewares created in test #%v", j) - continue - } - middleware := mids[len(mids)-1](nil).(*CaddyHandler) - assert.Equal(t, &test.config, middleware.config) + t.Run(fmt.Sprintf("test %v", j), func(t *testing.T) { + c := caddy.NewTestController("http", test.input) + err := setup(c) + if test.shouldErr { + assert.Error(t, err, "test ") + return + } else { + assert.NoError(t, err) + } + mids := httpserver.GetConfig(c).Middleware() + if len(mids) == 0 { + t.Errorf("no middlewares created in test #%v", j) + return + } + middleware := mids[len(mids)-1](nil).(*CaddyHandler) + assert.Equal(t, &test.config, middleware.config) + }) } } diff --git a/login/config.go b/login/config.go @@ -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") diff --git a/login/config_test.go b/login/config_test.go @@ -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{ diff --git a/login/handler.go b/login/handler.go @@ -173,10 +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 jwt.Claims) { +func (h *Handler) respondAuthenticated(w http.ResponseWriter, r *http.Request, userInfo model.UserInfo) { + userInfo.Expiry = time.Now().Add(h.config.JwtExpiry).Unix() token, err := h.createToken(userInfo) if err != nil { logging.Application(r.Header).WithError(err).Error() @@ -184,13 +188,18 @@ func (h *Handler) respondAuthenticated(w http.ResponseWriter, r *http.Request, u return } if wantHtml(r) { - // TODO: set livetime cookie := &http.Cookie{ Name: h.config.CookieName, Value: token, 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) @@ -220,8 +229,12 @@ func (h *Handler) getToken(r *http.Request) (userInfo model.UserInfo, valid bool return model.UserInfo{}, false } - u, v := token.Claims.(*model.UserInfo) - return *u, v + u, ok := token.Claims.(*model.UserInfo) + if !ok { + return model.UserInfo{}, false + } + + return *u, u.Valid() == nil } func (h *Handler) respondError(w http.ResponseWriter, r *http.Request) { diff --git a/login/handler_test.go b/login/handler_test.go @@ -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" @@ -278,7 +307,7 @@ func TestHandler_LoginError(t *testing.T) { func TestHandler_getToken_Valid(t *testing.T) { h := testHandler() - input := model.UserInfo{Sub: "marvin"} + input := model.UserInfo{Sub: "marvin", Expiry: time.Now().Add(time.Second).Unix()} token, err := h.createToken(input) assert.NoError(t, err) r := &http.Request{ @@ -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 +} diff --git a/model/user_info.go b/model/user_info.go @@ -1,15 +1,24 @@ package model +import ( + "errors" + "time" +) + type UserInfo struct { Sub string `json:"sub"` Picture string `json:"picture,omitempty"` Name string `json:"name,omitempty"` Email string `json:"email,omitempty"` Origin string `json:"origin,omitempty"` + Expiry int64 `json:"exp,omitempty"` } // this interface implementation // lets us use the user info as Claim for jwt-go func (u UserInfo) Valid() error { + if u.Expiry < time.Now().Unix() { + return errors.New("token expired") + } return nil } diff --git a/model/user_info_test.go b/model/user_info_test.go @@ -0,0 +1,13 @@ +package model + +import ( + . "github.com/stretchr/testify/assert" + "testing" + "time" +) + +func Test_UserInfo_Valid(t *testing.T) { + Error(t, UserInfo{Expiry: 0}.Valid()) + Error(t, UserInfo{Expiry: time.Now().Add(-1 * time.Second).Unix()}.Valid()) + NoError(t, UserInfo{Expiry: time.Now().Add(time.Second).Unix()}.Valid()) +}