monzero/cmd/monfront/authenticater.go

183 lines
4.3 KiB
Go

package main
import (
"crypto/hmac"
"crypto/rand"
"crypto/sha256"
"database/sql"
"encoding/base64"
"fmt"
"net/http"
"strings"
"time"
)
const (
BasicAuthPrompt = `Basic realm="auth for monfront"`
SessionCookie = `session`
UserAnonymous = `anonymous`
)
type (
// Authenticator is a middleware taking a context and authenticating
// the user.
Authenticator struct {
db *sql.DB
Mode string
Token []byte
AllowAnonymous bool
Header string
List [][]string
ClientCA string
sessions map[string]*session // maps a session key to a user
}
session struct {
user string
t time.Time
}
)
// Handler returns the handler for the authentication configuration.
func (a *Authenticator) Handler() (func(*Context) error, error) {
switch a.Mode {
case "none":
return func(_ *Context) error { return nil }, nil
case "header":
if a.Header == "" {
return nil, fmt.Errorf("authentication mode is 'header' but no header was provided")
}
return func(c *Context) error {
if user := c.r.Header.Get(a.Header); user == "" {
if a.AllowAnonymous {
c.User = UserAnonymous
return nil
}
return a.Unauthorized(c)
} else {
c.User = user
}
return nil
}, nil
case "list":
return func(c *Context) error {
user, pass, ok := c.r.BasicAuth()
if !ok || user == "" || pass == "" {
if a.AllowAnonymous {
c.User = UserAnonymous
return nil
}
c.w.Header().Set("WWW-Authenticate", BasicAuthPrompt)
return a.Unauthorized(c)
}
var found string
for _, entry := range a.List {
if entry[0] == user {
found = entry[1]
}
}
if found == "" {
c.w.Header().Set("WWW-Authenticate", BasicAuthPrompt)
return a.Unauthorized(c)
}
p := pwHash{}
if err := p.Parse(found); err != nil {
c.log.Warn("could not parse hash for user", "user", user, "error", err)
return a.Unauthorized(c)
}
if ok, err := p.compare(pass); err != nil {
c.w.Header().Set("WWW-Authenticate", BasicAuthPrompt)
return a.Unauthorized(c)
} else if !ok {
c.w.Header().Set("WWW-Authenticate", BasicAuthPrompt)
return a.Unauthorized(c)
}
c.User = user
return nil
}, nil
case "db":
return func(c *Context) error {
sessCookie := c.GetCookieVal(SessionCookie)
if sessCookie != "" {
ses := a.getSession(sessCookie)
if ses != "" {
// TODO fix time limit to make it variable
c.SetCookie(SessionCookie, sessCookie, time.Now().Add(2*time.Hour))
c.User = ses
return nil
}
}
return fmt.Errorf("NOT YET IMPLEMENTED")
}, fmt.Errorf("NOT YET IMPLEMENTED")
case "cert":
return func(c *Context) error {
return fmt.Errorf("NOT YET IMPLEMENTED")
}, fmt.Errorf("NOT YET IMPLEMENTED")
default:
return nil, fmt.Errorf("unknown mode '%s' for authentication", a.Mode)
}
return nil, fmt.Errorf("could not create authenticator")
}
func (a *Authenticator) Unauthorized(c *Context) error {
c.w.WriteHeader(http.StatusUnauthorized)
fmt.Fprintf(c.w, "unauthorized\n")
return fmt.Errorf("no authentication")
}
// creates a session for a user
func (a *Authenticator) createSession(user string) (string, error) {
raw := make([]byte, 32)
if _, err := rand.Read(raw); err != nil {
return "", fmt.Errorf("could not generate new session key")
}
res := a.mac(raw)
ses := fmt.Sprintf(
"%s-%s",
base64.StdEncoding.EncodeToString(raw),
base64.StdEncoding.EncodeToString(res),
)
a.sessions[ses] = &session{user: user, t: time.Now()}
return ses, nil
}
func (a *Authenticator) mac(input []byte) []byte {
mac := hmac.New(sha256.New, a.Token)
mac.Write(input)
return mac.Sum(nil)
}
// getSession returns the username of the current session.
func (a *Authenticator) getSession(session string) string {
if session == "" {
return ""
}
parts := strings.Split(session, "-")
if len(parts) != 2 {
return ""
}
msg, err := base64.StdEncoding.DecodeString(parts[0])
if err != nil {
return ""
}
mac, err := base64.StdEncoding.DecodeString(parts[1])
if err != nil {
return ""
}
verify := a.mac(msg)
if !hmac.Equal(mac, verify) {
return ""
}
if ses, found := a.sessions[session]; found {
// TODO make timeout a config option
if time.Now().Sub(ses.t) < 8*time.Hour {
delete(a.sessions, session)
return ""
}
ses.t = time.Now()
return ses.user
}
return ""
}