>1 account
This commit is contained in:
parent
8b8491199c
commit
12f5bf35f6
14
README.md
14
README.md
|
@ -5,9 +5,23 @@ Upon successful login, the browser gets a cookie,
|
||||||
and further attempts to access will get the success page.
|
and further attempts to access will get the success page.
|
||||||
|
|
||||||
I made this to use with the Traefik forward-auth middleware.
|
I made this to use with the Traefik forward-auth middleware.
|
||||||
|
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
|
||||||
|
|
||||||
|
It's just like `/etc/shadow`.
|
||||||
|
|
||||||
|
username:crypted-password
|
||||||
|
|
||||||
|
We use sha256,
|
||||||
|
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 Traefik
|
## Installation with Traefik
|
||||||
|
|
||||||
You need to have traefik forward the Path `/` to this application.
|
You need to have traefik forward the Path `/` to this application.
|
||||||
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/GehirnInc/crypt"
|
||||||
|
_ "github.com/GehirnInc/crypt/sha256_crypt"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
if len(os.Args) != 3 {
|
||||||
|
log.Fatal("Usage: crypt USERNAME PASSWORD")
|
||||||
|
}
|
||||||
|
username := os.Args[1]
|
||||||
|
password := os.Args[2]
|
||||||
|
c := crypt.SHA256.New()
|
||||||
|
if crypted, err := c.Generate([]byte(password), nil); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
} else {
|
||||||
|
fmt.Printf("%s:%s\n", username, crypted)
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,7 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bufio"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
@ -11,6 +12,8 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/GehirnInc/crypt"
|
||||||
|
_ "github.com/GehirnInc/crypt/sha256_crypt"
|
||||||
"github.com/nealey/simpleauth/pkg/token"
|
"github.com/nealey/simpleauth/pkg/token"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -18,51 +21,68 @@ const CookieName = "auth"
|
||||||
|
|
||||||
var secret []byte = make([]byte, 256)
|
var secret []byte = make([]byte, 256)
|
||||||
var lifespan time.Duration
|
var lifespan time.Duration
|
||||||
var password string
|
var cryptedPasswords map[string]string
|
||||||
var loginHtml []byte
|
var loginHtml []byte
|
||||||
var successHtml []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
|
||||||
|
}
|
||||||
|
|
||||||
func rootHandler(w http.ResponseWriter, req *http.Request) {
|
func rootHandler(w http.ResponseWriter, req *http.Request) {
|
||||||
mechanism := "unauthenticated"
|
|
||||||
authenticated := false
|
|
||||||
if _, passwd, _ := req.BasicAuth(); passwd == password {
|
|
||||||
authenticated = true
|
|
||||||
mechanism = "HTTP-Basic"
|
|
||||||
}
|
|
||||||
if req.FormValue("passwd") == password {
|
|
||||||
authenticated = true
|
|
||||||
mechanism = "Form"
|
|
||||||
}
|
|
||||||
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) {
|
||||||
// Bypass logging and cookie setting:
|
// Bypass logging and cookie setting:
|
||||||
// otherwise there is a torrent of logs
|
// otherwise there is a torrent of logs
|
||||||
w.Write(successHtml)
|
w.Write(successHtml)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Log the request
|
authenticated := ""
|
||||||
clientIP := req.Header.Get("X-Real-IP")
|
|
||||||
if clientIP == "" {
|
|
||||||
clientIP = req.RemoteAddr
|
|
||||||
}
|
|
||||||
log.Printf("%s %s %s [%s]", clientIP, req.Method, req.URL, mechanism)
|
|
||||||
|
|
||||||
if authenticated {
|
if username, password, ok := req.BasicAuth(); ok {
|
||||||
|
if authenticationValid(username, password) {
|
||||||
|
authenticated = "HTTP-Basic"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if authenticationValid(req.FormValue("username"), req.FormValue("password")) {
|
||||||
|
authenticated = "Form"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log the request
|
||||||
|
clientIP := req.Header.Get("X-Real-IP")
|
||||||
|
if clientIP == "" {
|
||||||
|
clientIP = req.RemoteAddr
|
||||||
|
}
|
||||||
|
log.Printf("%s %s %s [%s]", clientIP, req.Method, req.URL, authenticated)
|
||||||
|
|
||||||
|
if authenticated == "" {
|
||||||
|
w.WriteHeader(http.StatusUnauthorized)
|
||||||
|
w.Write(loginHtml)
|
||||||
|
} else {
|
||||||
t := token.New(secret, time.Now().Add(lifespan))
|
t := token.New(secret, time.Now().Add(lifespan))
|
||||||
http.SetCookie(w, &http.Cookie{
|
http.SetCookie(w, &http.Cookie{
|
||||||
Name: CookieName,
|
Name: CookieName,
|
||||||
Value: t.String(),
|
Value: t.String(),
|
||||||
Path: "/",
|
Path: "/",
|
||||||
Secure: true,
|
Secure: true,
|
||||||
SameSite: http.SameSiteStrictMode,
|
SameSite: http.SameSiteStrictMode,
|
||||||
})
|
})
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
w.Write(successHtml)
|
w.Write(successHtml)
|
||||||
} else {
|
|
||||||
w.WriteHeader(http.StatusUnauthorized)
|
|
||||||
w.Write(loginHtml)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -80,8 +100,8 @@ func main() {
|
||||||
)
|
)
|
||||||
passwordPath := flag.String(
|
passwordPath := flag.String(
|
||||||
"passwd",
|
"passwd",
|
||||||
"/run/secrets/password",
|
"/run/secrets/passwd",
|
||||||
"Path to a file containing the password",
|
"Path to a file containing passwords",
|
||||||
)
|
)
|
||||||
secretPath := flag.String(
|
secretPath := flag.String(
|
||||||
"secret",
|
"secret",
|
||||||
|
@ -95,11 +115,25 @@ func main() {
|
||||||
)
|
)
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
passwordBytes, err := ioutil.ReadFile(*passwordPath)
|
cryptedPasswords = make(map[string]string, 10)
|
||||||
if err != nil {
|
if f, err := os.Open(*passwordPath); err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
|
} else {
|
||||||
|
defer f.Close()
|
||||||
|
scanner := bufio.NewScanner(f)
|
||||||
|
for scanner.Scan() {
|
||||||
|
line := scanner.Text()
|
||||||
|
parts := strings.Split(line, ":")
|
||||||
|
if len(parts) >= 2 {
|
||||||
|
username := parts[0]
|
||||||
|
password := parts[1]
|
||||||
|
fmt.Println(username, password)
|
||||||
|
cryptedPasswords[username] = password
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
password = strings.TrimSpace(string(passwordBytes))
|
|
||||||
|
var err error
|
||||||
|
|
||||||
loginHtml, err = ioutil.ReadFile(path.Join(*htmlPath, "login.html"))
|
loginHtml, err = ioutil.ReadFile(path.Join(*htmlPath, "login.html"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -117,8 +151,8 @@ func main() {
|
||||||
}
|
}
|
||||||
defer f.Close()
|
defer f.Close()
|
||||||
l, err := f.Read(secret)
|
l, err := f.Read(secret)
|
||||||
if l == 0 {
|
if l < 8 {
|
||||||
log.Fatal("Secret file provided 0 bytes. That's not enough bytes!")
|
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)
|
||||||
}
|
}
|
||||||
|
|
2
go.mod
2
go.mod
|
@ -1,3 +1,5 @@
|
||||||
module github.com/nealey/simpleauth
|
module github.com/nealey/simpleauth
|
||||||
|
|
||||||
go 1.13
|
go 1.13
|
||||||
|
|
||||||
|
require github.com/GehirnInc/crypt v0.0.0-20200316065508-bb7000b8a962
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
github.com/GehirnInc/crypt v0.0.0-20200316065508-bb7000b8a962 h1:KeNholpO2xKjgaaSyd+DyQRrsQjhbSeS7qe4nEw8aQw=
|
||||||
|
github.com/GehirnInc/crypt v0.0.0-20200316065508-bb7000b8a962/go.mod h1:kC29dT1vFpj7py2OvG1khBdQpo3kInWP+6QipLbdngo=
|
|
@ -46,7 +46,8 @@
|
||||||
<body>
|
<body>
|
||||||
<h1>Log In</h1>
|
<h1>Log In</h1>
|
||||||
<form action="/" method="post">
|
<form action="/" method="post">
|
||||||
<div>Password: <input type="password" name="passwd"></div>
|
<div>Username: <input name="username"></div>
|
||||||
|
<div>Password: <input type="password" name="password"></div>
|
||||||
<div><input type="submit" value="Log In"></div>
|
<div><input type="submit" value="Log In"></div>
|
||||||
</form>
|
</form>
|
||||||
<div id="error"></div>
|
<div id="error"></div>
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
html {
|
html {
|
||||||
font-family: sans-serif;
|
font-family: sans-serif;
|
||||||
color: white;
|
color: white;
|
||||||
background: seagreen linear-gradient(315, rgba(255,255,255,0.2), transparent);
|
background: seagreen linear-gradient(315deg, rgba(255,255,255,0.2), transparent);
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
div {
|
div {
|
||||||
|
|
Loading…
Reference in New Issue