fixes
This commit is contained in:
parent
e013bb6a9a
commit
78d532a88b
2
build.sh
2
build.sh
|
@ -4,4 +4,4 @@ set -e
|
|||
|
||||
tag=git.woozle.org/neale/simpleauth:latest
|
||||
|
||||
docker buildx --push --tag $tag $(dirname $0)/.
|
||||
docker buildx build --push --tag $tag $(dirname $0)/.
|
||||
|
|
|
@ -12,9 +12,9 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
"git.woozle.org/neale/simpleauth/pkg/token"
|
||||
"github.com/GehirnInc/crypt"
|
||||
_ "github.com/GehirnInc/crypt/sha256_crypt"
|
||||
"git.woozle.org/neale/simpleauth/pkg/token"
|
||||
)
|
||||
|
||||
const CookieName = "simpleauth-token"
|
||||
|
@ -34,23 +34,46 @@ func authenticationValid(username, password string) bool {
|
|||
return false
|
||||
}
|
||||
|
||||
func rootHandler(w http.ResponseWriter, req *http.Request) {
|
||||
func usernameIfAuthenticated(req *http.Request) string {
|
||||
if cookie, err := req.Cookie(CookieName); err == nil {
|
||||
t, _ := token.ParseString(cookie.Value)
|
||||
if t.Valid(secret) {
|
||||
fmt.Print(w, "Valid token")
|
||||
return
|
||||
return t.Username
|
||||
}
|
||||
}
|
||||
|
||||
acceptsHtml := false
|
||||
if strings.Contains(req.Header.Get("Accept"), "text/html") {
|
||||
acceptsHtml = true
|
||||
authUsername, authPassword, ok := req.BasicAuth()
|
||||
if ok {
|
||||
if authenticationValid(authUsername, authPassword) {
|
||||
return authUsername
|
||||
}
|
||||
}
|
||||
|
||||
authenticated := false
|
||||
if username, password, ok := req.BasicAuth(); ok {
|
||||
authenticated = authenticationValid(username, password)
|
||||
return ""
|
||||
}
|
||||
|
||||
func rootHandler(w http.ResponseWriter, req *http.Request) {
|
||||
var status string
|
||||
username := usernameIfAuthenticated(req)
|
||||
if username == "" {
|
||||
status = "failed"
|
||||
} else {
|
||||
status = "succeeded"
|
||||
w.Header().Set("X-Simpleauth-Username", username)
|
||||
|
||||
if req.Header.Get("X-Simpleauth-Login") != "true" {
|
||||
// This is the only time simpleauth returns 200
|
||||
// That will cause Caddy to proceed with the original request
|
||||
http.Error(w, "Success", http.StatusOK)
|
||||
return
|
||||
}
|
||||
// Send back a token; this will turn into a cookie
|
||||
t := token.New(secret, username, time.Now().Add(lifespan))
|
||||
w.Header().Set("X-Simpleauth-Cookie", fmt.Sprintf("%s=%s", CookieName, t.String()))
|
||||
w.Header().Set("X-Simpleauth-Token", t.String())
|
||||
// Fall through to the 401 response, though,
|
||||
// so that Caddy will send our response back to the client,
|
||||
// which needs these headers to set the cookie and try again.
|
||||
}
|
||||
|
||||
// Log the request
|
||||
|
@ -58,40 +81,17 @@ func rootHandler(w http.ResponseWriter, req *http.Request) {
|
|||
if clientIP == "" {
|
||||
clientIP = req.RemoteAddr
|
||||
}
|
||||
log.Printf("%s %s %s [auth:%v]", clientIP, req.Method, req.URL, authenticated)
|
||||
log.Println(clientIP, req.Method, req.URL, status, username)
|
||||
|
||||
if !authenticated {
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
if !acceptsHtml {
|
||||
w.Header().Set("WWW-Authenticate", "Basic realm=\"simpleauth\"")
|
||||
}
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
w.Write(loginHtml)
|
||||
return
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
w.Header().Set("X-Simpleauth-Authentication", status)
|
||||
w.Header().Set("WWW-Authenticate", "Simpleauth-Login")
|
||||
if !strings.Contains(req.Header.Get("Accept"), "text/html") {
|
||||
// Make browsers use our login form instead of basic auth
|
||||
w.Header().Add("WWW-Authenticate", "Basic realm=\"simpleauth\"")
|
||||
}
|
||||
|
||||
// Set Cookie
|
||||
t := token.New(secret, time.Now().Add(lifespan))
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: CookieName,
|
||||
Value: t.String(),
|
||||
Path: "/",
|
||||
Secure: true,
|
||||
SameSite: http.SameSiteStrictMode,
|
||||
})
|
||||
|
||||
// Set cookie value in our fancypants header
|
||||
w.Header().Set("X-Simpleauth-Token", t.String())
|
||||
|
||||
if req.Header.Get("X-Simpleauth-Login") != "" {
|
||||
// Caddy treats any response <300 as "please serve original content",
|
||||
// so we'll use 302 (Found).
|
||||
// According to RFC9110, the server SHOULD send a Location header with 302.
|
||||
// We don't do that, because we don't know where to send you.
|
||||
// It's possible 300 is a less incorrect code to use here.
|
||||
w.WriteHeader(http.StatusFound)
|
||||
}
|
||||
fmt.Fprintln(w, "Authenticated")
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
w.Write(loginHtml)
|
||||
}
|
||||
|
||||
func main() {
|
||||
|
@ -154,7 +154,7 @@ func main() {
|
|||
}
|
||||
defer f.Close()
|
||||
l, err := f.Read(secret)
|
||||
if l < 8 {
|
||||
if l < 64 {
|
||||
log.Fatalf("Secret file provided %d bytes. That's not enough bytes!", l)
|
||||
} else if err != nil {
|
||||
log.Fatal(err)
|
||||
|
|
|
@ -5,38 +5,47 @@ import (
|
|||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/binary"
|
||||
"encoding/gob"
|
||||
"log"
|
||||
"time"
|
||||
)
|
||||
|
||||
type T struct {
|
||||
expiration time.Time
|
||||
mac []byte
|
||||
Expiration time.Time
|
||||
Username string
|
||||
Mac []byte
|
||||
}
|
||||
|
||||
func (t T) computeMac(secret []byte) []byte {
|
||||
zt := t
|
||||
zt.Mac = nil
|
||||
|
||||
mac := hmac.New(sha256.New, secret)
|
||||
binary.Write(mac, binary.BigEndian, t.expiration)
|
||||
mac.Write(zt.Bytes())
|
||||
return mac.Sum([]byte{})
|
||||
}
|
||||
|
||||
// String returns the string encoding of the token
|
||||
func (t T) String() string {
|
||||
// Bytes encodes the token
|
||||
func (t T) Bytes() []byte {
|
||||
f := new(bytes.Buffer)
|
||||
if err := binary.Write(f, binary.BigEndian, t.expiration.Unix()); err != nil {
|
||||
enc := gob.NewEncoder(f)
|
||||
if err := enc.Encode(t); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
f.Write(t.mac)
|
||||
return base64.StdEncoding.EncodeToString(f.Bytes())
|
||||
return f.Bytes()
|
||||
}
|
||||
|
||||
// String returns the ASCII string encoding of the token
|
||||
func (t T) String() string {
|
||||
return base64.StdEncoding.EncodeToString(t.Bytes())
|
||||
}
|
||||
|
||||
// Valid returns true iff the token is valid for the given secret and current time
|
||||
func (t T) Valid(secret []byte) bool {
|
||||
if time.Now().After(t.expiration) {
|
||||
if time.Now().After(t.Expiration) {
|
||||
return false
|
||||
}
|
||||
if !hmac.Equal(t.mac, t.computeMac(secret)) {
|
||||
if !hmac.Equal(t.Mac, t.computeMac(secret)) {
|
||||
return false
|
||||
}
|
||||
|
||||
|
@ -44,36 +53,25 @@ func (t T) Valid(secret []byte) bool {
|
|||
}
|
||||
|
||||
// New returns a new token
|
||||
func New(secret []byte, expiration time.Time) T {
|
||||
func New(secret []byte, username string, expiration time.Time) T {
|
||||
t := T{
|
||||
expiration: expiration,
|
||||
Username: username,
|
||||
Expiration: expiration,
|
||||
}
|
||||
t.mac = t.computeMac(secret)
|
||||
t.Mac = t.computeMac(secret)
|
||||
return t
|
||||
}
|
||||
|
||||
// Parse returns a new token from the given bytes
|
||||
func Parse(b []byte) (T, error) {
|
||||
t := T{
|
||||
mac: make([]byte, sha256.Size),
|
||||
}
|
||||
var t T
|
||||
f := bytes.NewReader(b)
|
||||
{
|
||||
var sec int64
|
||||
if err := binary.Read(f, binary.BigEndian, &sec); err != nil {
|
||||
return t, err
|
||||
}
|
||||
t.expiration = time.Unix(sec, 0)
|
||||
}
|
||||
if n, err := f.Read(t.mac); err != nil {
|
||||
return t, err
|
||||
} else {
|
||||
t.mac = t.mac[:n]
|
||||
}
|
||||
return t, nil
|
||||
dec := gob.NewDecoder(f)
|
||||
err := dec.Decode(&t)
|
||||
return t, err
|
||||
}
|
||||
|
||||
// ParseString parses a base64-encoded string, as created by T.String()
|
||||
// ParseString parses an ASCII-encoded string, as created by T.String()
|
||||
func ParseString(s string) (T, error) {
|
||||
b, err := base64.StdEncoding.DecodeString(s)
|
||||
if err != nil {
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
package token
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestToken(t *testing.T) {
|
||||
secret := []byte("bloop")
|
||||
username := "rodney"
|
||||
token := New(secret, username, time.Now().Add(10*time.Second))
|
||||
|
||||
if token.Username != username {
|
||||
t.Error("Wrong username")
|
||||
}
|
||||
if !token.Valid(secret) {
|
||||
t.Error("Not valid")
|
||||
}
|
||||
|
||||
tokenStr := token.String()
|
||||
if nt, err := ParseString(tokenStr); err != nil {
|
||||
t.Error("ParseString", err)
|
||||
} else if nt.Username != token.Username {
|
||||
t.Error("Decoded username wrong")
|
||||
}
|
||||
}
|
|
@ -19,45 +19,45 @@
|
|||
</style>
|
||||
<script>
|
||||
function error(msg) {
|
||||
document.querySelector("#error").textContent = msg
|
||||
}
|
||||
document.querySelector("#error").textContent = msg
|
||||
}
|
||||
|
||||
async function login(evt) {
|
||||
evt.preventDefault()
|
||||
let data = new FormData(evt.target)
|
||||
let username = data.get("username")
|
||||
let password = data.get("password")
|
||||
evt.preventDefault()
|
||||
let data = new FormData(evt.target)
|
||||
let username = data.get("username")
|
||||
let password = data.get("password")
|
||||
|
||||
url = new URL(evt.target.action)
|
||||
url.username = ""
|
||||
url.password = ""
|
||||
url = new URL(evt.target.action)
|
||||
url.username = ""
|
||||
url.password = ""
|
||||
|
||||
let headers = new Headers({
|
||||
"Authorization": "Basic " + btoa(username + ":" + password),
|
||||
"X-Simpleauth-Login": "true",
|
||||
})
|
||||
let req = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: headers,
|
||||
credentials: "same-origin",
|
||||
})
|
||||
if (req.status != 401) {
|
||||
let token = req.headers.get("X-Simpleauth-Token")
|
||||
if (token) {
|
||||
// Set a cookie, just in case
|
||||
let expiration = new Date()
|
||||
expiration.setFullYear(expiration.getFullYear() + 1)
|
||||
document.cookie = `simpleauth-token=${token}; expires=${expiration.toUTCString()}; path=/; Secure; SameSite=Strict`
|
||||
}
|
||||
location.reload()
|
||||
return
|
||||
let headers = new Headers({
|
||||
"Authorization": "Basic " + btoa(username + ":" + password),
|
||||
"X-Simpleauth-Login": "true",
|
||||
})
|
||||
let req = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: headers,
|
||||
})
|
||||
let cookie = req.headers.get("X-Simpleauth-Cookie")
|
||||
let domain = req.headers.get("X-Simpleauth-Domain")
|
||||
if (cookie) {
|
||||
let expiration = new Date()
|
||||
expiration.setFullYear(expiration.getFullYear() + 1)
|
||||
let cookieStr = `${cookie}; expires=${expiration.toUTCString()}; path=/; Secure; SameSite=Strict`
|
||||
if (domain) {
|
||||
cookieStr += `; domain=${domain}`
|
||||
}
|
||||
console.log(req.headers)
|
||||
error(req.statusText || "Authentication failed")
|
||||
document.cookie = cookieStr // JavaScript butt-magic!
|
||||
location.reload()
|
||||
}
|
||||
console.log(req.headers)
|
||||
error(req.statusText || "Authentication failed")
|
||||
}
|
||||
|
||||
async function init() {
|
||||
document.querySelector("form").addEventListener("submit", login)
|
||||
document.querySelector("form").addEventListener("submit", login)
|
||||
}
|
||||
|
||||
window.addEventListener("load", init)
|
||||
|
|
Loading…
Reference in New Issue