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
|
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"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"git.woozle.org/neale/simpleauth/pkg/token"
|
||||||
"github.com/GehirnInc/crypt"
|
"github.com/GehirnInc/crypt"
|
||||||
_ "github.com/GehirnInc/crypt/sha256_crypt"
|
_ "github.com/GehirnInc/crypt/sha256_crypt"
|
||||||
"git.woozle.org/neale/simpleauth/pkg/token"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const CookieName = "simpleauth-token"
|
const CookieName = "simpleauth-token"
|
||||||
|
@ -34,23 +34,46 @@ func authenticationValid(username, password string) bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func rootHandler(w http.ResponseWriter, req *http.Request) {
|
func usernameIfAuthenticated(req *http.Request) string {
|
||||||
if cookie, err := req.Cookie(CookieName); err == nil {
|
if cookie, err := req.Cookie(CookieName); err == nil {
|
||||||
t, _ := token.ParseString(cookie.Value)
|
t, _ := token.ParseString(cookie.Value)
|
||||||
if t.Valid(secret) {
|
if t.Valid(secret) {
|
||||||
fmt.Print(w, "Valid token")
|
return t.Username
|
||||||
return
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
acceptsHtml := false
|
authUsername, authPassword, ok := req.BasicAuth()
|
||||||
if strings.Contains(req.Header.Get("Accept"), "text/html") {
|
if ok {
|
||||||
acceptsHtml = true
|
if authenticationValid(authUsername, authPassword) {
|
||||||
|
return authUsername
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
authenticated := false
|
return ""
|
||||||
if username, password, ok := req.BasicAuth(); ok {
|
}
|
||||||
authenticated = authenticationValid(username, password)
|
|
||||||
|
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
|
// Log the request
|
||||||
|
@ -58,40 +81,17 @@ func rootHandler(w http.ResponseWriter, req *http.Request) {
|
||||||
if clientIP == "" {
|
if clientIP == "" {
|
||||||
clientIP = req.RemoteAddr
|
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")
|
||||||
w.Header().Set("Content-Type", "text/html")
|
w.Header().Set("X-Simpleauth-Authentication", status)
|
||||||
if !acceptsHtml {
|
w.Header().Set("WWW-Authenticate", "Simpleauth-Login")
|
||||||
w.Header().Set("WWW-Authenticate", "Basic realm=\"simpleauth\"")
|
if !strings.Contains(req.Header.Get("Accept"), "text/html") {
|
||||||
}
|
// Make browsers use our login form instead of basic auth
|
||||||
w.WriteHeader(http.StatusUnauthorized)
|
w.Header().Add("WWW-Authenticate", "Basic realm=\"simpleauth\"")
|
||||||
w.Write(loginHtml)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
w.WriteHeader(http.StatusUnauthorized)
|
||||||
// Set Cookie
|
w.Write(loginHtml)
|
||||||
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")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
@ -154,7 +154,7 @@ func main() {
|
||||||
}
|
}
|
||||||
defer f.Close()
|
defer f.Close()
|
||||||
l, err := f.Read(secret)
|
l, err := f.Read(secret)
|
||||||
if l < 8 {
|
if l < 64 {
|
||||||
log.Fatalf("Secret file provided %d bytes. That's not enough bytes!", l)
|
log.Fatalf("Secret file provided %d bytes. That's not enough bytes!", l)
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
|
|
|
@ -5,38 +5,47 @@ import (
|
||||||
"crypto/hmac"
|
"crypto/hmac"
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/binary"
|
"encoding/gob"
|
||||||
"log"
|
"log"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type T struct {
|
type T struct {
|
||||||
expiration time.Time
|
Expiration time.Time
|
||||||
mac []byte
|
Username string
|
||||||
|
Mac []byte
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t T) computeMac(secret []byte) []byte {
|
func (t T) computeMac(secret []byte) []byte {
|
||||||
|
zt := t
|
||||||
|
zt.Mac = nil
|
||||||
|
|
||||||
mac := hmac.New(sha256.New, secret)
|
mac := hmac.New(sha256.New, secret)
|
||||||
binary.Write(mac, binary.BigEndian, t.expiration)
|
mac.Write(zt.Bytes())
|
||||||
return mac.Sum([]byte{})
|
return mac.Sum([]byte{})
|
||||||
}
|
}
|
||||||
|
|
||||||
// String returns the string encoding of the token
|
// Bytes encodes the token
|
||||||
func (t T) String() string {
|
func (t T) Bytes() []byte {
|
||||||
f := new(bytes.Buffer)
|
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)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
f.Write(t.mac)
|
return f.Bytes()
|
||||||
return base64.StdEncoding.EncodeToString(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
|
// Valid returns true iff the token is valid for the given secret and current time
|
||||||
func (t T) Valid(secret []byte) bool {
|
func (t T) Valid(secret []byte) bool {
|
||||||
if time.Now().After(t.expiration) {
|
if time.Now().After(t.Expiration) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if !hmac.Equal(t.mac, t.computeMac(secret)) {
|
if !hmac.Equal(t.Mac, t.computeMac(secret)) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -44,36 +53,25 @@ func (t T) Valid(secret []byte) bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
// New returns a new token
|
// 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{
|
t := T{
|
||||||
expiration: expiration,
|
Username: username,
|
||||||
|
Expiration: expiration,
|
||||||
}
|
}
|
||||||
t.mac = t.computeMac(secret)
|
t.Mac = t.computeMac(secret)
|
||||||
return t
|
return t
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse returns a new token from the given bytes
|
// Parse returns a new token from the given bytes
|
||||||
func Parse(b []byte) (T, error) {
|
func Parse(b []byte) (T, error) {
|
||||||
t := T{
|
var t T
|
||||||
mac: make([]byte, sha256.Size),
|
|
||||||
}
|
|
||||||
f := bytes.NewReader(b)
|
f := bytes.NewReader(b)
|
||||||
{
|
dec := gob.NewDecoder(f)
|
||||||
var sec int64
|
err := dec.Decode(&t)
|
||||||
if err := binary.Read(f, binary.BigEndian, &sec); err != nil {
|
return t, err
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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) {
|
func ParseString(s string) (T, error) {
|
||||||
b, err := base64.StdEncoding.DecodeString(s)
|
b, err := base64.StdEncoding.DecodeString(s)
|
||||||
if err != nil {
|
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>
|
</style>
|
||||||
<script>
|
<script>
|
||||||
function error(msg) {
|
function error(msg) {
|
||||||
document.querySelector("#error").textContent = msg
|
document.querySelector("#error").textContent = msg
|
||||||
}
|
}
|
||||||
|
|
||||||
async function login(evt) {
|
async function login(evt) {
|
||||||
evt.preventDefault()
|
evt.preventDefault()
|
||||||
let data = new FormData(evt.target)
|
let data = new FormData(evt.target)
|
||||||
let username = data.get("username")
|
let username = data.get("username")
|
||||||
let password = data.get("password")
|
let password = data.get("password")
|
||||||
|
|
||||||
url = new URL(evt.target.action)
|
url = new URL(evt.target.action)
|
||||||
url.username = ""
|
url.username = ""
|
||||||
url.password = ""
|
url.password = ""
|
||||||
|
|
||||||
let headers = new Headers({
|
let headers = new Headers({
|
||||||
"Authorization": "Basic " + btoa(username + ":" + password),
|
"Authorization": "Basic " + btoa(username + ":" + password),
|
||||||
"X-Simpleauth-Login": "true",
|
"X-Simpleauth-Login": "true",
|
||||||
})
|
})
|
||||||
let req = await fetch(url, {
|
let req = await fetch(url, {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
headers: headers,
|
headers: headers,
|
||||||
credentials: "same-origin",
|
})
|
||||||
})
|
let cookie = req.headers.get("X-Simpleauth-Cookie")
|
||||||
if (req.status != 401) {
|
let domain = req.headers.get("X-Simpleauth-Domain")
|
||||||
let token = req.headers.get("X-Simpleauth-Token")
|
if (cookie) {
|
||||||
if (token) {
|
let expiration = new Date()
|
||||||
// Set a cookie, just in case
|
expiration.setFullYear(expiration.getFullYear() + 1)
|
||||||
let expiration = new Date()
|
let cookieStr = `${cookie}; expires=${expiration.toUTCString()}; path=/; Secure; SameSite=Strict`
|
||||||
expiration.setFullYear(expiration.getFullYear() + 1)
|
if (domain) {
|
||||||
document.cookie = `simpleauth-token=${token}; expires=${expiration.toUTCString()}; path=/; Secure; SameSite=Strict`
|
cookieStr += `; domain=${domain}`
|
||||||
}
|
|
||||||
location.reload()
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
console.log(req.headers)
|
document.cookie = cookieStr // JavaScript butt-magic!
|
||||||
error(req.statusText || "Authentication failed")
|
location.reload()
|
||||||
|
}
|
||||||
|
console.log(req.headers)
|
||||||
|
error(req.statusText || "Authentication failed")
|
||||||
}
|
}
|
||||||
|
|
||||||
async function init() {
|
async function init() {
|
||||||
document.querySelector("form").addEventListener("submit", login)
|
document.querySelector("form").addEventListener("submit", login)
|
||||||
}
|
}
|
||||||
|
|
||||||
window.addEventListener("load", init)
|
window.addEventListener("load", init)
|
||||||
|
|
Loading…
Reference in New Issue