Gibheer
8e6e01f47c
This commit replaces the various log entry points with a common logger provided via context. This is helpful as it groups all log entries together via the txid and should help in the future when debugging cascading errors.
183 lines
4.3 KiB
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 ""
|
|
}
|