~emersion/alps-dev

login: set encrypted "remember me" token v3 APPLIED

Drew DeVault: 1
 login: set encrypted "remember me" token

 7 files changed, 130 insertions(+), 13 deletions(-)
Export patchset (mbox)
How do I use this?

Copy & paste the following snippet into your terminal to import this patchset into git:

curl -s https://lists.sr.ht/~emersion/alps-dev/patches/10715/mbox | git am -3
Learn more about email & git
View this thread in the archives

[PATCH v3] login: set encrypted "remember me" token Export this patch

---
Squashes the two separate patches together and updates to address
feedback.

 cmd/alps/main.go             | 18 +++++++-
 go.mod                       |  1 +
 go.sum                       |  2 +
 plugins/base/routes.go       | 20 ++++++++-
 server.go                    | 79 ++++++++++++++++++++++++++++++++++--
 themes/alps/assets/style.css |  6 +++
 themes/alps/login.html       | 17 +++++---
 7 files changed, 130 insertions(+), 13 deletions(-)

diff --git a/cmd/alps/main.go b/cmd/alps/main.go
index c0b77f2..4c358d5 100644
--- a/cmd/alps/main.go
+++ b/cmd/alps/main.go
@@ -8,6 +8,7 @@ import (
	"syscall"

	"git.sr.ht/~emersion/alps"
	"github.com/fernet/fernet-go"
	"github.com/labstack/echo/v4"
	"github.com/labstack/echo/v4/middleware"
	"github.com/labstack/gommon/log"
@@ -21,11 +22,15 @@ import (
)

func main() {
	var options alps.Options
	var addr string
	var (
		addr     string
		loginKey string
		options  alps.Options
	)
	flag.StringVar(&options.Theme, "theme", "", "default theme")
	flag.StringVar(&addr, "addr", ":1323", "listening address")
	flag.BoolVar(&options.Debug, "debug", false, "enable debug logs")
	flag.StringVar(&loginKey, "login-key", "", "Fernet key for login persistence")

	flag.Usage = func() {
		fmt.Fprintf(flag.CommandLine.Output(), "usage: alps [options...] <upstream servers...>\n")
@@ -40,6 +45,15 @@ func main() {
		return
	}

	if loginKey != "" {
		fernetKey, err := fernet.DecodeKey(loginKey)
		if err != nil {
			flag.Usage()
			return
		}
		options.LoginKey = fernetKey
	}

	e := echo.New()
	e.HideBanner = true
	if l, ok := e.Logger.(*log.Logger); ok {
diff --git a/go.mod b/go.mod
index 6d761e8..e0f022b 100644
--- a/go.mod
+++ b/go.mod
@@ -15,6 +15,7 @@ require (
	github.com/emersion/go-smtp v0.13.0
	github.com/emersion/go-vcard v0.0.0-20200508080525-dd3110a24ec2
	github.com/emersion/go-webdav v0.3.1-0.20200513144525-a4e0e8100397
	github.com/fernet/fernet-go v0.0.0-20191111064656-eff2850e6001
	github.com/google/uuid v1.1.1
	github.com/gorilla/css v1.0.0 // indirect
	github.com/labstack/echo/v4 v4.1.16
diff --git a/go.sum b/go.sum
index 8a191bb..a0b4496 100644
--- a/go.sum
+++ b/go.sum
@@ -42,6 +42,8 @@ github.com/emersion/go-vcard v0.0.0-20200508080525-dd3110a24ec2 h1:g1RgqggIPPkEB
github.com/emersion/go-vcard v0.0.0-20200508080525-dd3110a24ec2/go.mod h1:HMJKR5wlh/ziNp+sHEDV2ltblO4JD2+IdDOWtGcQBTM=
github.com/emersion/go-webdav v0.3.1-0.20200513144525-a4e0e8100397 h1:XVnGMemAywvBnsUAIsx4v+avxzauS00Mf9l9oM9olFc=
github.com/emersion/go-webdav v0.3.1-0.20200513144525-a4e0e8100397/go.mod h1:uSM1VveeKtogBVWaYccTksToczooJ0rrVGNsgnDsr4Q=
github.com/fernet/fernet-go v0.0.0-20191111064656-eff2850e6001 h1:/UMxx5lGDg30aioUL9e7xJnbJfJeX7vhcm57fa5udaI=
github.com/fernet/fernet-go v0.0.0-20191111064656-eff2850e6001/go.mod h1:2H9hjfbpSMHwY503FclkV/lZTBh2YlOmLLSda12uL8c=
github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY=
diff --git a/plugins/base/routes.go b/plugins/base/routes.go
index 3b47668..d023b4b 100644
--- a/plugins/base/routes.go
+++ b/plugins/base/routes.go
@@ -160,6 +160,12 @@ func handleGetMailbox(ctx *alps.Context) error {
func handleLogin(ctx *alps.Context) error {
	username := ctx.FormValue("username")
	password := ctx.FormValue("password")
	remember := ctx.FormValue("remember-me")

	if username == "" && password == "" {
		username, password = ctx.GetLoginToken()
	}

	if username != "" && password != "" {
		s, err := ctx.Server.Sessions.Put(username, password)
		if err != nil {
@@ -170,18 +176,30 @@ func handleLogin(ctx *alps.Context) error {
		}
		ctx.SetSession(s)

		if remember == "on" {
			ctx.SetLoginToken(username, password)
		}

		if path := ctx.QueryParam("next"); path != "" && path[0] == '/' && path != "/login" {
			return ctx.Redirect(http.StatusFound, path)
		}
		return ctx.Redirect(http.StatusFound, "/mailbox/INBOX")
	}

	return ctx.Render(http.StatusOK, "login.html", alps.NewBaseRenderData(ctx))
	return ctx.Render(http.StatusOK, "login.html",
		&struct {
			alps.BaseRenderData
			CanRememberMe bool
		}{
			BaseRenderData: *alps.NewBaseRenderData(ctx),
			CanRememberMe:  ctx.Server.Options.LoginKey != nil,
		})
}

func handleLogout(ctx *alps.Context) error {
	ctx.Session.Close()
	ctx.SetSession(nil)
	ctx.SetLoginToken("", "")
	return ctx.Redirect(http.StatusFound, "/login")
}

diff --git a/server.go b/server.go
index 250e932..f6ef5ae 100644
--- a/server.go
+++ b/server.go
@@ -1,7 +1,9 @@
package alps

import (
	"encoding/json"
	"fmt"
	"log"
	"net/http"
	"net/url"
	"strings"
@@ -9,14 +11,19 @@ import (
	"time"

	"github.com/labstack/echo/v4"
	"github.com/fernet/fernet-go"
)

const cookieName = "alps_session"
const (
	cookieName           = "alps_session"
	loginTokenCookieName = "alps_login_token"
)

// Server holds all the alps server state.
type Server struct {
	e        *echo.Echo
	Sessions *SessionManager
	Options  *Options

	mutex   sync.RWMutex // used for server reload
	plugins []Plugin
@@ -34,11 +41,10 @@ type Server struct {
		tls      bool
		insecure bool
	}
	defaultTheme string
}

func newServer(e *echo.Echo, options *Options) (*Server, error) {
	s := &Server{e: e, defaultTheme: options.Theme}
	s := &Server{e: e, Options: options}

	s.upstreams = make(map[string]*url.URL, len(options.Upstreams))
	for _, upstream := range options.Upstreams {
@@ -195,7 +201,7 @@ func (s *Server) load() error {
		plugins = append(plugins, l...)
	}

	renderer := newRenderer(s.e.Logger, s.defaultTheme)
	renderer := newRenderer(s.e.Logger, s.Options.Theme)
	if err := renderer.Load(plugins); err != nil {
		return fmt.Errorf("failed to load templates: %v", err)
	}
@@ -262,6 +268,70 @@ func (ctx *Context) SetSession(s *Session) {
	ctx.SetCookie(&cookie)
}

type loginToken struct {
	Username string
	Password string
}

func (ctx *Context) SetLoginToken(username, password string) {
	cookie := http.Cookie{
		Expires:  time.Now().Add(30 * 24 * time.Hour),
		Name:     loginTokenCookieName,
		HttpOnly: true,
		Path:     "/login",
	}
	if username == "" {
		cookie.Expires = aLongTimeAgo // unset the cookie
		ctx.SetCookie(&cookie)
		return
	}

	loginToken := loginToken{username, password}
	payload, err := json.Marshal(loginToken)
	if err != nil {
		panic(err) // Should never happen
	}
	fkey := ctx.Server.Options.LoginKey
	if fkey == nil {
		return
	}

	bytes, err := fernet.EncryptAndSign(payload, fkey)
	if err != nil {
		log.Printf("Warning: login token encryption failed: %v", err)
		return
	}

	cookie.Value = string(bytes)
	ctx.SetCookie(&cookie)
}

func (ctx *Context) GetLoginToken() (string, string) {
	cookie, err := ctx.Cookie(loginTokenCookieName)
	if err != nil || cookie == nil {
		return "", ""
	}

	fkey := ctx.Server.Options.LoginKey
	if fkey == nil {
		return "", ""
	}

	bytes := fernet.VerifyAndDecrypt([]byte(cookie.Value),
		24 * time.Hour * 30, []*fernet.Key{fkey})
	if bytes == nil {
		return "", ""
	}

	var token loginToken
	err = json.Unmarshal(bytes, &token)
	if err != nil {
		panic(err) // Should never happen
	}

	return token.Username, token.Password
}

func isPublic(path string) bool {
	if strings.HasPrefix(path, "/plugins/") {
		parts := strings.Split(path, "/")
@@ -292,6 +362,7 @@ type Options struct {
	Upstreams []string
	Theme     string
	Debug     bool
	LoginKey  *fernet.Key
}

// New creates a new server.
diff --git a/themes/alps/assets/style.css b/themes/alps/assets/style.css
index 2c8ce16..5225b9d 100644
--- a/themes/alps/assets/style.css
+++ b/themes/alps/assets/style.css
@@ -398,6 +398,12 @@ main table tfoot {
  width: 100%;
}

.action-group .checkbox input {
  display: inline;
  width: 1rem;
  float: left;
}

.actions-message,
.actions-contacts {
  display: flex;
diff --git a/themes/alps/login.html b/themes/alps/login.html
index e642050..6f7c372 100644
--- a/themes/alps/login.html
+++ b/themes/alps/login.html
@@ -6,19 +6,24 @@

    <form method="post" action="/login">
      <div class="action-group">
        <label for="username">
          <strong>Username</strong>
        </label>
        <label for="username">Username</label>
        <input type="text" name="username" id="username" autofocus />
      </div>

      <div class="action-group">
        <label for="password">
          <strong>Password</strong>
        </label>
        <label for="password">Password</label>
        <input type="password" name="password" id="password" />
      </div>

      {{if .CanRememberMe}}
      <div class="action-group">
        <label for="remember-me" class="checkbox">
          <input type="checkbox" name="remember-me" id="remember-me" />
          Remember me
        </label>
      </div>
      {{end}}

      <div class="action-group">
        <button type="submit">Sign in</button>
      </div>
-- 
2.26.2
Nice, pushed. Thanks!