Browse Source

login: set encrypted "remember me" token

master
Drew DeVault 4 years ago
committed by Simon Ser
parent
commit
9465f8db6d
No known key found for this signature in database GPG Key ID: FDE7BE0E88F5E48
7 changed files with 130 additions and 13 deletions
  1. +16
    -2
      cmd/alps/main.go
  2. +1
    -0
      go.mod
  3. +2
    -0
      go.sum
  4. +19
    -1
      plugins/base/routes.go
  5. +75
    -4
      server.go
  6. +6
    -0
      themes/alps/assets/style.css
  7. +11
    -6
      themes/alps/login.html

+ 16
- 2
cmd/alps/main.go View File

@@ -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 {


+ 1
- 0
go.mod View File

@@ -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


+ 2
- 0
go.sum View File

@@ -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=


+ 19
- 1
plugins/base/routes.go View File

@@ -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")
}



+ 75
- 4
server.go View File

@@ -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.


+ 6
- 0
themes/alps/assets/style.css View File

@@ -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;


+ 11
- 6
themes/alps/login.html View File

@@ -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>


Loading…
Cancel
Save