Compare commits

..

No commits in common. "3bcc903be2147206adf5c55aa683dab5f1bf82ff" and "d10c46c434f70695edb9552a7dc90b14ac9377be" have entirely different histories.

5 changed files with 90 additions and 109 deletions

View File

@ -9,6 +9,7 @@ I now use Caddy: it works with that too.
All I need is a simple password, that's easy to fill with a password manager. All I need is a simple password, that's easy to fill with a password manager.
This checks those boxes. This checks those boxes.
## Format of the `passwd` file ## Format of the `passwd` file
It's just like `/etc/shadow`. It's just like `/etc/shadow`.
@ -21,25 +22,11 @@ until there's a Go library that supports everything.
There's a program included called `crypt` that will output lines for this file. There's a program included called `crypt` that will output lines for this file.
## Installation with Caddy
Run simpleauth as a service.
Make sure it can read your `passwd` file,
which you set up in the previous section.
You'll want a section like this in your Caddyfile:
```
forward_auth simpleauth:8080 {
uri /
copy_headers X-Simpleauth-Token
}
```
## Installation with Traefik ## Installation with Traefik
I don't use Traefik any longer, but when I did, You need to have traefik forward the Path `/` to this application.
I had it set up like this:
I only use docker swarm. You'd do something like the following:
```yaml ```yaml
services: services:
@ -66,23 +53,10 @@ secrets:
name: password-v1 name: password-v1
``` ```
# How It Works ## Note
Simpleauth uses a token cookie, in addition to HTTP Basic authentication. For some reason that I haven't bothered looking into,
The token is an HMAC digest of an expiration timestamp, I have to first load `/` in the browser.
plus the timestamp. I think it has something to do with cookies going through traefik simpleauth,
When the HMAC is good, and the timestamp is in the future, and I could probably fix it with some JavaScript,
the token is a valid authentication. but this is good enough for me.
This technique means there is no persistent server storage.
If you use the default of pulling the session secret from the OS PRNG,
then everybody will have to log in again every time the server restarts.
You can use the `-secret` argument to provide a persistent secret,
so this won't happen.
Some things,
like WebDAV,
will only ever use HTTP Basic auth.
That's okay:
Simpleauth will issue a new token for every request,
and the client will ignore it.

View File

@ -9,4 +9,4 @@ RUN go install -v ./...
FROM alpine FROM alpine
COPY --from=builder /go/bin/simpleauth /bin COPY --from=builder /go/bin/simpleauth /bin
COPY --from=builder /go/src/app/static /static COPY --from=builder /go/src/app/static /static
ENTRYPOINT ["/bin/simpleauth"] CMD ["/bin/simpleauth"]

View File

@ -17,12 +17,13 @@ import (
"github.com/nealey/simpleauth/pkg/token" "github.com/nealey/simpleauth/pkg/token"
) )
const CookieName = "simpleauth-token" const CookieName = "auth"
var secret []byte = make([]byte, 256) var secret []byte = make([]byte, 256)
var lifespan time.Duration var lifespan time.Duration
var cryptedPasswords map[string]string var cryptedPasswords map[string]string
var loginHtml []byte var loginHtml []byte
var successHtml []byte
func authenticationValid(username, password string) bool { func authenticationValid(username, password string) bool {
c := crypt.SHA256.New() c := crypt.SHA256.New()
@ -38,19 +39,23 @@ func rootHandler(w http.ResponseWriter, req *http.Request) {
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") // Bypass logging and cookie setting:
// otherwise there is a torrent of logs
w.Write(successHtml)
return return
} }
} }
acceptsHtml := false authenticated := ""
if strings.Contains(req.Header.Get("Accept"), "text/html") {
acceptsHtml = true if username, password, ok := req.BasicAuth(); ok {
if authenticationValid(username, password) {
authenticated = "HTTP-Basic"
}
} }
authenticated := false if authenticationValid(req.FormValue("username"), req.FormValue("password")) {
if username, password, ok := req.BasicAuth(); ok { authenticated = "Form"
authenticated = authenticationValid(username, password)
} }
// Log the request // Log the request
@ -58,40 +63,23 @@ 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.Printf("%s %s %s [%s]", clientIP, req.Method, req.URL, authenticated)
if !authenticated { if authenticated == "" {
w.Header().Set("Content-Type", "text/html")
if !acceptsHtml {
w.Header().Set("WWW-Authenticate", "Basic realm=\"simpleauth\"")
}
w.WriteHeader(http.StatusUnauthorized) w.WriteHeader(http.StatusUnauthorized)
w.Write(loginHtml) w.Write(loginHtml)
return } else {
t := token.New(secret, time.Now().Add(lifespan))
http.SetCookie(w, &http.Cookie{
Name: CookieName,
Value: t.String(),
Path: "/",
Secure: true,
SameSite: http.SameSiteStrictMode,
})
w.WriteHeader(http.StatusOK)
w.Write(successHtml)
} }
// 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")
} }
func main() { func main() {
@ -146,6 +134,10 @@ func main() {
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
successHtml, err = ioutil.ReadFile(path.Join(*htmlPath, "success.html"))
if err != nil {
log.Fatal(err)
}
// Read in secret // Read in secret
f, err := os.Open(*secretPath) f, err := os.Open(*secretPath)

View File

@ -3,6 +3,7 @@
<head> <head>
<title>Login</title> <title>Login</title>
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" href="data:,">
<style> <style>
html { html {
font-family: sans-serif; font-family: sans-serif;
@ -19,48 +20,39 @@
</style> </style>
<script> <script>
function error(msg) { 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")
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",
})
let token = req.headers.get("X-Simpleauth-Token")
if (token) {
// Set a cookie, just in case
document.cookie = `simpleauth-token=${token}; path=/; Secure; SameSite=Strict`
location.reload(true)
} else {
error(req.statusText || "Authentication failed")
}
} }
async function init() { async function login(evt) {
document.querySelector("form").addEventListener("submit", login) evt.preventDefault()
let data = new FormData(evt.target)
let username = data.get("username")
let password = data.get("password")
let headers = new Headers({
"Authorization": "Basic " + btoa(username + ":" + password),
})
let req = await fetch(evt.target.action, {
method: "GET",
headers: headers,
credentials: "same-origin",
})
if (! req.ok) {
error(req.statusText || "Authentication failed")
return
}
location.reload(true)
}
function init() {
document.querySelector("form").addEventListener("submit", login)
} }
window.addEventListener("load", init) window.addEventListener("load", init)
</script> </script>
</head> </head>
<body> <body>
<h1>Login</h1> <h1>Log In</h1>
<form> <form action="/" method="post">
<div>Username: <input type="text" autocomplete="username" name="username"></div> <div>Username: <input type="text" autocomplete="username" name="username"></div>
<div>Password: <input type="password" autocomplete="current-password" name="password"></div> <div>Password: <input type="password" autocomplete="current-password" name="password"></div>
<div><input type="submit" value="Log In"></div> <div><input type="submit" value="Log In"></div>

23
static/success.html Normal file
View File

@ -0,0 +1,23 @@
<!DOCTYPE html>
<html>
<head>
<title>Welcome</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" href="data:,">
<style>
html {
font-family: sans-serif;
color: white;
background: seagreen linear-gradient(315deg, rgba(255,255,255,0.2), transparent);
height: 100%;
}
div {
margin: 1em;
}
</style>
</head>
<body>
<h1>Welcome</h1>
<div>You have logged in successfully.</div>
</body>
</html>