actually working with caddy now
This commit is contained in:
parent
35a30f21d4
commit
cc53ee7ad8
43
README.md
43
README.md
|
@ -9,7 +9,6 @@ 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`.
|
||||
|
@ -22,11 +21,25 @@ 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
|
||||
|
||||
You need to have traefik forward the Path `/` to this application.
|
||||
|
||||
I only use docker swarm. You'd do something like the following:
|
||||
I don't use Traefik any longer, but when I did,
|
||||
I had it set up like this:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
|
@ -53,10 +66,20 @@ secrets:
|
|||
name: password-v1
|
||||
```
|
||||
|
||||
## Note
|
||||
# How It Works
|
||||
|
||||
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.
|
||||
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,
|
||||
but also means that if the server restarts,
|
||||
everybody has to log in again.
|
||||
|
||||
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.
|
||||
|
|
|
@ -17,23 +17,18 @@ import (
|
|||
"github.com/nealey/simpleauth/pkg/token"
|
||||
)
|
||||
|
||||
const CookieName = "auth"
|
||||
const CookieName = "simpleauth-token"
|
||||
|
||||
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()
|
||||
fmt.Println("checking", username, password)
|
||||
if crypted, ok := cryptedPasswords[username]; ok {
|
||||
fmt.Println(username, password, crypted)
|
||||
if err := c.Verify(crypted, []byte(password)); err == nil {
|
||||
return true
|
||||
} else {
|
||||
log.Println(err)
|
||||
}
|
||||
}
|
||||
return false
|
||||
|
@ -43,23 +38,19 @@ 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) {
|
||||
// Bypass logging and cookie setting:
|
||||
// otherwise there is a torrent of logs
|
||||
w.Write(successHtml)
|
||||
fmt.Print(w, "Valid token")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
authenticated := ""
|
||||
acceptsHtml := false
|
||||
if strings.Contains(req.Header.Get("Accept"), "text/html") {
|
||||
acceptsHtml = true
|
||||
}
|
||||
|
||||
authenticated := false
|
||||
if username, password, ok := req.BasicAuth(); ok {
|
||||
if authenticationValid(username, password) {
|
||||
authenticated = "HTTP-Basic"
|
||||
}
|
||||
}
|
||||
|
||||
if authenticationValid(req.FormValue("username"), req.FormValue("password")) {
|
||||
authenticated = "Form"
|
||||
authenticated = authenticationValid(username, password)
|
||||
}
|
||||
|
||||
// Log the request
|
||||
|
@ -67,12 +58,19 @@ func rootHandler(w http.ResponseWriter, req *http.Request) {
|
|||
if clientIP == "" {
|
||||
clientIP = req.RemoteAddr
|
||||
}
|
||||
log.Printf("%s %s %s [%s]", clientIP, req.Method, req.URL, authenticated)
|
||||
log.Printf("%s %s %s [auth:%v]", 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.Write(loginHtml)
|
||||
} else {
|
||||
return
|
||||
}
|
||||
|
||||
// Set Cookie
|
||||
t := token.New(secret, time.Now().Add(lifespan))
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: CookieName,
|
||||
|
@ -81,9 +79,19 @@ func rootHandler(w http.ResponseWriter, req *http.Request) {
|
|||
Secure: true,
|
||||
SameSite: http.SameSiteStrictMode,
|
||||
})
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write(successHtml)
|
||||
|
||||
// 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() {
|
||||
|
@ -127,7 +135,6 @@ func main() {
|
|||
if len(parts) >= 2 {
|
||||
username := parts[0]
|
||||
password := parts[1]
|
||||
fmt.Println(username, password)
|
||||
cryptedPasswords[username] = password
|
||||
}
|
||||
}
|
||||
|
@ -139,10 +146,6 @@ 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,7 +3,6 @@
|
|||
<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;
|
||||
|
@ -25,18 +24,34 @@
|
|||
|
||||
async function login(evt) {
|
||||
evt.preventDefault()
|
||||
let req = await fetch(evt.target.action, {
|
||||
method: evt.target.method,
|
||||
body: new FormData(evt.target),
|
||||
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",
|
||||
})
|
||||
if (! req.ok) {
|
||||
error(req.statusText || "Authentication failed")
|
||||
return
|
||||
}
|
||||
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")
|
||||
}
|
||||
function init() {
|
||||
}
|
||||
|
||||
async function init() {
|
||||
document.querySelector("form").addEventListener("submit", login)
|
||||
}
|
||||
|
||||
|
@ -44,8 +59,8 @@
|
|||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Log In</h1>
|
||||
<form action="/" method="post">
|
||||
<h1>Login</h1>
|
||||
<form>
|
||||
<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>
|
||||
|
|
|
@ -1,23 +0,0 @@
|
|||
<!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