login: set encrypted "remember me" token
This commit is contained in:
parent
ee3f66c24c
commit
9465f8db6d
7 changed files with 130 additions and 13 deletions
|
@ -8,6 +8,7 @@ import (
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
"git.sr.ht/~emersion/alps"
|
"git.sr.ht/~emersion/alps"
|
||||||
|
"github.com/fernet/fernet-go"
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
"github.com/labstack/echo/v4/middleware"
|
"github.com/labstack/echo/v4/middleware"
|
||||||
"github.com/labstack/gommon/log"
|
"github.com/labstack/gommon/log"
|
||||||
|
@ -21,11 +22,15 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
var options alps.Options
|
var (
|
||||||
var addr string
|
addr string
|
||||||
|
loginKey string
|
||||||
|
options alps.Options
|
||||||
|
)
|
||||||
flag.StringVar(&options.Theme, "theme", "", "default theme")
|
flag.StringVar(&options.Theme, "theme", "", "default theme")
|
||||||
flag.StringVar(&addr, "addr", ":1323", "listening address")
|
flag.StringVar(&addr, "addr", ":1323", "listening address")
|
||||||
flag.BoolVar(&options.Debug, "debug", false, "enable debug logs")
|
flag.BoolVar(&options.Debug, "debug", false, "enable debug logs")
|
||||||
|
flag.StringVar(&loginKey, "login-key", "", "Fernet key for login persistence")
|
||||||
|
|
||||||
flag.Usage = func() {
|
flag.Usage = func() {
|
||||||
fmt.Fprintf(flag.CommandLine.Output(), "usage: alps [options...] <upstream servers...>\n")
|
fmt.Fprintf(flag.CommandLine.Output(), "usage: alps [options...] <upstream servers...>\n")
|
||||||
|
@ -40,6 +45,15 @@ func main() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if loginKey != "" {
|
||||||
|
fernetKey, err := fernet.DecodeKey(loginKey)
|
||||||
|
if err != nil {
|
||||||
|
flag.Usage()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
options.LoginKey = fernetKey
|
||||||
|
}
|
||||||
|
|
||||||
e := echo.New()
|
e := echo.New()
|
||||||
e.HideBanner = true
|
e.HideBanner = true
|
||||||
if l, ok := e.Logger.(*log.Logger); ok {
|
if l, ok := e.Logger.(*log.Logger); ok {
|
||||||
|
|
1
go.mod
1
go.mod
|
@ -15,6 +15,7 @@ require (
|
||||||
github.com/emersion/go-smtp v0.13.0
|
github.com/emersion/go-smtp v0.13.0
|
||||||
github.com/emersion/go-vcard v0.0.0-20200508080525-dd3110a24ec2
|
github.com/emersion/go-vcard v0.0.0-20200508080525-dd3110a24ec2
|
||||||
github.com/emersion/go-webdav v0.3.1-0.20200513144525-a4e0e8100397
|
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/google/uuid v1.1.1
|
||||||
github.com/gorilla/css v1.0.0 // indirect
|
github.com/gorilla/css v1.0.0 // indirect
|
||||||
github.com/labstack/echo/v4 v4.1.16
|
github.com/labstack/echo/v4 v4.1.16
|
||||||
|
|
2
go.sum
2
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-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 h1:XVnGMemAywvBnsUAIsx4v+avxzauS00Mf9l9oM9olFc=
|
||||||
github.com/emersion/go-webdav v0.3.1-0.20200513144525-a4e0e8100397/go.mod h1:uSM1VveeKtogBVWaYccTksToczooJ0rrVGNsgnDsr4Q=
|
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 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
|
||||||
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY=
|
github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY=
|
||||||
|
|
|
@ -160,6 +160,12 @@ func handleGetMailbox(ctx *alps.Context) error {
|
||||||
func handleLogin(ctx *alps.Context) error {
|
func handleLogin(ctx *alps.Context) error {
|
||||||
username := ctx.FormValue("username")
|
username := ctx.FormValue("username")
|
||||||
password := ctx.FormValue("password")
|
password := ctx.FormValue("password")
|
||||||
|
remember := ctx.FormValue("remember-me")
|
||||||
|
|
||||||
|
if username == "" && password == "" {
|
||||||
|
username, password = ctx.GetLoginToken()
|
||||||
|
}
|
||||||
|
|
||||||
if username != "" && password != "" {
|
if username != "" && password != "" {
|
||||||
s, err := ctx.Server.Sessions.Put(username, password)
|
s, err := ctx.Server.Sessions.Put(username, password)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -170,18 +176,30 @@ func handleLogin(ctx *alps.Context) error {
|
||||||
}
|
}
|
||||||
ctx.SetSession(s)
|
ctx.SetSession(s)
|
||||||
|
|
||||||
|
if remember == "on" {
|
||||||
|
ctx.SetLoginToken(username, password)
|
||||||
|
}
|
||||||
|
|
||||||
if path := ctx.QueryParam("next"); path != "" && path[0] == '/' && path != "/login" {
|
if path := ctx.QueryParam("next"); path != "" && path[0] == '/' && path != "/login" {
|
||||||
return ctx.Redirect(http.StatusFound, path)
|
return ctx.Redirect(http.StatusFound, path)
|
||||||
}
|
}
|
||||||
return ctx.Redirect(http.StatusFound, "/mailbox/INBOX")
|
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 {
|
func handleLogout(ctx *alps.Context) error {
|
||||||
ctx.Session.Close()
|
ctx.Session.Close()
|
||||||
ctx.SetSession(nil)
|
ctx.SetSession(nil)
|
||||||
|
ctx.SetLoginToken("", "")
|
||||||
return ctx.Redirect(http.StatusFound, "/login")
|
return ctx.Redirect(http.StatusFound, "/login")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
79
server.go
79
server.go
|
@ -1,7 +1,9 @@
|
||||||
package alps
|
package alps
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
|
@ -9,14 +11,19 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/labstack/echo/v4"
|
"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.
|
// Server holds all the alps server state.
|
||||||
type Server struct {
|
type Server struct {
|
||||||
e *echo.Echo
|
e *echo.Echo
|
||||||
Sessions *SessionManager
|
Sessions *SessionManager
|
||||||
|
Options *Options
|
||||||
|
|
||||||
mutex sync.RWMutex // used for server reload
|
mutex sync.RWMutex // used for server reload
|
||||||
plugins []Plugin
|
plugins []Plugin
|
||||||
|
@ -34,11 +41,10 @@ type Server struct {
|
||||||
tls bool
|
tls bool
|
||||||
insecure bool
|
insecure bool
|
||||||
}
|
}
|
||||||
defaultTheme string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func newServer(e *echo.Echo, options *Options) (*Server, error) {
|
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))
|
s.upstreams = make(map[string]*url.URL, len(options.Upstreams))
|
||||||
for _, upstream := range options.Upstreams {
|
for _, upstream := range options.Upstreams {
|
||||||
|
@ -195,7 +201,7 @@ func (s *Server) load() error {
|
||||||
plugins = append(plugins, l...)
|
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 {
|
if err := renderer.Load(plugins); err != nil {
|
||||||
return fmt.Errorf("failed to load templates: %v", err)
|
return fmt.Errorf("failed to load templates: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -262,6 +268,70 @@ func (ctx *Context) SetSession(s *Session) {
|
||||||
ctx.SetCookie(&cookie)
|
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 {
|
func isPublic(path string) bool {
|
||||||
if strings.HasPrefix(path, "/plugins/") {
|
if strings.HasPrefix(path, "/plugins/") {
|
||||||
parts := strings.Split(path, "/")
|
parts := strings.Split(path, "/")
|
||||||
|
@ -292,6 +362,7 @@ type Options struct {
|
||||||
Upstreams []string
|
Upstreams []string
|
||||||
Theme string
|
Theme string
|
||||||
Debug bool
|
Debug bool
|
||||||
|
LoginKey *fernet.Key
|
||||||
}
|
}
|
||||||
|
|
||||||
// New creates a new server.
|
// New creates a new server.
|
||||||
|
|
|
@ -398,6 +398,12 @@ main table tfoot {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.action-group .checkbox input {
|
||||||
|
display: inline;
|
||||||
|
width: 1rem;
|
||||||
|
float: left;
|
||||||
|
}
|
||||||
|
|
||||||
.actions-message,
|
.actions-message,
|
||||||
.actions-contacts {
|
.actions-contacts {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
|
@ -6,19 +6,24 @@
|
||||||
|
|
||||||
<form method="post" action="/login">
|
<form method="post" action="/login">
|
||||||
<div class="action-group">
|
<div class="action-group">
|
||||||
<label for="username">
|
<label for="username">Username</label>
|
||||||
<strong>Username</strong>
|
|
||||||
</label>
|
|
||||||
<input type="text" name="username" id="username" autofocus />
|
<input type="text" name="username" id="username" autofocus />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="action-group">
|
<div class="action-group">
|
||||||
<label for="password">
|
<label for="password">Password</label>
|
||||||
<strong>Password</strong>
|
|
||||||
</label>
|
|
||||||
<input type="password" name="password" id="password" />
|
<input type="password" name="password" id="password" />
|
||||||
</div>
|
</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">
|
<div class="action-group">
|
||||||
<button type="submit">Sign in</button>
|
<button type="submit">Sign in</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
Loading…
Reference in a new issue