Compare commits
No commits in common. "3bcc903be2147206adf5c55aa683dab5f1bf82ff" and "d10c46c434f70695edb9552a7dc90b14ac9377be" have entirely different histories.
3bcc903be2
...
d10c46c434
46
README.md
46
README.md
|
@ -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.
|
||||
This checks those boxes.
|
||||
|
||||
|
||||
## Format of the `passwd` file
|
||||
|
||||
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.
|
||||
|
||||
|
||||
## 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
|
||||
|
||||
I don't use Traefik any longer, but when I did,
|
||||
I had it set up like this:
|
||||
You need to have traefik forward the Path `/` to this application.
|
||||
|
||||
I only use docker swarm. You'd do something like the following:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
|
@ -66,23 +53,10 @@ secrets:
|
|||
name: password-v1
|
||||
```
|
||||
|
||||
# How It Works
|
||||
## Note
|
||||
|
||||
Simpleauth uses a token cookie, in addition to HTTP Basic authentication.
|
||||
The token is an HMAC digest of an expiration timestamp,
|
||||
plus the timestamp.
|
||||
When the HMAC is good, and the timestamp is in the future,
|
||||
the token is a valid authentication.
|
||||
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.
|
||||
For some reason that I haven't bothered looking into,
|
||||
I have to first load `/` in the browser.
|
||||
I think it has something to do with cookies going through traefik simpleauth,
|
||||
and I could probably fix it with some JavaScript,
|
||||
but this is good enough for me.
|
||||
|
|
|
@ -9,4 +9,4 @@ RUN go install -v ./...
|
|||
FROM alpine
|
||||
COPY --from=builder /go/bin/simpleauth /bin
|
||||
COPY --from=builder /go/src/app/static /static
|
||||
ENTRYPOINT ["/bin/simpleauth"]
|
||||
CMD ["/bin/simpleauth"]
|
||||
|
|
|
@ -17,12 +17,13 @@ import (
|
|||
"github.com/nealey/simpleauth/pkg/token"
|
||||
)
|
||||
|
||||
const CookieName = "simpleauth-token"
|
||||
const CookieName = "auth"
|
||||
|
||||
var secret []byte = make([]byte, 256)
|
||||
var lifespan time.Duration
|
||||
var cryptedPasswords map[string]string
|
||||
var loginHtml []byte
|
||||
var successHtml []byte
|
||||
|
||||
func authenticationValid(username, password string) bool {
|
||||
c := crypt.SHA256.New()
|
||||
|
@ -38,19 +39,23 @@ func rootHandler(w http.ResponseWriter, req *http.Request) {
|
|||
if cookie, err := req.Cookie(CookieName); err == nil {
|
||||
t, _ := token.ParseString(cookie.Value)
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
acceptsHtml := false
|
||||
if strings.Contains(req.Header.Get("Accept"), "text/html") {
|
||||
acceptsHtml = true
|
||||
authenticated := ""
|
||||
|
||||
if username, password, ok := req.BasicAuth(); ok {
|
||||
if authenticationValid(username, password) {
|
||||
authenticated = "HTTP-Basic"
|
||||
}
|
||||
}
|
||||
|
||||
authenticated := false
|
||||
if username, password, ok := req.BasicAuth(); ok {
|
||||
authenticated = authenticationValid(username, password)
|
||||
if authenticationValid(req.FormValue("username"), req.FormValue("password")) {
|
||||
authenticated = "Form"
|
||||
}
|
||||
|
||||
// Log the request
|
||||
|
@ -58,19 +63,12 @@ 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.Printf("%s %s %s [%s]", clientIP, req.Method, req.URL, authenticated)
|
||||
|
||||
if !authenticated {
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
if !acceptsHtml {
|
||||
w.Header().Set("WWW-Authenticate", "Basic realm=\"simpleauth\"")
|
||||
}
|
||||
if authenticated == "" {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
w.Write(loginHtml)
|
||||
return
|
||||
}
|
||||
|
||||
// Set Cookie
|
||||
} else {
|
||||
t := token.New(secret, time.Now().Add(lifespan))
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: CookieName,
|
||||
|
@ -79,19 +77,9 @@ func rootHandler(w http.ResponseWriter, req *http.Request) {
|
|||
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)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write(successHtml)
|
||||
}
|
||||
fmt.Fprintln(w, "Authenticated")
|
||||
}
|
||||
|
||||
func main() {
|
||||
|
@ -146,6 +134,10 @@ func main() {
|
|||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
successHtml, err = ioutil.ReadFile(path.Join(*htmlPath, "success.html"))
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Read in secret
|
||||
f, err := os.Open(*secretPath)
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
<head>
|
||||
<title>Login</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="icon" href="data:,">
|
||||
<style>
|
||||
html {
|
||||
font-family: sans-serif;
|
||||
|
@ -28,30 +29,21 @@
|
|||
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, {
|
||||
let req = await fetch(evt.target.action, {
|
||||
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 {
|
||||
if (! req.ok) {
|
||||
error(req.statusText || "Authentication failed")
|
||||
return
|
||||
}
|
||||
location.reload(true)
|
||||
}
|
||||
|
||||
async function init() {
|
||||
function init() {
|
||||
document.querySelector("form").addEventListener("submit", login)
|
||||
}
|
||||
|
||||
|
@ -59,8 +51,8 @@
|
|||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Login</h1>
|
||||
<form>
|
||||
<h1>Log In</h1>
|
||||
<form action="/" method="post">
|
||||
<div>Username: <input type="text" autocomplete="username" name="username"></div>
|
||||
<div>Password: <input type="password" autocomplete="current-password" name="password"></div>
|
||||
<div><input type="submit" value="Log In"></div>
|
||||
|
|
|
@ -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>
|
Loading…
Reference in New Issue